0x01 前言
我們通常把反序列化漏洞和反序列化利用鏈分開來看,有反序列化漏洞不一定有反序列化利用鏈(經常用shiro反序列化工具的人一定遇到過一種場景就是找到了key,但是找不到gadget,這也就是在這種場景下沒有可利用的反序列化利用鏈)。如果我們向某個漏洞提交平臺提交一個反序列化漏洞,但是不給反序列化利用鏈,那麼平臺大機率是不會接受這種漏洞的。
反序列化利用鏈是整個反序列化利用過程中最關鍵的一環,通常反序列化利用鏈需要藉助常用的第三方jar包,其中最有名的就是CommonCollections利用鏈(簡稱CC鏈)。
往期推薦
1、告別指令碼小子系列丨JAVA安全(1)——JAVA本地除錯和遠端除錯技巧
2、告別指令碼小子系列丨JAVA安全(2)——JAVA反編譯技巧
3、告別指令碼小子系列丨JAVA安全(3)——JAVA反射機制
4、告別指令碼小子系列丨JAVA安全(4)——ClassLoader機制與冰蠍Webshell分析
5、告別指令碼小子系列丨JAVA安全(5)——序列化與反序列化
0x02 反序列化環境準備
為了更方便初學者來學習反序列化利用鏈,我們首先要準備當前需要的實驗環境。學習反序列化利用鏈最好的辦法是參考ysoserial(https://github.com/frohoff/ysoserial),ysoserial是一個開源的整合化反序列化利用鏈工具,可以快速生成反序列化利用鏈payload。
如果是不追求細節的小夥伴,可以直接按照參照ysoserial使用文件來生成payload,如下圖所示。
本文的主要目的是教會小夥伴們告別指令碼小子,所以就不直接使用編譯好的jar包,但是我們後面很多利用程式碼會參考ysoserial中的程式碼,ysoserial中關於反序列化利用鏈的程式碼都在ysoserial.payloads包中。
我們的目的是不透過ysoserial框架,自己實現相應的反序列化利用鏈。我們搭建反序列化測試的基礎環境,新建maven專案,新增專案依賴的jar包。
為了模擬序列化和反序列化的過程,編寫兩個公用的靜態方法,這兩個方法將在後面的反序列化利用鏈中被多次使用。其中searialize方法的作用是把物件obj序列化之後儲存到檔案filename中,unserialize方法的作用是把檔案filename中的資料讀出來反序列化為物件。
透過讀寫檔案來模擬序列化和反序列化的過程是最直觀的實現方式,但是在實際環境中,我們遇到的都是基於POST輸入的字元輸入流來進行反序列化,如下圖所示。本地讀寫檔案的反序列化和基於POST輸入字元的反序列化是完全沒有區別的,所以我們後面在研究反序列化利用鏈的時候都是透過本地檔案讀寫的方式來實現,而不會準備專門的WEB漏洞環境。
某系統反序列化漏洞例項
0x03 反序列化利用鏈
0x3.1 URLDNS利用鏈
URLDNS鏈是JAVA眾多利用鏈中最簡單的一條利用鏈,非常適合初學者研究學習,具有下面的特點:
1)利用鏈只依賴jdk本身提供的類,不依賴其他第三方類,所以具有很高的通用性,可以用於判斷目標是否存在反序列化漏洞。
2)利用鏈本身只能執行域名解析的操作,不能執行系統命令或者其他惡意操作。如果向漏洞提交平臺提交反序列化漏洞,但是利用鏈是URLDNS的利用鏈,那麼漏洞提交平臺可能會拒絕這個漏洞。
Ysoserial中關於URLDNS鏈的主要程式碼如下圖3.1.1,圖3.1.2所示。這裡面有一個很關鍵的點是使用了自定義的SilentURLStreamHandler類,為什麼要使用這個自定義的類,我將在後面說明原因。
圖3.1.1 生成URLDNS利用鏈物件
圖3.1.2 自定義SilentURLStreamHandler類
我們先把ysoserial的程式碼搬運到本地,做一名合格的搬運工。直接搬運過來之後執行payload可以檢視到DNSLOG的日誌,證明搬運工沒有問題了。搬運過來之後需要去掉ysoserial中的反射呼叫,我們這裡沒有Reflections類,所以自己寫關於反射呼叫的程式碼,如圖3.1.3所示,執行結果如圖3.1.4所示
圖3.1.3 本地引用URLDNS利用鏈
圖3.1.4 執行之後可以在DNSLOG檢視DNS請求日誌
為了理清楚利用鏈的整個流程,我們在java.net. URLStreamHandler類的getHostAddress方法中下斷點除錯一下,如圖3.1.5所示。
圖3.1.5 下斷點除錯URLDNS利用鏈
執行整個payload,根據debug檢視棧呼叫情況,如圖3.1.6所示。其中最關鍵的紅色的框中的部分,可以看出整個利用鏈非常簡單。
圖3.1.6 URLDNS利用鏈的棧呼叫情況
首先反序列化入口是在HashMap的readObject,在這個方法中會呼叫本類的hash方法,如圖3.1.7所示。
圖3.1.7 在HashMap的readObject方法中呼叫
HashMap的hash方法
繼續跟進hash方法,在這個方法中會呼叫key的hashCode方法。Key對應的值是java.net.URL類的物件,如圖3.1.8所示。
圖3.1.8 透過hash方法呼叫URL類的hashCode方法
繼續跟進會呼叫java.net.URL類的hashCode方法,會呼叫handler欄位的hashCode方法。而handler定義來自於URLStreamHandler類,所以會呼叫URLStreamHandler類的handler方法,如圖3.1.9所示。
圖3.1.9 URL類的hashCode方法呼叫URLStreamHandler類的hashCode方法
繼續跟進java.net.URLStreamHandler類的hashCode方法,如圖3.1.10所示。這裡可以看出裡面就直接呼叫了getHostAddress方法,該方法執行之後會進行域名到IP地址的解析請求。
圖3.1.10 最終呼叫了域名解析相關的方法getHostAddress方法
到這裡已經看完了整個呼叫棧的流程,但是還是沒有找到任何理由必須要用自定義的SilentURLStreamHandler類。為了理清楚原因,我們對原來的payload進行修改,去除自定義的SilentURLStreamHandler類,如圖3.1.11所示。
圖3.1.11 修改後的URLDNS利用鏈
還是在java.net. URLStreamHandler類的getHostAddress方法中下斷點。可以看到整個棧呼叫情況如圖3.1.12所示。可以看出觸發getHostAddress方法的入口點是從generalURLDNS2這個方法,而不是unserialize方法,也就是在生成序列化物件的時候就已經觸發了域名解析的請求。由於JAVA內部對DNS請求存在快取機制,那麼在反序列化的時候會優先從DNS快取中查詢域名解析記錄,那麼反序列化的時候就收不到正確的DNS請求資料。
圖3.1.12 修改後的URLDNS利用鏈
我們跟一下在生成payload時候觸發getHostAddress的流程,其中最關鍵的是執行HashMap中的put方法,如圖3.1.13所示。
圖3.1.13 URLDNS利用鏈生成物件時呼叫HashMap的put方法
跟蹤put方法的定義,如圖3.1.14所示。裡面會呼叫hash方法
圖3.1.14 HashMap的put方法會呼叫本類的hash方法
剩下的流程和上面分析呼叫棧一樣,最終會導致觸發執行getHostAddress方法。
為了避免在生成payload的時候觸發DNS請求,影響反序列化時DNS請求的執行。有兩種解決辦法。
1)第一種就是像ysoserial程式碼中的方式一樣,定義一個類繼承自URLStreamHandler,並且在類中重寫getHostAddress等方法,使得在序列化的時候不會執行getHostAddress方法。
2)第二種辦法是透過透過java.net.URL類中的hashCode方法中的邏輯來避免執行後續的getHostAddress方法。
第一種方法在ysoserial的程式碼中已經寫很清楚了,這裡主要再說一下第二種方式。在整個利用鏈中一個很重要的步驟是會呼叫java.net.URL類中的hashCode方法。如圖3.1.15所示。
圖3.1.15 java.net.URL類中的hashCode方法程式碼邏輯
這裡有一個很重要的判斷語句是,如果hashCode不等於-1,則直接返回對應hashCode的值,否則呼叫hashCode方法計算對應的值。這裡需要特別注意的一點是這裡的hashCode既是java.net.URL類的方法,也是java.net.URL類的欄位。所以我們需要
1)第一次put的時候把hashCode欄位設定為不是-1的值,避免執行下面的handler.hashCode(this)。
2)生成序列化物件的時候又需要把hashCode欄位設定為-1,因為反序列化的時候需要執行下面的handler.hashCode(this)。
所以我們也可以透過反射修改hashCode欄位的方式來避免在序列化的時候觸發DNS請求,再次修改後的程式碼如圖3.1.16所示。這裡最主要的是增加了透過反射修改hashCode欄位的操作。
圖3.1.16 透過控制hashCode欄位避免序列化時候觸發DNS請求
這樣之後也能達到和ysoserial程式碼一樣的效果。也能正常觸發URLDNS的請求,如圖3.1.17所示。
圖3.1.17 透過修改的payload觸發URLDNS鏈的DNS請求
0x3.2 CC鏈
CC鏈是最早出現的影響較大的java反序列化利用鏈,原作者一共給出7條利用鏈,但是後來有很多大牛在此基礎上給出了一些改進的利用鏈,對初學者而言,學習CC鏈是屬於學習JAVA反序列化利用鏈的重要基礎。
關於CC鏈的內容很多,限於篇幅有限,這次的文章先開頭對部分CC鏈進行講解,關於CC鏈的更多內容將在下一篇文章中詳述。本次我們主要先講關於CC鏈中的Transform鏈。
在學習Transform鏈之前,首先需要說明JAVA中命令執行的方式。JAVA中最典型的執行作業系統命令的辦法是透過Runtime類來執行,如圖3.2.1所示。
圖3.2.1 JAVA中執行系統命令的方式
但是在反序列化的過程中不能直接透過Runtime來執行,因為Runtime類沒有繼承Serializable介面,如圖3.2.2所示。
圖3.2.2 Runtime類沒有繼承Serializable介面
但是我們可以透過反射的方式來呼叫Runtime類執行,並且裡面涉及到的全部類都繼承了Serializable介面,如圖3.2.3所示。這裡有一個坑是,透過反射來執行方法的返回值型別一定是Object型別,需要做強制型別轉換,把Objectl型別轉化為Runtime型別。後續的Transform利用鏈基本上都是透過執行這段程式碼來執行系統命令的。
圖3.2.3 透過反射的方式來執行Runtime類的exec方法
嚴格來說Transform鏈屬於整個CC鏈中的Sink點,CC鏈基本上都是透過Transform來最終執行系統命令的。學習Tranform鏈是掌握CC鏈的基礎前提,最典型的Transform鏈如圖3.2.4所示。
圖3.2.4 典型Transform鏈執行情況
直接執行上面的程式碼是可以彈出計算器的,多數CC鏈最終也是透過呼叫類似的程式碼來執行系統命令。為了理解Transforml鏈的內容,需要分開來看裡面涉及到的幾個類:InvokerTransformer類、ConstantTransformer類和ChainedTransformer類。
在InvokerTransformer類中,最主要的是transform方法。該方法中透過反射的方式執行任意一個類的方式,如圖3.2.5所示。Transform執行的方法必須是public修飾符的。
圖3.2.5 透過InvokerTransformer類的transform方法執行任意public方法
InvokerTransformer類的transform方法提供了一種執行任意其他方法的路徑,我們寫一個簡單的例子來幫助大家理解transform方法,如圖3.2.6所示。定義一個類TEST,類中定義一個方法Hello,那麼我們就可以透過tranform方法來執行TEST類的Hello方法。
圖3.2.6 典型的transform方法呼叫
可能有的小夥伴會疑惑,我要執行Hello,為什麼不直接呼叫,非要透過transform來呼叫呢?試想一下,如果我們要執行的是“Runtime.getRuntime()”這樣的方法,但是整個系統中都沒有任何地方直接呼叫了這個,那麼我們是不是就可以透過transform來間接的執行這個方法呢。
但是現在透過transform來執行方法還有一個很明顯的不足,就是隻能執行單個物件的單個方法,不能鏈式呼叫。形象一點來說明這個問題,我們可以執行”Runtime.getRuntim()”,但是我們不能執行“Runtime.getRuntime().exec(xxxx)”。我們需要找到鏈式呼叫的方式,幸運的是CommonsCollections中提供了另一個類ChainedTransformer。
ChainedTransformer類中提供了鏈式呼叫transform方法的辦法,如圖3.2.7所示。ChainedTransformer類的構造方法是傳入Transformer型別的陣列,透過transform方法依次遍歷陣列中的每一個元素,上一步的方法呼叫的輸出作為下一步的方法呼叫的輸入,完美的鏈式呼叫解決辦法。
圖3.2.7 透過ChainedTransformer類鏈式呼叫transform方法
現在已經可以鏈式呼叫來執行命令了,但是還有一個問題沒有解決,從圖3.2.3中可以看出要執行命令,第一步是獲取Runtime.class物件,如何透過某個類的transform物件來獲取Runtime.class物件呢?這個時候就要用到CommonCollections包裡面另一個類ChainedTransformer。
ChainedTransformer類的transform方法很簡單,就是直接返回建構函式中的iConstant欄位,如圖3.2.8所示。這完美地解決了我們想要Runtime.class物件的願望。
圖3.2.8 透過ChainedTransformer類獲取Runtime.class物件
透過上面的分析可以知道透過Transform鏈已經可以達到執行作業系統命令的效果,如圖3.2.4所示。目前的情況是如果反序列化的時候,可以呼叫ChainedTransformer類的transform方法,則可以導致命令執行,但是反序列化的時候會自動呼叫的是readObject方法,如何透過readObject方法來呼叫transform方法呢?這是下一次分享的內容,未完待續。
0x04 總結
這次主要分享的是關於反序列化利用鏈的知識,主要介紹了URLDNS利用鏈和CC鏈中的Transform鏈。這些都是屬於JAVA反序列化的基礎知識,後續的知識都會在此基礎上進行延展。從文中可以看出反序列化利用鏈通常包含比較複雜的邏輯知識,就算是最簡單的URLDNS鏈也能讓初學者難受,分析反序列化利用鏈不能太快,理解很重要。後續還有更多關於反序列化利用鏈的分享,歡迎交流溝通。
參考連結
https://xz.aliyun.com/t/9873
https://github.com/frohoff/ysoserial
瞭解更多