- 從0到1:CTFer成長之路
- Nu1L戰隊
- 4920字
- 2021-01-07 17:32:03
3.1 反序列化漏洞
在各類語言中,將對象的狀態信息轉換為可存儲或可傳輸的過程就是序列化,序列化的逆過程便是反序列化,主要是為了方便對象的傳輸,通過文件、網絡等方式將序列化后的字符串進行傳輸,最終通過反序列化可以獲取之前的對象。
很多語言都存在序列化函數,如Python、Java、PHP、.NET等。在CTF中,經常可以看到PHP反序列化的身影,原因在于PHP提供了豐富的魔術方法,加上自動加載類的使用,為構寫EXP提供了便利。作為目前最流行的Web知識點,本節將對PHP序列化漏洞逐步介紹,通過一些案例,讓讀者對PHP反序列漏洞有更深的了解。
3.1.1 PHP反序列化
本節介紹PHP反序列化的基礎,以及常見的利用技巧。當然,這些不僅是CTF比賽的常備,更是代碼審計中必須掌握的基礎。PHP對象需要表達的內容較多,如類屬性值的類型、值等,所以會存在一個基本格式。下面則是PHP序列化后的基本類型表達:
? 布爾值(bool):b:value=>b:0。
? 整數型(int):i:value=>i:1。
? 字符串型(str):s:length:"value";=>s:4:"aaaa"。
? 數組型(array):a:<length>:{key,value pairs};=>a:1:{i:1;s:1:"a"}。
? 對象型(object):O:<class_name_length>:。
? NULL型:N。
最終序列化數據的數據格式如下:

接下來通過一個簡單的例子來講解反序列化。序列化前的對象如下:


通過serialize()函數進行序列化:

其中,O表示這是一個對象,6表示對象名的長度,person則是序列化的對象名稱,3表示對象中存在3個屬性。第1個屬性s表示是字符串,4表示屬性名的長度,后面說明屬性名稱為name,它的值為N(空);第2個屬性是age,它的值是為整數型19;第3個屬性是sex,它的值也是為空。
這時就存在一個問題,如何利用反序列化進行攻擊呢?PHP中存在魔術方法,即PHP自動調用,但是存在調用條件,比如,__destruct是對象被銷毀的時候進行調用,通常PHP在程序塊執行結束時進行垃圾回收,這將進行對象銷毀,然后自動觸發__destruct魔術方法,如果魔術方法還存在一些惡意代碼,即可完成攻擊。
常見魔術方法的觸發方式如下。
? 當對象被創建時:__construct。
? 當對象被銷毀時:__destruct。
? 當對象被當作一個字符串使用時:__toString。
? 序列化對象前調用(其返回需要是一個數組):__sleep。
? 反序列化恢復對象前調用:__wakeup。
? 當調用對象中不存在的方法時自動調用:__call。
? 從不可訪問的屬性讀取數據:__get。
下面對一些常見的反序列化利用挖掘進行介紹。
3.1.1.1 常見反序列化
PHP代碼如下:

這段代碼存在一個test類中,其中__destruct魔術函數中還存在eval($_GET['cmd'])的代碼,然后通過參數u來接收序列化后的字符串。所以,可以進行以下利用,__destruct在對象銷毀時會自動調用此方法,然后通過cmd參數傳入PHP代碼,即可達到任意代碼執行。
在利用程序中,首先定義test類,然后對它進行實例化,再進行序列化輸出字符串,將利用代碼保存為PHP文件,瀏覽器訪問后即可顯示出序列化后的字符串,即O:4:"test":0:{}。代碼如下:

通過傳值進行任意代碼執行,u參數傳入O:4:"test":0:{},cmd參數傳入system("whoami"),即最后代碼會執行system()函數來調用whoami命令。
漏洞利用結果見圖3-1-1。

圖3-1-1
有時我們會遇到魔術方法中沒有利用代碼,即不存在eval($_GET['cmd']),卻有調用其他類方法的代碼,這時可以尋找其他有相同名稱方法的類。例如,圖3-1-2是存在漏洞的代碼。
以上代碼便存在normal正常類和evil惡意類??梢园l現,lemon類正常調用便是創建了一個normal實例,在destruct中還調用了normal實例的action方法,如果將$this->ClassObj替換為evil類,當調用action方法時會調用evil的action方法,從而進入eval($this->data)中,導致任意代碼執行。

