Thinkphp5.0.24反序列化
0x01前言
- 最近在學習代碼審計,因為java還不太擅長就先學習php的代碼審計。thinkphp框架是php比較經典的一個框架了,所以就先選擇了thinkphp進行審計。爭取一到兩周能發一篇審計的博客,督促自己不要偷懶。(這篇就拖了3個月,3個月前寫了一半,但是后面一半到今天才寫完,最終還是偷懶了,哈哈)
0x02下載與設置功能點
-
thinkphp 5.0.24下載地址:http://www.thinkphp.cn/donate/download/id/1279.html
-
因為從官網上下載的thinkphp5.0.24自身是沒有使用反序列化的功能的,所以我們需要自己添加上去,以便利用漏洞。我們將application/index/controller/Index.php的index()方法修改為
public function index()
{
$yhck = unserialize($_GET['yhck']);
var_dump($yhck);
}
- 這樣我們就可以通過app\index\Index路由的index()方法使用反序列化的功能了
0x03尋找pop鏈
- thinkphp5.0.24反序列化漏洞的流程大致是通過__toString()方法調用__call()方法最終實現寫webshell,因此我們需要先找到可以利用的__toString()方法。在這里我們首先尋找的是think\process\pipes\Windows(對應thinkphp/think/process/pipes/windows.php文件)里的__destruct()方法。

- 跟入removeFiles()方法

- 我們可以看到removeFiles()方法的if分支中有file_exists()函數,執行file_exists()時$filename會被當做字符串,因此我們可以通過file_exists()函數觸發__toString()函數,在這里選擇\think\Model中的__toString()進行調用,進入\think\Model中的__toString()方法

- 跟進toJson()方法

- 跟進toArray()方法

- 我們可以看到第912行,$value調用了一個getAttr()方法,如果$value可控的話,我們就可以通過控制$value調用__call()方法。我們往上跟進,看看$value是否可控。

- 向上跟進,發現902行$value是由getRelationData()的返回值進行賦值的,我們跟入getRelationData()函數

- 我們發現getRelationData()函數中如果我們傳入的參數為Relation類且滿足if分支的條件,那么$value就會由$this->parent的值決定,我們現在已經進到了if分支,那么就先看滿不滿足if分支的條件。$this->parent的值可控,if分支的第一個條件可以滿足,接下來我們看第二個條件,跟進isSelfRelation()函數

- isSelfRelation()函數返回$this->selfRelation,可控,因此我們可以滿足if的第二個條件,跟進getModel()函數

- getModel()函數返回$this->query->getModel(),$query可控,因此此時我們需要查找哪個類的getModel()可控,在這里找到了\think\db\Query類,跟進\think\db\Query類

- \think\db\Query類的getModel()方法返回$this->model,可控,並且$this->parent可控,因此第三個條件滿足,if分支滿足

- 接下來我們需要判斷是否能傳入一個Relation類的參數,我們回到\think\Model,發現調用getRelationData()函數時傳入的是$modelRelation變量,跟進$relation(),發現$relation()函數是根據$relation的值進行調用的並且要滿足method_exists()函數,跟進parseName()

- parseName()函數只對傳進來的$name做了一些大小寫替換,沒有實質上的過濾操作,因此$name可控,$relation可控

- $relation可控的前提下我們要滿足method_exists()函數就需要我們將$relation的值設定為\think\Model擁有的方法,在這里我們選擇的是getError()方法。這個方法在這里用處很大,第一是這個方法是\think\Model擁有的方法,第二是$this->error可控,這樣我們不僅滿足了method_exists()函數,還讓$modelRelation可控,這樣$value也就可控了

- 雖然$value可控,但是我們還要滿足兩個if的條件才能調用__call()方法,我們跟進這兩個if條件

- 第一個if條件需要滿足$modelRelation存在getBindAttr()函數,並且$bindAttr變量由getBindAttr()函數返回值決定,我們全局搜索一下getBindAttr()函數,發現Relation類中不存在該方法,但OneToOne中存在且OneToOne是Relation類的子類、$this->bindAttr可控

