作者:welkin@京東安全
公眾號:京東安全
本文主要分為兩方面,其一是基于PriorityQueue類的序列化對象的構造,另一方面是PriorityQueue對象在反序列化過程中惡意代碼的觸發原理。
背景及概要
隨著Java應用的推廣和普及,Java安全問題越來越被人們重視,縱觀近些年來的Java安全漏洞,反序列化漏洞占了很大的比例。就影響程度來說,反序列化漏洞的總體影響也明顯高于其他類別的漏洞。
在反序列化漏洞的利用過程中,攻擊者會構造一系列的調用鏈以完成其攻擊行為。如何高效的生成符合條件且可以穩定利用的攻擊Payload成為了攻擊鏈條中的重要一環,當前已經有很多現成的工具幫助我們完成Payload的生成工作。本文主要以Ysoserial工具為例分析了基于org.apache.commons.collections4類庫的Gadget,其通過構造一個特殊的PriorityQueue對象,將其序列化成字節流后,在字節流反序列化的過程中觸發代碼執行。
更多關于Ysoserial的信息,請參考:
https://github.com/frohoff/ysoserial
本文主要分為兩方面,其一是基于PriorityQueue類的序列化對象的構造,另一方面是PriorityQueue對象在反序列化過程中惡意代碼的觸發原理。下文將從這兩方面展開描述一些細節以及實際測試時的一些問題,整體的流程如圖1-1所示。

序列化對象的構造
首先,被序列化為字節流的對象實際是一個特殊的PriorityQueue對象,本小節主要分析構造該對象的過程,即圖1-1的第一步。
圖2-1為ysoserial.payloads.CommonsCollections4中getObject方法的代碼,是用于構造該PriorityQueue對象的代碼:

上圖中需要注意的有如下兩點:
-
通過createTemplatesImpl方法生成templates對象
-
通過PriorityQueue類的比較器將構造的一系列transformer串聯起來
0x0A createTemplatesImpl方法生成攻擊載荷
通過createTemplatesImpl方法生成templates對象是非常重要的一部分,因為這是實際承載我們惡意代碼的對象,詳細說一下,跟進分析createTemplatesImpl方法,其代碼具體實現和關鍵點流程分別如下圖2-2和圖2-3所示:


首先生成TemplatesImpl實例,然后通過javassist類庫修改StubTransletPayload類字節碼,在其中插入執行命令的代碼(這里是通過java.lang.Runtime.getRuntime().exec()方法執行命令,也可以插入其他利用代碼,如反彈shell等),然后將其父類設置為abstTranslet類,最后將修改后的字節碼通過反射寫入到TemplatesImpl實例的_bytecodes變量中,這里還同時寫入了Foo.class的字節碼。除此之外,為了后續惡意代碼的觸發(如作者注釋中所寫:required to make TemplatesImpl happy),還要修改TemplatesImpl實例的_name和_tfactory變量,否則后面會在命令代碼執行前拋出異常。 StubTransletPayload類代碼實現如圖2-4所示:

StubTransletPayload類繼承自AbstractTranslet類并實現了Serializable接口,通常我們構造一個惡意類可能會直接在static代碼塊或構造方法中寫入我們想要執行的代碼,這一步在上面通過javassist類庫實現,關于StubTransletPayload類需要繼承AbstractTranslet類的原因會在反序列化惡意代碼觸發時解釋。 以上即為createTemplatesImpl方法的實現,其本質上是構造了一個特定結構的TemplatesImpl類實例,具體變量的值如圖2-5所示:

構造并串聯transformer
回到圖2-1本段開始處getObject方法的代碼中,在35行和40行分別初始化了ConstantTransformer對象和InstantiateTransformer對象,47行將兩個對象構造成Transformer數組作為參數初始化了ChainedTransformer對象chain。
而在50行,這個ChainedTransformer對象chain又是我們要序列化的對象PriorityQueue中comparator構造方法的參數,comparator可以理解為在PriorityQueue中決定優先次序的比較器,此處用的是TransformingComparator對象。
在44-45行、55-57行利用java的反射機制和引用傳遞的特性修改chain對象中的變量,ConstantTransformer對象中iConstant變量的值設為com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter.class,InstantiateTransformer對象中iParamTypes設為javax.xml.transform.Templates.class,iArgs設為此前構造的templates對象。
51、52行向隊列中插入兩個1,這里是為了后面堆化時觸發一次堆排序。 最終構造了一個用TransformingComparator對象作比較器的PriorityQueue對象,其內存中變量示意圖和抽象結構圖分別如圖2-6和圖2-7所示:


