ThinkPHP v6.0.x 反序列化漏洞利用


前言:

上次做了成信大的安詢杯第二屆CTF比賽,遇到一個tp6的題,給了源碼,目的是讓通過pop鏈審計出反序列化漏洞。

這里總結一下tp6的反序列化漏洞的利用。

0x01環境搭建

現在tp新版本的官網不開源了,但是可以用composer構建環境,系統需先安裝composer。然后執行命令:

composer create-project topthink/think=6.0.x-dev v6.0
cd v6.0
php think run

或者可以去github上下載,但是需要改動很多,也可以去csdn上下載人家配好了的源碼。

tp6需要php7.1及以上的環境才能搭建成功。

搭建完成后訪問ip加/public即可

0x02利用條件

需要在根目錄下的 /app/controller的index.php里面存在unserialize()函數且為可控點,例如存在

unserialize($_GET['payload'])

0x03POP鏈分析

總的目的是跟蹤尋找可以觸發__toString()魔術方法的點

先從起點__destruct()或__wakeup方法開始,因為它們就是unserialize的觸發點。

剛裝了系統,還沒下載phpstorm,先利用seay審計,然后全局搜索__destruct()方法,這里用了/vendor/topthink/think-orm/src下的Model.php,因為它里面含有save()方法可以被觸發。

 

 

 跟進去,當$this->lazySave == true時,$this->save()方法將被觸發

$this->lazySave == true時,會觸發$this->save()方法

 

 

 跟進save()方法

 

 

發現對$this->exists屬性進行判斷,如果為true則調用updateData()方法,false則調用insertData()方法。

 試着跟進一下updateData()方法,發現updateData方法可以繼續利用

 

 

 那么我們首先要構造參數使得updateData()被觸發,需要將下面這個if過掉

 

 

 跟進isEmpty()方法看一下,

 

 

 保證isEmpty返回false,以及$this->trigger(‘BeforeWrite’)返回true即可繞過判斷然后觸發我們的updateData()方法。

構造$this->data為非空數組
構造$this->withEvent為false
構造$this->exists為true

繼續跟進updateData()方法

需要繞過含有return的判斷,第一個判斷前面的save方法符合條件,第二個if需要$data非空

 

 

 $data的屬性值由方法getChangedData()控制,跟進它

 

 

 如圖兩個變量初始值為[] 符合下面if的判斷,結果返回1,即默認返回$data=1,確保這一步繞過之后我們就到了checkAllowFields()方法了

跟進checkAllowFields方法

發現了個字符串拼接

 

 

 由於$this->field==null,$this->schema==null,因此默認滿足

那么我們看上面的db()方法

跟進db()方法

 

 

 發現滿足判斷條件的話,就存在$this->table . $this->suffix參數的拼接,這不就是那兩個熟悉的拼接嗎,可以觸發__toString

滿足connect函數的調用即可。

后面就是延續tp5反序列化的觸發toString魔術方法了,就是原來vendor/topthink/think-orm/src/model/concern/Conversion.php的__toString開始的利用鏈

目前的邏輯鏈圖:

 

 

 

 

 

 

 

 

 

 也就是:

__destruct()——>save()——>updateData()——>checkAllowFields()——>db()——>$this->table . $this->suffix(字符串拼接)——>toString()

這一部分構造的參數為:

$this->exists = true;
$this->$lazySave = true;
$this->$withEvent = false;

尋找__toString觸發點

全局搜索__toString(),找到vendor/topthink/think-orm/src/model/concern/Conversion.php

 

 跟進toJson()方法

 

 跟進toArray()方法

 

 第三個foreach里面存在getAttr方法,他是個關鍵方法,我們需要觸發他

觸發條件:

$this->visible[$key]存在,即$this->visible存在鍵名為$key的鍵,而$key則來源於$data的鍵名,$data則來源於$this->data,也就是說$this->data和$this->visible要有相同的鍵名$key

構造參數,把$key做為參數傳入getAttr方法

跟進getAttrr()方法

 

 然后$key值就傳入到了getData()方法,跟進getData方法

 

 第一個if判斷傳入的值,$key值不為空,因此繞過,然后$key值傳入到了getRealFieldName()方法,跟進getRealFieldName方法

 當$this->stricttrue時直接返回$name,即$key

回到getData方法,此時$fieldName = $key,進入判斷語句:

 

 返回$this->data[$fielName]也就是$this->data[$key],記為$value,再回到getAttr

 

 也就是返回  $this->getValue($key, $value, null);

跟進getValue()方法

 

只要滿足$this->withAttr[$key]存在且$this->withAttr[$key]不為數組就可以觸發命令執行。

 最終會把$this->withAttr[$key]作為函數名動態執行函數,而$value作為參數,就可以利用執行系統函數達到命令執行。

到這里呈現了一條完整的POP鏈。

后半部分__toString的邏輯鏈圖

 

 

 

 

 

 

 

 

 

 這一部分參數賦值:

$this->table = new think\model\Pivot();
$this->data = ["key"=>$command];
$this->visible = ["key"=>1];
$this->withAttr = ["key"=>$function];
$this->$strict = true;

因為這里的Model類為抽象abstract類,所以我們需要找一個繼承於Model的類,比如Pivot

0x04 利用exp

tp默認主頁目錄為/public/,這下面的index.php會定位到/app/controller/index.php文件,因此url中/public/將調用app/controller/index.php,那么我們要知道里面的GET變量,用來傳參數。

那么我們定位到app/controller/index.php,找到里面的unserialize()函數里面的GET變量。我這里的環境里面沒有unserialize()函數,我們手動加上並加上一個GET變量

 

 網上我看到其他tp6框架的這個文件里面用的是$u這個變量來裝unserialize()的值,並且用的GET變量是c,而且還對GET變量進行了base64_decode()編碼,我這里為了對應安詢杯比賽環境,沒有加上base64編碼。

首先利用phpggc工具生成exp,phpggc是一個反序列化payload生成工具,大佬們已經將tp6的exp上傳到了phpggc,需要安裝在linux上,然后執行以下命令生成exp的payload

./phpggc -u ThinkPHP/RCE2 'phpinfo();'

將生成的序列化字符串的payload傳參給GET變量c,發送請求,然后將執行phpinfo()。

 

 如果是真實的tp6框架,試試將payload進行base64編碼后再發送請求。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM