您好,登錄后才能下訂單哦!
PHP反序列化與WordPress BUG的有趣結合是怎樣的,很多新手對此不是很清楚,為了幫助大家解決這個難題,下面小編將為大家詳細講解,有這方面需求的人可以來學習下,希望你能有所收獲。
幾個月前,我正在編寫一篇關于PHP反序列化漏洞的博客文章,決定為這篇文章找一個真實目標,能夠讓我將測試數據傳輸給PHP unserialize ()函數來實現演示目的。于是我下載了一批WordPress插件,并開始通過grepping來尋找調用unserialize ()的代碼實例:
$url = 'http://api.wordpress.org/plugins/info/1.0/'; $response = wp_remote_post ($url, array ('body' => $request)); $plugin_info = @unserialize ($response ['body']); if (isset ($plugin_info->ratings)) {
這個插件的問題在于發送明文HTTP請求,并且將該請求響應傳遞給了unserialize ()函數。就真實攻擊而言,它并不是最佳入口點,但是如果我能通過這種微不足道的方式向unserialize ()函數提供輸出來觸發代碼的話,這就足夠了!
簡單來說,當攻擊者能夠將他的數據提供給應用程序,而該應用程序將數據轉化為運行對象時沒有作適當驗證的時候就會出現反序列化漏洞。如果攻擊者數據被允許去控制運行對象的屬性,那么攻擊者就可以操縱任何使用這些對象屬性的代碼執行流程,就有可能使用它發起攻擊。這是一種稱為面向屬性編程(POP)的技術,一個POP小工具是可以通過這種方式控制任何代碼片段,開發實現是通過向應用程序提供特制對象,以便在這些對象進行反序列化的時候觸發一些有用的行為。如果想了解更多詳情的話,可以參閱我的博客文章《Attacking Java Deserialization》(https://nickbloor.co.uk/2017/08/13/attacking-java-deserialization/),其中的一般概念適用于任何基礎技術。
在PHP應用程序的現狀來看,POP小工具最為人熟知和最可靠的原因在于類的__wakeup()方法(PHP“魔術方法”,unserialize()函數會檢查是否存在__wakeup(),如果存在,則會先調用__wakeup()方法,預先準備對象需要的資源),如果一個類定義了__wakeup()方法,那么無論何時該類的某個對象使用了unserialize ()函數進行反序列化都能保證__wakeup()方法被調用,另外一個原因是__destruct ()方法(當創建的對象被銷毀或遇到PHP結束標記的時候,比如程序已經執行完畢,對象會自動調用__destruct()執行一些相應的操作,可以自行定義),例如PHP腳本執行完成時(未發生致命錯誤),當反序列化對象超出范圍時仍幾乎可以保證__destruct ()方法被調用。
除了__wakeup ()和__destruct ()方法之外, PHP還有其他“魔術方法”,可以在類中定義,也可以在反序列化之后調用,這取決于反序列化對象的使用方式。在一個更大更復雜的應用程序中可能很難追蹤到反序列化對象在哪里結束以及如何來使用它或調用那些方法,于是確定那些類可以用于PHP反序列化漏洞利用也很困難,因為相關文件可能未包含在入口點,或者一個類的自動加載器(例如spl_autoload_register()函數)可能以及被注冊來進一步混淆。
0x02 通用的PHP POP小工具
為了簡化這個過程,我編寫了一個PHP類,它定義了所有魔術方法并且在調用任何魔術方法時將詳細信息寫入日志文件。特別有趣的是魔術方法__get()和__call(),如果應用程序嘗試獲取不存在的屬性或調用該類中不存在的方法時就會調用以上魔術方法,前者可以用來識別在payload object上設置的屬性,以便操縱并使用這些屬性的代碼,而后者可以用來識別POP小工具觸發使用的非魔術方法(并且可以將它們自身用作POP小工具)。
該類的__wakeup ()方法還使用了get_declared_classes ()函數來檢索和記錄可以利用exploit payload的已聲明類的列表(雖然這不會反映當前未聲明但可以自動加載的類)。
<?php if(!class_exists("UniversalPOPGadget")) { class UniversalPOPGadget { private function logEvent($event) { file_put_contents('UniversalPOPGadget.txt', $event . "\r\n", FILE_APPEND); } public function __construct() { $this->logEvent('UniversalPOPGadget::__construct()'); } public function __destruct() { $this->logEvent('UniversalPOPGadget::__destruct()'); } public function __call($name, $args) { $this->logEvent('UniversalPOPGadget::__call(' . $name . ', ' . implode(',', $args) . ')'); } public static function __callStatic($name, $args) { $this->logEvent('UniversalPOPGadget::__callStatic(' . $name . ', ' . implode(',', $args) . ')'); } public function __get($name) { $this->logEvent('UniversalPOPGadget::__get(' . $name . ')'); } public function __set($name, $value) { $this->logEvent('UniversalPOPGadget::__set(' . $name . ', ' . $value . ')'); } public function __isset($name) { $this->logEvent('UniversalPOPGadget::__isset(' . $name . ')'); } public function __unset($name) { $this->logEvent('UniversalPOPGadget::__unset(' . $name . ')'); } public function __sleep() { $this->logEvent('UniversalPOPGadget::__sleep()'); return array(); } public function __wakeup() { $this->logEvent('UniversalPOPGadget::__wakeup()'); $this->logEvent(" [!] Defined classes:"); foreach(get_declared_classes() as $c) { $this->logEvent(" [+] " . $c); } } public function __toString() { $this->logEvent('UniversalPOPGadget::__toString()'); } public function __invoke($param) { $this->logEvent('UniversalPOPGadget::__invoke(' . $param . ')'); } public function __set_state($properties) { $this->logEvent('UniversalPOPGadget::__set_state(' . implode(',', $properties) . ')'); } public function __clone() { $this->logEvent('UniversalPOPGadget::__clone()'); } public function __debugInfo() { $this->logEvent('UniversalPOPGadget::__debugInfo()'); } }} ?>
將上面的代碼保存到一個PHP文件中,我們可以通過這個在其他任何PHP腳本中插入一個include'/path/to/UniversalPOPGadget.php'語句,并使這個類可用。以下Python腳本將查找給定目錄中所有PHP文件,并將語句寫入文件前端,從而有效地檢測應用程序,以便我們可以向為其提供序列化的UniversalPOPGadget對象,來用它們研究反序列化的入口點。
import os import sys #Set this to the absolute path to the file containing the UniversalPOPGadget class GADGET_PATH = "/path/to/UniversalPOPGadget.php" #File extensions to instrument FILE_EXTENSIONS = [".php", ".php3", ".php4", ".php5", ".phtml", ".inc"] #Check command line args if len(sys.argv) != 2: print "Usage: GadgetInjector.py <path>" print "" sys.exit() #Search the given path for PHP files and modify them to include the universal POP gadget for root, dirs, files in os.walk(sys.argv[1]): for filename in files: for ext in FILE_EXTENSIONS: if filename.lower().endswith(ext): #Instrument the file and stop checking file extensions fIn = open(os.path.join(root, filename), "rb") phpCode = fIn.read() fIn.close() fOut = open(os.path.join(root, filename), "wb") fOut.write("<?php include '" + GADGET_PATH + "'; ?>" + phpCode) fOut.close() break
回到剛剛那個調用unserialize()函數的WordPress插件代碼片段,我不知道該如何去實際觸發unserialize()函數的調用,我所知道的是這個插件應該向http://api.wordpress.org/plugins/info/1.0/發送HTTP請求,于是我使用上面的Python腳本來測試WordPress和插件代碼,然后修改了服務器上的hosts文件,將api.wordpress.org指向同一臺服務器。以下代碼放在Web根目錄中的/plugins/info/1.0/index.php文件中,以便提供UniversalPOPGadget payload:
<?php include('UniversalPOPGadget.php'); print serialize(new UniversalPOPGadget());
在使用這種手段后,我開始像往常一樣使用WordPress實例,特別注意了與目標WordPress插件相關的所有功能,同時查看UniversalPOPGadget日志文件。很快地,生成了一些日志文件,其中包括以下內容(為簡潔起見,已將大量可用類刪除):
UniversalPOPGadget::__wakeup() [!] Defined classes: [...Snipped...] UniversalPOPGadget::__get(sections) UniversalPOPGadget::__isset(version) UniversalPOPGadget::__isset(author) UniversalPOPGadget::__isset(requires) UniversalPOPGadget::__isset(tested) UniversalPOPGadget::__isset(homepage) UniversalPOPGadget::__isset(downloaded) UniversalPOPGadget::__isset(slug) UniversalPOPGadget::__get(sections) UniversalPOPGadget::__get(sections) UniversalPOPGadget::__isset(banners) UniversalPOPGadget::__get(name) UniversalPOPGadget::__get(sections) UniversalPOPGadget::__isset(version) UniversalPOPGadget::__isset(author) UniversalPOPGadget::__isset(last_updated) UniversalPOPGadget::__isset(requires) UniversalPOPGadget::__isset(tested) UniversalPOPGadget::__isset(active_installs) UniversalPOPGadget::__isset(slug) UniversalPOPGadget::__isset(homepage) UniversalPOPGadget::__isset(donate_link) UniversalPOPGadget::__isset(rating) UniversalPOPGadget::__isset(ratings) UniversalPOPGadget::__isset(contributors) UniversalPOPGadget::__isset(tested) UniversalPOPGadget::__isset(requires) UniversalPOPGadget::__get(sections) UniversalPOPGadget::__isset(download_link)
日志文件中顯示,在UniversalPOPGadget對象被反序列化之后,用程序試圖獲取或檢查是否存在多個屬性(段、版本、作者等等)。首先這就告訴我們,通過這個特定的入口點我們可以使用任何可用類中的任何定義在__get ()或__isset ()方法中的代碼來作為POP小工具,其次它揭示了目標應用程序試圖獲得的幾個屬性,這些屬性幾乎保證影響執行流程,因此可能對開發有用處。
上面的日志文件顯示,與反序列化對象的首次交互是嘗試獲取名為sections的屬性。
$url = 'http://api.wordpress.org/plugins/info/1.0/'; $response = wp_remote_post ($url, array ('body' => $request)); $plugin_info = @unserialize ($response ['body']); if (isset ($plugin_info->ratings)) {
現在來看最初的目標插件,它在調用unserialize ()之后做的第一件事是檢查名為rating的屬性是否存在,那么這個日志并不是我當初注意的第三方插件產生的!
對WordPress代碼進行一次快速grep,對于上面提到的HTTP URL,顯示該請求是由wp-admin/includes/plugin-install.php文件中的WordPress插件API發送的。瀏覽代碼時并不清楚反序列化的payload object是如何使用的,或者切確地說這個HTTP請求以及隨后對unserialize ()函數的調用是從哪里觸發的。我繼續點擊WordPress管理界面,發現日志是從主控制面板、更新頁面和插件頁面生成的。重新加載這些頁面使我能夠觸發目標HTTP請求,并向unserialize ()函數提供任意數據。
我記錄了一些WordPress發出的HTTP請求并把它們發送到真正的api.wordpress.org以獲取實例響應,結果響應的是stdClass類型的序列化對象,更重要的是示例響應給了我一個預期中WordPress會收到的屬性的確切列表,其中每個屬性都有可能用于操控某些核心WordPress代碼的執行流程。我根據捕獲到的真實響應修改了偽造的api.wordpress.org用來返回序列化對象。以下是這個的一個簡單例子:
<?php $payloadObject = new stdClass(); $payloadObject->name = "PluginName"; $payloadObject->slug = "PluginSlug"; $payloadObject->version = "PluginVersion"; print serialize($payloadObject);
我開始修改這些對象的屬性并刷新相關的WordPress頁面,來測試修改內容對結果頁面有何影響(如果有的話)。在有些情況下WordPress使用了HTML編碼來防止HTML/JavaScript注入,但是最終我發現了幾個可以插入任意HTML和JavaScript的字段。請記住這個情況是發生在管理界面內,如果管理員登錄并瀏覽“更新”或“插件”頁面,攻擊者就能夠對WordPress站點執行MitM攻擊或DNS欺騙,也可能會利用此漏洞實現遠程代碼執行。
在快速嘗試一些JavaScript和Python腳本之后我有了假設漏洞的運用證明。這個PoC會導致WordPress管理界面中的“更新和插件”菜單旁顯示一個徽章,表示有更新可用(當然即使沒有也會顯示),這可能會誘導管理員點擊這些鏈接來檢查并可能安裝這些更新。如果有管理員點擊任一鏈接,那么一個JavaScript payload被注入到該頁面中,然后就添加了一個新的管理員賬戶并將一個基本PHP命令shell注入到現行的WordPress主題的index.php中。
在大多數情況下這種PoC攻擊足以實現代碼執行,但是我也發現了我可以使用類似方式向WordPress發送一個錯誤的插件更新來攻擊WordPress管理界面的點擊更新功能,如果有管理員點擊了更新按鈕,就會導致下載一個假插件更新的ZIP文件并將其提取至服務器上。
深入挖掘這一點,我注意到即使沒有登錄,WordPress也會發送了類似對api.wordpress.org的HTTP請求,我開始對WordPress進行代碼審計來了解其中發生了什么,以及它是否可能遭受了類似攻擊。我在wp-includes/update.php文件中發現了wp_schedule_update_checks()函數。
function wp_schedule_update_checks() { if ( ! wp_next_scheduled( 'wp_version_check' ) && ! wp_installing() ) wp_schedule_event(time(), 'twicedaily', 'wp_version_check'); if ( ! wp_next_scheduled( 'wp_update_plugins' ) && ! wp_installing() ) wp_schedule_event(time(), 'twicedaily', 'wp_update_plugins'); if ( ! wp_next_scheduled( 'wp_update_themes' ) && ! wp_installing() ) wp_schedule_event(time(), 'twicedaily', 'wp_update_themes'); }
WordPress會每天兩次調用wp_version_check ()函數、wp_update_plugins ()函數和wp_update_themes ()函數。默認情況下,這些更新檢查也可以通過wp-cron.php發送HTTP請求來觸發。于是我開始手動審計這些函數,并修改代碼來記錄各種數據以及分支和函數調用的結果,查看發生了什么,函數是否根據來自api.wordpress.org的響應而做出了任何危險的操作。
最終我設法偽造了來自api.wordpress.org的幾個響應,來觸發對$upgrader->upgrade()的調用,然而以前的偽造插件更新攻擊在這里似乎不起作用,之后我在should_update()方法中發現了以下注釋:
/** * [...Snipped...] * * Generally speaking, plugins, themes, and major core versions are not updated * by default, while translations and minor and development versions for core * are updated by default. * * [...Snipped...] */
事實證明這是WordPress試圖升級內置Hello Dolly插件的翻譯,我一直試圖從downloads.wordpress.org下載hello-dolly-1.6-en_GB.zip,而不是請求我偽造的插件zip文件。我下載了原始文件,添加了一個shell.php文件,并將其托管在我的虛假downloads.wordpress.org網站上。于是下一次我訪問了wp-cron.php,WordPress下載了偽造的更新并解壓到wp-content/languages/plugins/,其中包括shell等等。
攻擊者既然可以對WordPress網站執行MitM攻擊或DNS欺騙,那么就可以針對自動更新功能執行零交互攻擊,并將惡意腳本寫入服務器。當然這不一定是一次簡單的攻擊,但這仍然不可能!
WordPress團隊意識到這些問題,但是他們的立場似乎是,如果HTTPS啟用失敗,為了允許在具有舊或損壞的SSL堆棧系統上運行的WordPress網站進行更新,WordPress將會故意降級為HTTP連接(或者安裝惡意代碼)……
當請求更新詳細信息和更新存檔時,WordPress會嘗試首先通過HTTPS連接到api.wordpress.org和downloads.wordpress.org,但是如果由于任何原因導致HTTPS啟用失敗,則使用明文HTTP連接。
如果WordPress的PHP腳本屬于不同的用戶,那么WordPress將默認無法自動更新(因此不容易受到上述攻擊),例如index.php為用戶foo擁有,但WordPress是在用戶www-data權限下運行的。
看完上述內容是否對您有幫助呢?如果還想對相關知識有進一步的了解或閱讀更多相關文章,請關注億速云行業資訊頻道,感謝您對億速云的支持。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。