作者:天融信阿爾法實驗室
公眾號:https://mp.weixin.qq.com/s/RL8_kDoHcZoED1G_BBxlWw
前言
在學習PHP的過程中發現有些PHP特性的東西不好理解,如PHP中的00截斷,MD5缺陷,反序列化繞過__wakeup等等。本人不想拘泥于表面現象的理解,想探究PHP內核到底是怎樣做到的。
下面是將用CTF中常用的一個反序列化漏洞CVE-2016-7124(繞過魔法函數__wakeup)為例,將此次調試PHP內核的過程分享出來。包括從內核源碼調試環境的搭建,序列化與反序列化內核源碼分析到最后的漏洞分析整個部分。
一、 一個例子引發的思考
我們可以首先看本人寫的小例子。

根據上圖我們先介紹下PHP中的魔法函數:
我們先看下官方文檔對幾個常用魔法函數的介紹:



這里稍作總結,當一個類被初始化為實例時會調用__construct,當被銷毀時會調用__destruct。
當一個類調用serialize進行序列化時會自動調用__sleep函數,當字符串要利用unserialize反序列化成一個類時會調用__wakeup函數。上述魔法函數如果存在都將會自動進行調用。不用自己手動進行顯示調用。
現在我們來看最開始的代碼部分,在__destruct函數中有寫入文件的敏感操作。我們這里利用反序列化構造危險的字符串有可能會造成代碼執行漏洞。
當我們構造好相應的字符串準備進行利用時,我們卻發現它的__wakeup函數中有過濾操作,這就給我們的構造造成了阻礙。因為我們知道反序列化無論如何都是要先調用__wakeup函數的。
這里我們不禁想到了利用這個PHP反序列化漏洞CVE-2016-7124(繞過魔法函數__wakeup),輕松繞過反序列化會自動調用的魔法函數___wakeup,把敏感操作寫入進了文件。
當然,上面的代碼只是我個人舉得一個簡單例子,真實情況中不乏有上述情況的出現。但是這種繞過方法卻使我非常感興趣。PHP的內部到底是如何操作和處理才會影響到上層代碼邏輯出現如此神奇的情況(BUG)。接下來本人將對PHP內核進行動態調試分析。探究此問題。
此漏洞(CVE-2016-7124)受影響版本PHP5系列為5.6.25之前,7.x系列為7.0.10之前。所以我們后面會編譯兩個版本:一為不受此漏洞影響的版本7.3.0,另一個版本為漏洞存在的版本5.6.10。通過兩個版本的對比來更詳細的了解其差異。
二、PHP源碼調試環境搭建
我們都知道PHP是由C語言開發,因本人所使用環境為WIN 10,所以主要介紹Windows下的環境搭建。我們需要如下材料:
-
PHP源碼。
-
PHP SDK工具包,用于構建PHP
-
調試所需要IDE。
源碼可在GITHUB上下載,鏈接:https://github.com/php/php-src,可以選擇所需要的版本進行下載。
PHP SDK的工具包下載地址:https://github.com/Microsoft/php-sdk-binary-tools 這個地址所下載的工具包只支持VC14,VC15。當然你也可以從https://windows.php.net/downloads/ 找到支持PHP低版本的VC11,VC12等,在使用PHP SDK之前必須保證你有安裝對應版本Windows SDK組件的VS。
后文中會使用PHP7.3.0和5.6.10,下面會介紹這兩個版本的源碼編譯,其他版本手法類似。
2.1 編譯Windows PHP 7.3.0
本機環境WIN10 X64,PHP SDK是在上述github鏈接上下載。進入SDK目錄,發現4個批處理文件,這里雙擊phpsdk-vc15-x64。

接著在此shell中輸入 phpsdk_buildtree php7,會發現同目錄下出現了php7文件夾,并且shell目錄也發生了變化。

接著我們把解壓后的源碼放在\php7\vc15\x64下,shell進入此文件夾內,利用phpsdk_deps –update –branch master 命令更新下載相關依賴組件。等待完成后,進入源碼目錄下雙擊buildconf.bat批處理文件,它會釋放configure.bat和configure.js兩個文件,在shell中運行configure –disable-all –enable-cli –enable-debug –enable-phar 配置相應的編譯選項,如還有別的需求,可執行 configure –help 查看

根據提示,直接使用nmake進行編譯。

編譯完成,可執行文件目錄在php7\vc15\x64\php-src\x64\Debug_TS文件夾下。我們可輸入php -v查看相關信息。

2.2 編譯Windows PHP 5.6.10
方法跟7.3.0 相同,只需注意的是PHP5.6使用Windows SDK組件版本為VC11,需要下載VS2012,并且不能使用github上下載的PHP SDK進行編譯,需要在https://windows.php.net/downloads/ 上選擇VC11 的PHP SDK和相關依賴組件進行編譯,其余和上述完全相同,這里不再重復。

2.3 調試配置
因為我們上述已經編譯好了PHP解釋器,我們這里直接使用VSCODE來進行調試。
下載完成后安裝C/C++調試擴展。