接下來將分析下這個對象序列化后的字節序列如何在反序列化的過程中觸發代碼執行。
0x3 反序列化過程中惡意代碼的觸發原理
反序列化開始至觸發代碼執行的整體流程如圖3-1所示:

反序列化過程中首先進入ObjectInputStream類的readObject方法中,然后進入readObject0方法中讀取字節流,其中會讀取tc標記,然后根據tc標記的類型進入不同的邏輯處理函數中,標記類型可見圖3-2:

反序列化的是PriorityQueue對象,這里會進入TC_OBJECT的處理邏輯中,跟進到readOrdinaryObject方法里,其具體代碼如圖3-3,在1769行讀取類描述信息,1780行通過類描述信息,初始化對象obj(即PriorityQueue對象):

在圖3-4中1793行判斷是否實現Externalizable接口,通過Externalizable接口可以通過調用對象的readExternal方法實現自定義地完全控制某一對象及其超類的流格式和內容,這里代碼進入默認的readSerialData方法中。

在圖3-5中1882行判斷序列化對象是否有readObject方法,如果有則通過反射調用對象的readObject方法為成員變量賦值,接下來就進入了PriorityQueue對象的readObject方法中。

圖3-6為PriorityQueue對象的readObject方法:

圖3-7中在defaultReadObject方法中會調用defaultReadFields方法為成員變量賦值:

defaultReadFields方法中1989行會遞歸調用readObject0方法為對象的成員變量賦值直至完成,邏輯與前面描述相似,此處不再贅述。

defaultReadObject方法執行完成后,代碼流程回到PriorityQueue對象的readObject方法(圖3-6)中,讀取被transient修飾的Object數組queue(此前被賦值為兩個int型的數值1),這部分可以和PriorityQueue類的writeObject方法對照著看(圖3-9)。

然后代碼流程進入圖3-6中173行的heapify方法,PriorityQueue本質上是一個最小堆,通過siftDown方法進行次序的調整實現堆化,之前往PriorityQueue對象中插入兩個1,可以使隊列的SIZE滿足for循環的條件從而進入siftDown方法中。

繼續跟進siftDown方法,次序的調整必然涉及比較,在這兒此前精心構造的比較器就派上用場了,跟進siftDownUsingComparator方法,在圖3-11中699行調用了比較器的compare方法。


跟進compare方法,在比較前會先通過transformer的transform方法轉換一下對象。而此處的transformer正是我們此前構造的ChainedTransformer對象chain序列化成字節流后又反序列化所得(在遞歸調用readObject0方法時實現),如圖3-12所示。


繼續跟進到ChainedTransformer的transform方法中,此時iTransformers中有ConstantTransformer對象和InstantiateTransformer對象,此處代碼邏輯是將ConstantTransformer對象中transform方法的返回值作為參數傳入InstantiateTransformer對象的transform方法中。

ConstantTransformer對象中transform方法的返回iConstant變量,即com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter.class:

InstantiateTransformer對象中transform方法反射獲取構造方法后生成了TrAXFilter類的實例,通過newInstance方法進入了TrAXFilter類含參構造方法TrAXFilter(Templates templates)中,并將TemplatesImpl實例作為參數傳入,如圖3-15所示。

TrAXFilter(Templates templates)方法代碼如圖3-16所示,在64行調用了TemplatesImpl對象的newTransformer方法,newTransformer方法中又調用getTransletInstance方法(圖3-17中410行),惡意代碼的觸發便是在該方法中。


如圖3-18所示,getTransletInstance方法中第376行調用了defineTransletClasses方法后,380行會將_class數組中的某個類實例化:

跟進defineTransletClasses方法發現有如圖3-19所示這樣一段代碼:

其在for循環里遍歷_bytecodes數組并通過TransletClassLoader加載字節碼,其中會判斷_class[i]的父類是否為ABSTRACT_TRANSLET(”com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet”),這解釋了為什么_bytecodes中的StubTransletPayload類要繼承自AbstractTranslet類,_transletIndex變量初始化時為-1,若此處判斷條件為false,_transletIndex的值仍為-1,則程序執行流程會進入后面if (_transletIndex < 0)的代碼塊中拋出異常。構造StubTransletPayload類為AbstractTranslet類的子類即可把惡意類的索引值i賦值給_transletIndex。defineTransletClasses方法執行完成后,跳回到getTransletInstance方法中,將_class[_transletIndex](即StubTransletPayload類)實例化觸發我們之前通過javassist類庫插入的代碼塊,實現代碼執行(圖3-20)。