圖3-1-2
在Exploit構造中,我們可以在__construct中將Classobj換為evil類,然后將evil類的私有屬性data賦值為phpinfo()。Exploit構造見圖3-1-3。
保存為PHP文件后訪問,最終會得到一串字符:

注意,因為ClassObj是protected屬性,所以存在“%00*%00”來表示它,而“%00”是不可見字符,在構造Exploit的時候盡量使用urlencode后的字符串來避免“%00”缺失。

圖3-1-3
最終使用Exploit可以執行phpinfo代碼,結果見圖3-1-3。

圖3-1-3
3.1.1.2 原生類利用
實際的挖洞過程中經常遇到沒有合適的利用鏈,這需要利用PHP本身自帶的原生類。
1.__call方法
__call魔術方法是在調用不存在的類方法時候將會觸發。該方法有兩個參數,第一個參數自動接收不存在的方法名,第二個參數接收不存在方法中的參數。例如,PHP代碼如下:

通過unserialize進行反序列化類為對象,再調用類的notexist方法,將觸發__call魔術方法。
PHP存在內置類SoapClient::__Call,存在可以進行__call魔術方法時,意味著可以進行一個SSRF攻擊,具體利用代碼見Exploit。
Exploit生成(適用于PHP 5/7):

上面是new SoapClient進行配置,將uri設置為自己的VPS服務器地址,然后將location設置為http://vps/aaa。以上生成的字符串放入unserialize()函數,進行反序列化,再進行不存在方法的調用,則會進行SSRF攻擊,見圖3-1-4。

圖3-1-4
圖3-1-4便是進行一次Soap接口的請求,但是只能做一次HTTP請求。當然,可以使用CRLF(換行注入)進行更加深入的利用。通過“'uri'=>'http://vps/i am here/'”注入換行字符。CRLF利用代碼如下:

注入結果見圖3-1-5,CRLF字符已經將“i am evil string”字符串放到新的一行。

圖3-1-5
這里進而轉換為如下兩種攻擊方式。
(1)構造post數據包來攻擊內網HTTP服務
這里存在的問題是Soap默認頭中存在Content-Type:text/xml,但可以通過user_agent注入數據,將Content-Type擠下,最終data=abc后的數據在容器處理下會忽略后面的數據。
構造POST包結果見圖3-1-6。
(2)構造任意的HTTP頭來攻擊內網其他服務(Redis)
例如,注入Redis命令:


圖3-1-6
若Redis未授權,則會執行此命令。當然,也可以通過寫crontab文件進行反彈Shell。攻擊redis結果見圖3-1-7。

圖3-1-7
因為Redis對命令的接收較為寬松,即一行行對HTTP請求頭中進行解析命令,遇到圖3-1-7中的“config set dir/root/”,便會作為Redis命令進行執行。
2.__toString
__toString是當對象作為字符串處理時,便會自動觸發。PHP代碼如下:

Exploit生成(適用于PHP 5/7):

主要利用了Exception類對錯誤消息沒有做過濾,導致最終反序列化后輸出內容在網頁中造成XSS,構寫Exploit生成時,將XSS代碼作為Exception類的參數即可。
通過echo將Exception反序列化后,便會進行一個報錯,然后將XSS代碼輸出在網頁。最終觸發結果見圖3-1-8。

