由Typecho 深入理解PHP反序列化漏洞
零、前言
Typecho是一個輕量版的博客系統,前幾天爆出getshell漏洞,網上也已經有相關的漏洞分析發布。這個漏洞是由PHP反序列化漏洞造成的,所以這裡我們分析一下這個漏洞,並藉此漏洞深入理解PHP反序列化漏洞。
一、 PHP反序列化漏洞
1.1 漏洞簡介
PHP反序列化漏洞也叫PHP對象注入,是一個非常常見的漏洞,這種類型的漏洞雖然有些難以利用,但一旦利用成功就會造成非常危險的後果。漏洞的形成的根本原因是程序沒有對用戶輸入的反序列化字元串進行檢測,導致反序列化過程可以被惡意控制,進而造成代碼執行、getshell等一系列不可控的後果。反序列化漏洞並不是PHP特有,也存在於Java、Python等語言之中,但其原理基本相通。
1.2 漏洞原理
接下來我們通過幾個實例來理解什麼是PHP序列化與反序列化以及漏洞形成的具體過程,首先建立1.php文件文件內容如下:
文件中有一個TestClass類,類中定義了一個$variable變數和一個PrintVariable函數,然後實例化這個類並調用它的方法,運行結果如下:
這是一個正常的類的實例化和成員函數調用過程,但是有一些特殊的類成員函數在某些特定情況下會自動調用,稱之為magic函數,magic函數命名是以符號__開頭的,比如__construct當一個對象創建時被調用,__destruct當一個對象銷毀時被調用,__toString當一個對象被當作一個字元串被調用。為了更好的理解magic方法是如何工作的,在2.php中增加了三個magic方法,__construct, __destruct和__toString。
運行結果如下,注意還有其他的magic方法,這裡只列舉了幾個。
php允許保存一個對象方便以後重用,這個過程被稱為序列化。為什麼要有序列化這種機制呢?因為在傳遞變數的過程中,有可能遇到變數值要跨腳本文件傳遞的過程。試想,如果在一個腳本中想要調用之前一個腳本的變數,但是前一個腳本已經執行完畢,所有的變數和內容釋放掉了,我們要如何操作呢?難道要前一個腳本不斷的循環,等待後面腳本調用?這肯定是不現實的。serialize和unserialize就是用來解決這一問題的。serialize可以將變數轉換為字元串並且在轉換中可以保存當前變數的值;unserialize則可以將serialize生成的字元串變換回變數。讓我們在3.php中添加序列化的例子,看看php對象序列化之後的格式。
輸出如下
O表示對象,4表示對象名長度為4,」User」為類名,2表示成員變數個數,大括弧里分別為變數的類型、名稱、長度及其值。想要將這個字元串恢復成類對象需要使用unserialize重建對象,在4.php中寫入如下代碼
運行結果
magic函數__construct和__destruct會在對象創建或者銷毀時自動調用,__sleep方法在一個對象被序列化的時候調用,__wakeup方法在一個對象被反序列化的時候調用。在5.php中添加這幾個magic函數的例子。
運行結果
OK,到此我們已經知道了magic函數、序列化與反序列化這幾個重要概念,那麼這個過程漏洞是怎麼產生的呢?我們再來看一個例子6.php
這段代碼包含兩個類,一個example和一個process,在process中有一個成員函數close(),其中有一個eval()函數,但是其參數不可控,我們無法利用它執行任意代碼。但是在example類中有一個__destruct()析構函數,它會在腳本調用結束的時候執行,析構函數調用了本類中的一個成員函數shutdown(),其作用是調用某個地方的close()函數。於是開始思考這樣一個問題:能否讓他去調用process中的close()函數且$pid變數可控呢?答案是可以的,只要在反序列化的時候$handle是process的一個類對象,$pid是想要執行的任意代碼代碼即可,看一下如何構造POC
執行效果
當我們序列化的字元串進行反序列化時就會按照我們的設定生成一個example類對象,當腳本結束時自動調用__destruct()函數,然後調用shutdown()函數,此時$handle為process的類對象,所以接下來會調用process的close()函數,eval()就會執行,而$pid也可以進行設置,此時就造成了代碼執行。這整個攻擊線路我們稱之為ROP(Return-oriented programming)鏈,其核心思想是在整個進程空間內現存的函數中尋找適合代碼片斷(gadget),並通過精心設計返回代碼把各個gadget拼接起來,從而達到惡意攻擊的目的。構造ROP攻擊的難點在於,我們需要在整個進程空間中搜索我們需要的gadgets,這需要花費相當長的時間。但一旦完成了「搜索」和「拼接」,這樣的攻擊是無法抵擋的,因為它用到的都是程序中合法的的代碼,普通的防護手段難以檢測。反序列化漏洞需要滿足兩個條件:
1、程序中存在序列化字元串的輸入點n2、程序中存在可以利用的magic函數n
接下來通過Typecho的序列化漏洞進行實戰分析。
二 Typecho漏洞分析
漏洞的位置發生在install.php,首先有一個referer的檢測,使其值為一個站內的地址即可繞過。
入口點在232行
這裡將cookie中的__typecho_config值取出,然後base64解碼再進行反序列化,這就滿足了漏洞發生的第一個條件:存在序列化字元串的輸入點。接下來就是去找一下有什麼magic方法可以利用。先全局搜索__destruct()和__wakeup()
找到兩處__destruct(),跟進去沒有可利用的地方,跟著代碼往下走會實例化一個Typecho_Db,位於varTypechoDb.php,Typecho_Db的構造函數如下
在第120行使用.運算符連接$adapterName,這時$adapterName如果是一個實例化的對象就會自動調用__toString方法(如果存在的話),那全局搜索一下__toString()方法。找到3處
前兩處無法利用,跟進第三處,__toString()在varTypechoFeed.php 223行
跟進代碼在290處有如下代碼
如何$item[author]是一個類而screenName是一個無法被直接調用的變數(私有變數或根本就不存在的變數),則會自動調用__get() magic方法,進而再去尋找可以利用的__get()方法,全局搜索
共匹配到10處,其中在varTypechoRequest.php中的代碼可以利用,跟進
再跟進到get函數
接著進入_applyFilter函數
可以看到array_map和call_user_func函數,他們都可以動態的執行函數,第一個參數表示要執行的函數的名稱,第二個參數表示要執行的函數的參數。我們可以在這裡嘗試執行任意代碼。接下來梳理一下整個流程,數據的輸入點在install.php文件的232行,從外部讀入序列化的數據。然後根據我們構造的數據,程序會進入Db.php的__construct()函數,然後進入Feed.php的__toString()函數,再依次進入Request.php的__get()、get()、_applyFilter()函數,最後由call_user_func實現任意代碼執行,整個ROP鏈形成。構造POC如下
POC的22行其實與反序列化無關,但是不加這一行程序就不會有回顯,因為在 install.php 的開頭部分調用了程序調用了ob_start(),它會開啟緩衝區並將要輸出的內容都放進緩衝區,想要使用的時候可以再取出。但是我們的對象注入會在後續的代碼中造成資料庫錯誤
然後會觸發exception,其中的ob_end_clean()會將緩衝區中的內容清空,導致無法回顯。
想要解決這個問題需要在ob_end_clean()執行之前是程序退出,兩種方法:
1、使程序跳轉到存在exit()的代碼段
2、使程序提前報錯,退出代碼
POC中使用的是第二種方法
解決了上述問題後就可以執行任意代碼並能看到回顯了,執行的時候在http頭添加referre使其等於一個站內地址,然後在cookie中添加欄位__typecho_config,其值為上述exp的輸出。
有些利用方式並不需要回顯,比如寫個shell什麼的,POC如下
執行結果,在根目錄生成shell.php
三 聲明
文章旨在普及網路安全知識,提高小夥伴的安全意識的同時介紹常見漏洞的特徵、挖掘技巧等。若讀者因此做出危害網路安全的行為後果自負,與合天智匯及本人無關,特此聲明。(PS:本文為合天原創獎勵文章,未經允許,禁止以任何形式轉載!)
推薦閱讀:
※同時學三門編程語言是什麼體驗?
※php為什麼弄點號連接字元串?
※大部分已經幹了兩三年的程序員水平是怎樣的?
※有密集型(高頻) https api 請求的需求,該用什麼技術棧?
※PHP 新手該如何學習使用開發框架,有案例嗎?