接著打開源碼目錄,點擊調試—>打開配置,會打開launch.json文件。

根據上圖,配置好這三個參數后,可在當前目錄下1.php中寫PHP代碼,在PHP源碼中下斷點直接進行調試。
調試環境搭建完成。
三、PHP反序列化源碼解析
一般提及PHP反序列化,往往就是serialize和unserialize兩個成對出現的函數,當然必不可少的還有__sleep()和__wakeup()這兩個魔術方法。眾所周知,序列化簡單點來說就是對象存文件,反序列化剛好相反,從文件中把對象讀取出來并實例化。
下面,我們根據上面搭好的調試環境,通過動態調試的手法來直觀的反應PHP(7.3.0版本)中序列化與反序列化到底干了哪些事情。
3.1 serialize源碼分析
我們先寫個不含有__sleep魔法函數的簡單Demo:

接著我們在源碼中全局搜索serialize函數,定位此函數是在var.c文件中。我們直接在函數頭下斷點,并啟動調試。

我們可見在做了一些準備工作后,開始進入序列化處理函數,我們跟進php_var_serialize函數。

我們這里繼續跟進php_var_serialize_intern函數,下面就是主要處理函數了,因為函數代碼比較多,我們這里只截出關鍵部分,此函數還在var.c文件中。

整個函數的結構是switch case,通過宏Z_TYPE_P解析struc變體的類型(此宏展開為struc->u1.v.type),來判斷要序列化的類型,從而進入相應的CASE部分進行操作。下圖為類型定義。

根據上圖紅框中的數字8,我們可知此時需要要序列化為一個對象IS_OBJECT,進入相應的CASE分支

我們在上圖中看到了魔法函數__sleep的調用時機,因為我們寫的Demo中并沒有此函數,所以流程并不會進入此分支。不同的分支代表不同的處理流程,我們稍后再看帶有魔法函數__sleep的流程。

因上面case IS_OBJECT分支中沒有流程命中,case中又沒有break語句,繼續執行進入IS_ARRAY分支,在這里從struc結構中提取出類名,計算其長度并賦值到buf結構中,并提取出類中要序列化的結構存入哈希數組中。

接下來就是利用php_var_serialize_intern函數遞歸解析整個哈希數組的過程,從中分別提取出變量名和值進行格式解析并將解析完成的字符串拼接到buf結構中。最后當整個過程結束后,整個字符串講完全存進柔性數組結構buf中。

從上圖紅框中可看出跟最終結果是相吻合的。我們接下來稍微修改下Demo,添加魔法函數__sleep,根據官方文檔中描述,__sleep函數必須返回一個數組。我們并在該函數中調用了一個類的成員函數。觀察其具體行為。

前面流程完全相同,此處不再重復,我們從分支點開始看。

我們直接跟進php_var_serialize_call_sleep函數。

我們這里繼續跟進call_user_function,根據宏定義,它實際上是調用了_call_user_function_ex函數,在這里做了一些拷貝動作,故不做截圖,流程接下來進入zend_call_function函數的調用。

函數zend_call_function中,實際情況下,在__sleep中需要做一些我們自己的事情,這里PHP將要做的操作壓入PHP自己的zend_vm引擎堆棧中,稍后會進行一條條解析(就是解析相應的OPCODE)。

這里流程會命中此分支,我們跟進zend_execute_ex函數。

我們這里可以看到在ZEND_VM中,整體體處理流程為while(1)循環,不斷解析ZEND_VM棧中的操作。上圖紅框中ZEND_VM引擎會利用ZEND_FASTCALL方式派發到到相應的處理函數。


因為我們在__sleep中調用了成員函數show,這里首先定位出了show,接著會將接下來的操作繼續壓入ZEND_VM堆棧中進行下一輪新的解析(這里是處理show中的操作),直到解析完整個操作為止。我們這里不再繼續跟進。

還記得上面的傳出參數retval么,也就是__sleep的返回值,上圖為返回數組的第一個元素x,當然你也可以從變量中直接查看。
繞了這么大一圈,殊途同歸,在處理完_sleep函數中的一系列操作之后,接下來用php_var_serialize_class函數來序列化類名,遞歸序列化其_sleep函數返回值中的結構。最終都把結果存在了buf結構中。至此序列化的整個流程完畢。
3.1.1 SERIALIZE流程小結。
我們總結下序列化的流程 :
當沒有魔法函數時,序列化類名–>利用遞歸序列化剩下的結構
當存在魔法函數時,調用魔法函數__sleep–>利用ZEND_VM引擎解析PHP操作—>返回需要序列化結構的數組–>序列化類名–>利用遞歸序列化__sleep的返回值結構。
3.2 unserialize源碼分析
看完serialize的流程,接下來,我們還是從最簡單的一個Demo來看unserialize流程。此例子不含魔法函數。

方法跟上面相同,unserialize源碼也在var.c文件中。


上圖中涉及到了PHP7中的新特性,帶過濾的反序列化,根據allowed_classes的設置情況來過濾相應的PHP對象,防止非法數據注入。被過濾的對象會被轉化成__PHP_Incomplete_Class對象不能被直接使用,但是這里對反序列化流程沒有影響,這里不做詳細探討。我們跟進php_var_unserialize函數。