圖3-1-8
3.__construct
通常情況下,在反序列化中是無法觸發__construct魔術方法,但是經過開發者的魔改后便可能存在任意類實例化的情況。例如,在代碼中加入call_user_func_array調用,再禁止調用其他類中方法,這時便可以對任意類進行實例化,從而調用了construct方法(案例可參考https://5haked.blogspot.jp/2016/10/how-i-hacked-pornhub-for-fun-and-profit.html?m=1),在原生類中可以找到SimpleXMLElement的利用??梢詮墓倬W中找到SimpleXMLElement類的描述:

通常進行以下調用:

調用時注意,Libxml 2.9后默認不允許解析外部實體,但是可以通過函數參數LIBXML_NOENT進行開啟解析。xxe_evil內容見圖3-1-9。

圖3-1-9
攻擊分為兩個XML文件,xxe_evil是加載遠程的xxe_read_passwd文件,xxe_read_passwd則通過PHP偽協議加載/etc/passwd文件,再對文件內容進行Base64編碼,最后通過拼接方式,放到HTTP請求中帶出來。
最終通過反序列化的利用也能夠獲取/etc/passwd的信息,結果見圖3-1-10。

圖3-1-10
3.1.1.3 Phar反序列化
2017年,hitcon首次出現Phar反序列化題目。2018年,blackhat提出了Phar反序列化后被深入挖掘,2019年便可以看到花式Phar題目。Phar之所以能反序列化,是因為PHP使用phar_parse_metadata在解析meta數據時,會調用php_var_unserialize進行反序列化操作,其中解析代碼見圖3-1-11。

圖3-1-11
可以生成一個Phar包進行觀察,需要注意php.ini中的phar.readonly選項需要設為Off。生成Phar包的代碼見圖3-1-12。

圖3-1-12
通過winhex編輯器對Phar包進行編輯,可以看到,文件中存在反序列化后的字符串內容,見圖3-1-13。
那么,如何觸發Phar反序列化?因為在PHP中Phar是屬于偽協議,偽協議的使用最多的便是一些文件操作函數,如fopen()、copy()、file_exists()、filesize()等。當然,繼續深挖,如尋找內核中的*_php_stream_open_wrapper_ex函數,PHP封裝調用此類函數,會讓更多函數支持封裝協議,如getimagesize、get_meta_tags、imagecreatefromgif等。再通過傳入phar:///var/www/html/1.phar便可觸發反序列化。
例如,通過file_exists("phar://./demo.phar")觸發phar反序列化,結果見圖3-1-14。

圖3-1-13

圖3-1-14
3.1.1.4 小技巧
反序列化中的一些技巧使用頻率較高,但是目前很難出單純的考點,更多的是以一種組合的形式加入構造利用鏈。
1.__wakeup失效:CVE-2016-7124
這個問題主要由于__wakeup失效,從而繞過其中可能存在的限制,繼而觸發可能存在的漏洞,影響版本為PHP 5至5.6.25、PHP 7至7.0.10。
原因:當屬性個數不正確時,process_nested_data函數會返回為0,導致后面的call_user_function_ex函數不會執行,則在PHP中就不會調用__wakeup()。
具體代碼見圖3-1-15。

圖3-1-15
可以使用圖3-1-16的代碼進行本地測試,輸入:

可以看到,圖3-1-17觸發了wakeup中的代碼。
當更改demo后的屬性個數為2時(見圖3-1-18):

可以發現,“i am wakeup”消失了,證明wakeup并沒有觸發。

圖3-1-16

圖3-1-17

圖3-1-18
這個小技巧最經典的真實案例是SugarCRM v6.5.23反序列化漏洞,它在wakeup進行限制,從圖3-1-19中的__wakeup代碼可以看出,它會對所有屬性進行清空,并且拋出報錯,這也限制了執行。但是通過改變屬性個數讓wakeup失效后,便可以利用destruct進行寫入文件。sugarcrm代碼見圖3-1-19。

圖3-1-19
2.bypass反序列化正則
當執行反序列化時,使用正則“/[oc]:\d+:/i”進行攔截,代碼見圖3-1-20,主要攔截了這類反序列化字符:

這是反序列中最常見的一種形式,那么如何進行繞過呢?通過對PHP的unserialize()函數進行分析,發現PHP內核中最后使用php_var_unserialize進行解析,代碼見圖3-1-21。

圖3-1-20

圖3-1-21
上面的代碼主要是解析“'O':”語句段,跟入yy17段中,還會存在“+”的判斷。所以,如果輸入“O:+4:"demo":1:{s:5:"demoa";a:0:{}}”,可以看到當“'O':”后面為“+”時,就會從yy17跳轉到yy19處理,然后繼續對“+”后面的數字進行判斷,意味著這是支持“+”來表達數字,從而對上面的正則進行繞過。
3.反序列化字符逃逸
這里的小技巧是出自漏洞案例Joomla RCE(CVE-2015-8562),這個漏洞產生的原因在于序列化的字符串數據沒有被過濾函數正確的處理最終反序列化。那么,這會導致什么問題呢?我們知道,PHP在序列化數據的過程中,如果序列化的是字符串,就會保留該字符串的長度,然后將長度寫入序列化后的數據,反序列化時就會按照長度進行讀取,并且PHP底層實現上是以“;”作為分隔,以“}”作為結尾。類中不存在的屬性也會進行反序列化,這里就會發生逃逸問題,而導致對象注入。下面以一個demo為例,代碼見圖3-1-22。

