首頁 >後端開發 >PHP7 >PHP核心層解析反序列化漏洞

PHP核心層解析反序列化漏洞

藏色散人
藏色散人轉載
2019-04-02 12:00:003536瀏覽

PHP核心層解析反序列化漏洞

前言

#在學習PHP的過程中發現有些PHP特性的東西不好理解,如PHP中的00截斷,MD5缺陷,反序列化繞過__wakeup等等。本人不想拘泥於表面現象的理解,想探究PHP內核到底是怎麼做到的。

以下是將用CTF常用的一個反序列化漏洞CVE-2016-7124(繞過魔法函數__wakeup)為例,將此調試PHP核心的過程分享出來。包括從核心原始碼調試環境的搭建,序列化與反序列化核心原始碼分析到最後的漏洞分析整個部分。 (推薦:PHP教學

一、一個例子引發的思考

我們可以先看本人寫的小例子。

PHP核心層解析反序列化漏洞

根據上圖我們先介紹下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,可以選擇所需的版本進行下載。

PHPSDK 的工具包下載位址: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

PHP核心層解析反序列化漏洞

接著在此shell中輸入 phpsdk_buildtreephp7,會發現同目錄下出現了php7資料夾,而且shell目錄也發生了變化。

PHP核心層解析反序列化漏洞

PHP核心層解析反序列化漏洞

#

接著我們把解壓縮後的原始碼放在\php7\vc15\x64下,shell進入此資料夾內,利用phpsdk_deps–update–branchmaster指令更新下載相關依賴元件。

等待完成後,進入原始碼目錄下雙擊buildconf.bat批次文件,它會釋放configure.batconfigure.js#兩個文件,在shell中運行configure–disable-all–enable-cli–enable-debug–enable-phar 配置相應的編譯選項,如還有別的需求,可執行configure –help 查看

PHP核心層解析反序列化漏洞

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

PHP核心層解析反序列化漏洞

編譯完成,可執行檔案目錄在php7\vc15\x64\php-src\x64\Debug_TS資料夾下。我們可輸入php -v查看相關資訊。

PHP核心層解析反序列化漏洞

2.2 編譯Windows PHP 5.6.10

方法跟7.3.0 相同,只要注意的是PHP5.6使用WindowsSDK 元件版本為VC11,需要下載VS2012,且無法使用github上下載的PHP SDK進行編譯,需要在https://windows.php.net/downloads/ 上選擇VC11 的PHP SDK和相關依賴元件進行編譯,其餘和上述完全相同,這裡不再重複。

PHP核心層解析反序列化漏洞

2.3 偵錯組態

因為我們上述已經編譯了PHP解釋器,我們這裡直接使用VSCODE來進行偵錯。

下載完成後安裝C/C 偵錯擴充功能。

PHP核心層解析反序列化漏洞

接著開啟原始碼目錄,點選偵錯—>開啟配置,會開啟launch.json檔案。

PHP核心層解析反序列化漏洞

根據上圖,配置好這三個參數後,可在目前目錄下1.php中寫PHP程式碼,在PHP源碼中下斷點直接進行調試。

調試環境建置完成。

三、PHP反序列化原始碼解析

一般提及PHP反序列化,往往就是serialize與unserialize兩個成對出現的函數,當然必不可少的還有__sleep()和__wakeup()這兩個魔術方法。眾所周知,序列化簡單點來說就是物件存文件,反序列化剛好相反,從文件中把物件讀取出來並實例化。

下面,我們根據上面搭好的調試環境,透過動態調試的手法來直觀的反應PHP(7.3.0版本)中序列化與反序列化到底乾了哪些事情。

3.1 serialize原始碼分析

我們先寫一個不含__sleep魔法函數的簡單Demo:

PHP核心層解析反序列化漏洞

#接著我們在原始碼中全域搜尋serialize函數,定位此函數是在var.c檔案中。我們直接在函數頭下斷點,並啟動偵錯。

PHP核心層解析反序列化漏洞

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

PHP核心層解析反序列化漏洞

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

PHP核心層解析反序列化漏洞

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

PHP核心層解析反序列化漏洞

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

PHP核心層解析反序列化漏洞

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

PHP核心層解析反序列化漏洞

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

PHP核心層解析反序列化漏洞

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

PHP核心層解析反序列化漏洞

從上圖紅框中可看出跟最終結果是相吻合的。我們接下來稍微修改下Demo,加入魔法函數__sleep,根據官方文件中描述,__sleep函數必須傳回一個陣列。我們並在該函數中呼叫了一個類別的成員函數。觀察其具體行為。

PHP核心層解析反序列化漏洞

前面流程完全相同,這裡不再重複,我們從分支點開始看。

PHP核心層解析反序列化漏洞

我們直接跟進php_var_serialize_call_sleep函數。

PHP核心層解析反序列化漏洞

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

PHP核心層解析反序列化漏洞

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

PHP核心層解析反序列化漏洞

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

PHP核心層解析反序列化漏洞

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

PHP核心層解析反序列化漏洞

PHP核心層解析反序列化漏洞

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

PHP核心層解析反序列化漏洞

還記得上面的傳出參數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流程。此例子不含魔法函數。

PHP核心層解析反序列化漏洞

方法跟上面相同,unserialize原始碼也在var.c檔案中。

PHP核心層解析反序列化漏洞

PHP核心層解析反序列化漏洞

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

PHP核心層解析反序列化漏洞

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

PHP核心層解析反序列化漏洞

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

PHP核心層解析反序列化漏洞

這裡首先會解析出物件名字,並進行查表操作確定此物件確實存在,我們繼續向下看。

PHP核心層解析反序列化漏洞

上述操作做完之後,我們這裡根據物件名稱new出了自己新的物件並進行了初始化,但是我們的反序列化操作還是沒有完成,我們跟進object_common2函數。

在這裡我們看到了對魔法函數的判斷與偵測,但是呼叫部分並不在這裡。我們繼續跟進process_nested_data函數。

PHP核心層解析反序列化漏洞

PHP核心層解析反序列化漏洞

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

PHP核心層解析反序列化漏洞

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

PHP核心層解析反序列化漏洞

這裡開始新的一輪偵錯。發現在序列化完成後,在PHP_VAR_UNSERIALIZE_DESTROY釋放空間處出現了我們所希望看到的呼叫。

PHP核心層解析反序列化漏洞

#

還記得反序列化流程中當發現有__wakeup時對其進行的VAR_WAKEUP_FLAG標誌麼,在這裡當遍歷bar_dtor_hash數組遇到這個標誌時,正式開啟對__wakeup調用,後期的調用手法和前面所介紹的__sleep呼叫手法完全相同,這裡不再做重複說明。至此,反序列化所有流程完畢。

3.2.1 serialize流程小結

我們可以從上面可以看到,反序列化流程相對於序列化流程來說並沒有因為是否出現魔法函數來對流程造成分歧。 Unserialize流程如下:

取得反序列化字串–>依型別進行反序列化—>查表找到對應的反序列化類別–>依字串判斷元素個數–> new出新實例–>迭代解析化剩餘的字串–>判斷是否具有魔法函數__wakeup並標記—>釋放空間並判斷是否具有標記—>開啟呼叫。

四、PHP反序列化漏洞

有了上面原始碼基礎的鋪墊,我們現在再來探究漏洞CVE-2016-7124(繞過__wakeup)魔法函數。

因此漏洞對版本有一定要求,我們使用上面編譯好的另一個PHP版本(5.6.10)來復現和偵錯此漏洞。

首先我們進行漏洞復現:

PHP核心層解析反序列化漏洞

我們這裡可以看到,TEST類別只包含一個元素$a,我們這裡在反序列化時當修改元素字串中代表元素個數的數值時,會觸發此漏洞,該類別避過了魔法函數__wakeup的呼叫。

當然在觸發漏洞的過程中也發現了一個有趣的現象,觸發手段並不只有這一種。

PHP核心層解析反序列化漏洞

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

PHP核心層解析反序列化漏洞

我們根據上圖可以看到,在反序列化的字串中,只要在解析類別中的元素出現錯誤時,都會觸發此漏洞。但是更改類別元素內部操作(如上圖的修改字串長度,類別變數類型等)會導致類別成員變數賦值失敗。只有在修改類別成員的個數(比原有成員個數大)時,才能保證類別成員賦值時成功的。

我們下面來透過調試來看問題所在:

根據第三部分我們對反序列化原始碼的分析,猜測可能是在最後解析變數那裡出了問題。我們在這裡直接上調試器動態調試下:

PHP核心層解析反序列化漏洞

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

PHP核心層解析反序列化漏洞

在函數object_common2中,有兩個主要操作,process_nested_data迭代解析類別中的資料和魔法函數__wakeup的調用,且當process_nested_data函數解析失敗後,直接傳回0值,後面的__wakeup函數將沒有呼叫的機會。

這裡就解釋了為何觸發漏洞不只一種payload。

PHP核心層解析反序列化漏洞

當只修改類別成員的數量時,while循環可以完成的進行一次,這使得我們類別中成員變數能被完整的賦值。當修改成員變數內部時,pap_var_unserialize函數呼叫失敗,緊接著會呼叫zval_dtor 和FREE_ZVAL函數釋放目前key(變數)空間,導致類別中的變數賦值失敗。

反觀在PHP7.3.0版本中此處並沒有出現呼叫過程,只是做了簡單的標記,整個魔法函數的呼叫過程的時機移至釋放資料處。這樣就避免了這個繞過的問題。此漏洞應該屬於邏輯上的缺陷所導致的。

以上是PHP核心層解析反序列化漏洞的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:freebuf.com。如有侵權,請聯絡admin@php.cn刪除