我們這里繼續跟入php_var_unserialize_internal函數。

此函數內部主要操作流程為對字符串進行解析,然后跳轉到相應的處理流程。上圖中解析出第一個字母0,代表此次反序列化為一個對象。

這里首先會解析出對象名字,并進行查表操作確定此對象確實存在,我們繼續向下看。

上述操作做完之后,我們這里根據對象名稱new出了自己新的對象并進行了初始化,但是我們的反序列化操作還是沒有完成,我們跟進object_common2函數。
在這里我們看到了對魔法函數的判斷與檢測,但是調用部分并不在此。我們繼續跟進process_nested_data函數。


看來這個函數利用WHILE循環來嵌套解析剩余的部分了,·其中包含兩個php_var_unserialize_internal函數,第一個會解析名稱,第二個是解析名稱所對應的值。process_nested_data函數運行完畢后,字符串解析完畢,反序列化操作主要內容已經完成,流程即將進入尾聲了。

逐層返回至最初的函數PHP_FUNCTION中,我們看到就是一些掃尾工作了,釋放申請的空間,反序列化完畢。這里并沒有調用到我們的魔法函數__wakeup。為了找出__wakeup的調用時機,我們這里修改下Demo。

這里開始新的一輪調試。發現在序列化完成后,在PHP_VAR_UNSERIALIZE_DESTROY釋放空間處出現了我們所希望看到的調用。

還記得反序列化流程中當發現有__wakeup時對其進行的VAR_WAKEUP_FLAG標志么,在這里當遍歷bar_dtor_hash數組遇到這個標志時,正式開啟對__wakeup調用,后期的調用手法和前面所介紹的__sleep調用手法完全相同,這里不再做重復說明。至此,反序列化所有流程完畢。
3.2.1 UNSERIALIZE流程小結。
我們可以從上面可以看到,反序列化流程相對于序列化流程來說并沒有因為是否出現魔法函數來對流程造成分歧。Unserialize流程如下:
獲取反序列化字符串–>根據類型進行反序列化—>查表找到對應的反序列化類–>根據字符串判斷元素個數–>new出新實例–>迭代解析化剩下的字符串–>判斷是否具有魔法函數__wakeup并標記—>釋放空間并判斷是否具有具有標記—>開啟調用。
四、 PHP反序列化漏洞
有了上面源碼基礎的鋪墊,我們現在再來探究漏洞CVE-2016-7124(繞過__wakeup)魔法函數。因此漏洞對版本有一定要求,我們使用上面編譯好的另一個PHP版本(5.6.10)來復現和調試此漏洞。
首先我們進行一下漏洞復現:

我們這里可以看到,TEST類中只包含一個元素$a,我們這里在反序列化時當修改元素字符串中代表元素個數的數值時,會觸發此漏洞,該類避過了魔法函數__wakeup的調用。
當然在觸發漏洞的過程中也發現了一個有趣的現象,觸發手段并不只有這一種.

上圖中4個payload所對應的反序列化操作都會觸發此漏洞。雖然說下方這四個都會觸發漏洞,但是其中還有一些微小的差別。這里我們稍微修改下代碼:

我們根據上圖可以看到,在反序列化的字符串中,只要在解析類中的元素出現錯誤時,都會觸發此漏洞。但是更改類元素內部操作(如上圖的修改字符串長度,類變量類型等)會導致類成員變量賦值失敗。只有修改類成員的個數(比原有成員個數大)時,才能保證類成員賦值時成功的。
我們下面來通過調試來看問題所在:
根據第三部分我們對反序列化源碼的分析,猜測可能是在最后解析變量那里出了問題。我們這里直接上調試器動態調試下:

我們可以看到,與7.3.0版本的源碼對比,此版本沒有過濾參數,且經過這么多版本的迭代,低版本的處理過程現在看來也相對簡略。但是整體諧邏輯并沒有改變,我們這里直接跟進php_var_unserialize函數,此后相同邏輯不再進行重復說明,我們直接跟到差異處(object_common2函數)也就是處理類中成員變量的代碼

在函數object_common2中,存在兩個主要操作,process_nested_data迭代解析類中的數據和魔法函數__wakeup的調用,且當process_nested_data函數解析失敗后,直接返回0值,后面的__wakeup函數將沒有調用的機會。
這里就解釋了為何觸發漏洞不止一種payload。

當只修改類成員的個數時,while循環可以完成的進行一次,這使得我們類中成員變量能被完整的賦值。當修改成員變量內部時,pap_var_unserialize函數調用失敗,緊接著會調用zval_dtor 和FREE_ZVAL函數釋放當前key(變量)空間,導致類中的變量賦值失敗。
反觀在PHP7.3.0版本中此處并沒有出現調用過程,只是做了簡單的標記,整個魔法函數的調用過程的時機移至釋放數據處。這樣就避免了這個繞過的問題。
此漏洞應該屬于邏輯上的缺陷導致的。
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/866/
暫無評論