到這兒基本上整個Gadget的觸發流程就走完了。此處通過調用TemplatesImpl對象的newTransformer方法去間接的調用getTransletInstance方法實現代碼執行。除此之外,TemplatesImpl類中的getOutputProperties方法又調用了newTransformer,例如fastjson的反序列化中基于TemplatesImpl類的Gadget便是通過getOutputProperties方法去觸發代碼執行。 理論上只要構造特定的TemplatesImpl類對象,然后調用其getTransletInstance方法就可以實現代碼執行。為方便理解,我寫了一個簡單的Demo,通過反射正向構造了一個TemplatesImpl對象并調用其getTransletInstance方法來觸發代碼執行,代碼如下:

evil.java代碼如下:

關于類的加載中靜態代碼的執行
在demo中是通過插入靜態代碼塊的方式注入惡意代碼,我看到后面defineClass對類的加載時一度以為這樣的實現類似于fastjson中基于com.sun.org.apache.bcel.internal.util.ClassLoader類實現的POC(具體可參考文章DefineClass在Java反序列化當中的利用),在類加載的過程中實現的static代碼塊執行,但后來調試時發現static{}中插入的惡意代碼仍然是在類實例化(即調用newInstance())時觸發。 關于類加載的過程,在《深入理解Java虛擬機》中虛擬機類加載機制一節中有詳細的說明,類加載可分為加載、驗證、準備、解析和初始化這五個階段。其中我們關心的static代碼塊的執行是在初始化階段,初始化階段實際是執行類構造器<clinit>()的過程,<clinit>()是在Javac編譯過程中生成字節碼時被添加到語法樹中。
<clinit>()方法是編譯器自動收集類中的所有類變量的賦值動作和靜態語句塊中的語句合并產生。——《深入理解Java虛擬機》
書中還提到虛擬機規范嚴格規定了有且只有四種情況必須立即對類進行初始化:
-
遇到new、getstatic、putstatic、invokestatic這四條字節碼指令時,如果類還沒有進行過初始化,則需要先觸發其初始化。生成這四條指令最常見的Java代碼場景是:使用new關鍵字實例化對象時、讀取或設置一個類的靜態字段(static)時(被static修飾又被final修飾的,已在編譯期把結果放入常量池的靜態字段除外)、以及調用一個類的靜態方法時。
-
使用Java.lang.refect包的方法對類進行反射調用時,如果類還沒有進行過初始化,則需要先觸發其初始化。
-
當初始化一個類的時候,如果發現其父類還沒有進行初始化,則需要先觸發其父類的初始化。
-
當虛擬機啟動時,用戶需要指定一個要執行的主類,虛擬機會先執行該主類。
前面在通過TransletClassLoader中的defineClass方法加載類時僅將字節碼裝載到了JVM中,沒有執行類的初始化,而fastjson的Poc中通過Class.forName()加載類時,Class.forName()方法除了將對應的類裝載到JVM中,還會執行類構造器<clinit>()對類進行初始化,從而執行static代碼塊。Class.forName()代碼實現(JDK1.7)見圖4-1:


forName0()方法用native關鍵字修飾,說明這個方法是原生函數,非Java語言實現。可從forName()方法的注釋中看到第二個參數決定類是否會被初始化,在forName(String className)中默認為true。以上基本解釋了我在關于注入的靜態代碼觸發位置的疑惑。
總結
整個Gadget的調用棧見圖5-1:

反序列化時首先從ObjectInputStream類的readObject方法中進入到PriorityQueue類的readObject方法里,其readObject方法中會進行堆化,堆化時隊列中元素大于等于2時會進行堆排序,這時會調用自定義的比較器(TransformingComparator),TransformingComparator在比較次序時會將對象進行轉換。轉換時使用的transformer是基于ConstantTransformer對象和InstantiateTransformer對象構造的ChainedTransformer對象,ChainedTransformer對象在其轉換方法(transform())中會依次調用ConstantTransformer對象和InstantiateTransformer對象的transform方法,并將前一個對象transform方法的返回值作為參數傳入后一個對象的transform方法中,InstantiateTransformer對象中的transform方法會基于參數(這里即ConstantTransformer.transform()的返回值com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter)新建實例,則進入了TrAXFilter類的構造方法中,這里調用了TransformerImpl實例的newTransformer方法,又調用了getTransletInstance方法,加載_bytecodes中修改后的StubTransletPayload類字節碼并生成實例,從而觸發代碼執行。
參考
https://github.com/frohoff/ysoserial
https://stackoverflow.com/questions/39504847/why-does-class-not-invoke-the-static-block-in-a-class
https://www.freebuf.com/articles/others-articles/167932.html
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/786/
暫無評論