圖3-1-22
閱讀代碼,可知這里正確的結果應該是“a:2:{i:0;s:5:"apple";i:1;s:6:"orange";}”。修改數組中的orange為orangex時,結果會變成“a:2:{i:0;s:5:"apple";i:1;s:7:"orangehi";}”,比原來序列化數據的長度多了1個字符,但是實際上多了2個,這個肯定會反序列化失敗。假設利用過濾函數提供的一個字符變兩個的功能來逃逸出可用的字符串,從而注入想要修改的屬性,最終我們能通過反序列化來修改屬性。
這里假設payload為“";i:1;s:8:"scanfsec";}”,長度為22,需要填充22個x,來逃逸我們payload所需的長度,注入序列化數據,最后反序列化,就能修改數組中的屬性orange為scanfsec,見圖3-1-23。

圖3-1-23
4.Session反序列化
PHP默認存在一些Session處理器:php、php_binary、php_serialize(處理情況見圖3-1-24)和wddx(不過它需要擴展支持,較為少見,這里不做講解)。注意,這些處理器都是有經過序列化保存值,調用的時候會反序列化。

圖3-1-24
php處理器(PHP默認處理):

php_serialize處理器:

當存與讀出現不一致時,處理器便會出現問題??梢钥吹剑琾hp_serialize注入的stdclass字符串,在php處理下成為stdclass對象,對比情況見圖3-1-25??梢钥闯觯趐hp_serialize處理下存入“|O:8:"stdClass":0:{}”,然后在php處理下讀取,這時會以“a:2:{s:20:"”作為key,后面的“O:8:"stdClass":0:{}”則作為value進行反序列化。

圖3-1-25
其真實案例為Joomla 1.5-3.4遠程代碼執行。在PHP內核中可以看到,php處理器在序列化的時候是會對“|”(豎線)作為界限判斷,見圖3-1-26。
但是Joomla是自寫了Session模塊,保存方式為“鍵名+豎線+經過serialize()函數反序列處理的值”,由于沒有處理豎線這個界限而導致問題出現。

圖3-1-26
5.PHP引用
題目存在just4fun類,其中有enter、secret屬性。由于$secret是未知的,那么如何突破$o->secret===$o->enter的判斷?
題目代碼見圖3-1-27,PHP中存在引用,通過“&”表示,其中“&$a”引用了“$a”的值,即在內存中是指向變量的地址,在序列化字符串中則用R來表示引用類型。利用代碼見圖3-1-28。

圖3-1-27

圖3-1-28
在初始化時,利用“&”將enter指向secret的地址,最終生成利用字符串:

可以看到,存在“s:6:"secret";R:2”,即通過引用的方式將兩者的屬性值成為同一個值。解題結果見圖3-1-29。

圖3-1-29
6.Exception繞過
有時會遇上throw問題,因為報錯導致后面代碼無法執行,代碼見圖3-1-30。
B類中__destruct會輸出全局的flag變量,反序列化點則在throw前。正常情況下,報錯是使用throw拋出異常導致__destruct不會執行。但是通過改變屬性為“O:1:"B":1:{1}”,解析出錯,由于類名是正確的,就會調用該類名的__destruct,從而在throw前執行了__destruct。

圖3-1-30
3.1.2 經典案例分析
前面講述了PHP反序列化漏洞中的各種技巧,那么在實際做題過程中,往往會出現一些現實情況下的反序列化漏洞,如Laravel反序列化、Thinkphp反序列化以及一些第三方反序列化問題,這里以第三方庫Guzzle為例。Guzzle是一個PHP的HTTP客戶端,在Github上也有不少的關注量,在6.0.0<=6.3.3+中存在任意文件寫入漏洞。至于Guzzle如何搭建環境,這里不做贅述,讀者可自行查閱。
下面對該漏洞進行講解,環境假設為存在任意圖片文件上傳,同時存在一個參數可控的任意文件讀取(如readfile)。那么,如何獲取權限呢?
首先,在guzzle/src/Cookie/FileCookieJar.php中存在如下代碼:

而save()函數定義如下:

可以發現,在第二個if判斷的地方存在任意文件寫入,文件名跟內容都是我們可以控制的;接著看第一個if判斷中的shouldPersist()函數:

我們需要讓$cookie->getExpires()為true,$cookie->getDiscard()為false或null。這兩個函數的定義如下:

接著看$json[]=$cookie->toArray():

而SetCookie中的toArray()如下,即返回所有數據。


所以最后的構造如下:

然后將生成的1.gif傳到題目服務器上,利用Phar協議觸發反序列化即可。