- 跟進OneToOne.php,發現OneToOne是個抽象類,無法生成實例,我們全局搜索繼承它的類,發現HasOne類繼承了OneToOne類,因此我們可以令$modelRelation的值為HasOne,此時便可滿足第一個if條件,由於$this->bindAttr可控,因此我們也能滿足第二個if條件

- 我們往下跟進,發現$attr變量由$bindAttr決定,且$attr變量用於912行的$value->getAttr()中,因此$value->getAttr($attr)可控,所以我們可以根據$value->getAttr($attr)調用__call()方法,此時我們需要尋找能寫webshell的__call()方法,在這里選擇的是think\console\Output類

- 在這里$method和$this->styles是可控的,array_unshift()對調用block()方法沒有影響,因此我們跟進block()方法

- 跟進writeln()函數

- 跟進write()函數

- 在這里$this->handle是可控的,我們尋找能寫webshell的write()方法,此次選擇了 think\session\driver\Memcache類

- 在這里$this->handler又是可控的,我們繼續尋找能寫webshell的set()方法,此次選擇了think\cache\driver\File 類,我們可以看到think\cache\driver\File 類的set()方法通過file_put_contents()將$data寫進了文件,我們跟入$data和$filename,看$data與$filename是否可控

- 往上跟進發現$filename的值是由getCacheKey()方法決定的,我們跟進getCacheKey()函數

- 從getCacheKey()函數80行處的語句我們可以知道$filename的后綴是寫死的,為php,並且文件名的一部分可控,這時如果$data可控的話就可以getshell了。我們跟進$data,發現$data最終是由think\console\Output類的write()方法決定的,$data的值為true,已經被寫死了

- 這樣就說明了file_put_contents()函數能寫入php文件,但內容不可控,無法寫shell。繼續往下看,發現有個setTagItem()函數,跟進該函數看看

- 我們可以看到在setTagItem()函數中又一次調用了set()函數,並且這次的$key是可控的,$value由之前的$filename決定,這也意味着我們可以通過setTagItem()再一次的寫入php文件進行getshell

- 到這里整個pop鏈已經梳理完了,接下來我們看看如何利用這條pop鏈進行getshell
0x04利用pop鏈
- 利用的時候我們首先需要繞過exit()的限制,因為利用file_put_contents()寫入文件時內容有exit()函數並且在比較靠前的位置,如果執行到了exit()函數就會自動退出,不會執行我們寫入的shell,所以我們需要繞過這個函數,這里用到php的偽協議進行繞過,具體原理見下圖

- 訪問之后寫入的文件內容

-
也就是說我們只需要在文件名中使用偽協議即可對exit()函數進行繞過
-
到這里我們其實可以寫出整個payload了,但是目前只能寫出Linux下的payload。為什么說是linux下的payload呢,因為windows文件名不能包含“<”、“?”、“>”等字符,但我們在使用偽協議時使用了這幾個字符,所以我們想在windows下利用這條pop鏈的話還需要想一些其他的辦法,此時我們就需要尋找其他的地方去賦值文件名,在這里我們找的是think\cache\driver\Memcached的set()方法,即當程序走到Memcache.php中的write方法時我們不直接賦予$this->handle為File對象,而是賦值為cache中的Memcached對象


- 我們可以看到think\cache\driver\Memcached set()方法的第114行又調用了一次$this->handler->set()方法,並且文件名是通過$key決定的,然而$key又是通過getCacheKey()決定的,我們跟進getCacheKey()方法

- 根據getCacheKey()方法我們知道$key是可控的,但是$value在114行的set()方法中又是不可控的,為0,此次雖然寫進了一個文件,但並不能獲取shell,所以我們需要通過setTagItem()方法寫shell

-
可以看到setTagItem()方法將$name作為$value傳入set()函數,也就是將$key作為函數內容傳入,這樣我們就可以控制文件名和函數內容getshell了
-
還需要注意一個坑點就是在通過這種方法在windows寫shell的話需要php關閉短標簽,否則執行的時候會報錯,這時候我們可以通過iconv.UCS-4LE.UCS-4BE偽協議進行繞過
0x05利用效果
- 最后通過寫出的poc在windows下利用的效果如下


