Author:Welkinhtml
隨着Java應用的推廣和普及,Java安全問題愈來愈被人們重視,縱觀近些年來的Java安全漏洞,反序列化漏洞佔了很大的比例。就影響程度來講,反序列化漏洞的整體影響也明顯高於其餘類別的漏洞。java
在反序列化漏洞的利用過程當中,攻擊者會構造一系列的調用鏈以完成其攻擊行爲。如何高效的生成符合條件且能夠穩定利用的攻擊Payload成爲了攻擊鏈條中的重要一環,當前已經有不少現成的工具幫助咱們完成Payload的生成工做。本文主要以Ysoserial工具爲例分析了基於org.apache.commons.collections4類庫的Gadget,其經過構造一個特殊的PriorityQueue對象,將其序列化成字節流後,在字節流反序列化的過程當中觸發代碼執行。git
文章主要分爲兩方面,其一是基於PriorityQueue類的序列化對象的構造,另外一方面是PriorityQueue對象在反序列化過程當中惡意代碼的觸發原理。下文將從這兩方面展開描述一些細節以及實際測試時的一些問題,總體的流程如圖1-1所示。github
Ysoserial是一個能夠產生反序列化漏洞利用payload的工具,其中包括了許多可用於攻擊反序列化漏洞的Gadget的實現。shell
ysoserial is a collection of utilities and property-oriented programming "gadget chains" discovered in common java libraries that can, under the right conditions, exploit Java applications performing unsafe deserialization of objects. The main driver program takes a user-specified command and wraps it in the user-specified gadget chain, then serializes these objects to stdout. When an application with the required gadgets on the classpath unsafely deserializes this data, the chain will automatically be invoked and cause the command to be executed on the application host.apache
更多關於Ysoserial的說明可參考:https://github.com/frohoff/ysoserialjson
首先,被序列化爲字節流的對象實際是一個特殊的PriorityQueue對象,本小節主要分析構造該對象的過程,即圖1-1的第一步。圖3-1爲ysoserial.payloads.CommonsCollections4中getObject方法的代碼,是用於構造該PriorityQueue對象的代碼:
api
上圖中須要注意的有以下兩點:數組
經過createTemplatesImpl方法生成templates對象是很是重要的一部分,由於這是實際承載咱們惡意代碼的對象,詳細說一下,跟進分析createTemplatesImpl方法,其代碼具體實現和關鍵點流程分別以下圖3-2和圖3-3所示:安全
首先生成TemplatesImpl實例,而後經過javassist類庫修改StubTransletPayload類字節碼,在其中插入執行命令的代碼(這裏是經過java.lang.Runtime.getRuntime().exec()方法執行命令,也能夠插入其餘利用代碼,如反彈shell等),而後將其父類設置爲abstTranslet類,最後將修改後的字節碼經過反射寫入到TemplatesImpl實例的_bytecodes變量中,這裏還同時寫入了Foo.class的字節碼。除此以外,爲了後續惡意代碼的觸發(如做者註釋中所寫:required to make TemplatesImpl happy),還要修改TemplatesImpl實例的_name和_tfactory變量,不然後面會在命令代碼執行前拋出異常。
StubTransletPayload類代碼實現如圖3-4所示:
StubTransletPayload類繼承自AbstractTranslet類並實現了Serializable接口,一般咱們構造一個惡意類可能會直接在static代碼塊或構造方法中寫入咱們想要執行的代碼,這一步在上面經過javassist類庫實現,關於StubTransletPayload類須要繼承AbstractTranslet類的緣由會在反序列化惡意代碼觸發時解釋。
以上即爲createTemplatesImpl方法的實現,其本質上是構造了一個特定結構的TemplatesImpl類實例,具體變量的值如圖3-5所示,
回到圖3-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對象。
5一、52行向隊列中插入兩個1,這裏是爲了後面堆化時觸發一次堆排序。
最終構造了一個用TransformingComparator對象做比較器的PriorityQueue對象,其內存中變量示意圖和抽象結構圖分別如圖3-6和圖3-7所示。
接下來將分析下這個對象序列化後的字節序列如何在反序列化的過程當中觸發代碼執行。
反序列化開始至觸發代碼執行的總體流程如圖4-1所示:
反序列化過程當中首先進入ObjectInputStream類的readObject方法中,而後進入readObject0方法中讀取字節流,其中會讀取tc標記,而後根據tc標記的類型進入不一樣的邏輯處理函數中,標記類型可見圖4-2,
反序列化的是PriorityQueue對象,這裏會進入TC_OBJECT的處理邏輯中,跟進到readOrdinaryObject方法裏,其具體代碼如圖4-3,在1769行讀取類描述信息,1780行經過類描述信息,初始化對象obj(即PriorityQueue對象),
在圖4-4中1793行判斷是否實現Externalizable接口,經過Externalizable接口能夠經過調用對象的readExternal方法實現自定義地徹底控制某一對象及其超類的流格式和內容,這裏代碼進入默認的readSerialData方法中。
在圖4-5中1882行判斷序列化對象是否有readObject方法,若是有則經過反射調用對象的readObject方法爲成員變量賦值,接下來就進入了PriorityQueue對象的readObject方法中。
圖4-6爲PriorityQueue對象的readObject方法,
圖4-7中在defaultReadObject方法中會調用defaultReadFields方法爲成員變量賦值,
defaultReadFields方法中1989行會遞歸調用readObject0方法爲對象的成員變量賦值直至完成,邏輯與前面描述類似,此處再也不贅述。
defaultReadObject方法執行完成後,代碼流程回到PriorityQueue對象的readObject方法(圖4-6)中,讀取被transient修飾的Object數組queue(此前被賦值爲兩個int型的數值1),這部分能夠和PriorityQueue類的writeObject方法對照着看(圖4-9)。
而後代碼流程進入圖4-6中173行的heapify方法,PriorityQueue本質上是一個最小堆,經過siftDown方法進行次序的調整實現堆化,以前往PriorityQueue對象中插入兩個1,可使隊列的SIZE知足for循環的條件從而進入siftDown方法中。
繼續跟進siftDown方法,次序的調整必然涉及比較,在這兒此前精心構造的比較器就派上用場了,跟進siftDownUsingComparator方法,在圖4-11中699行調用了比較器的compare方法。
跟進compare方法,在比較前會先經過transformer的transform方法轉換一下對象。而此處的transformer正是咱們此前構造的ChainedTransformer對象chain序列化成字節流後又反序列化所得(在遞歸調用readObject0方法時實現),如圖4-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實例做爲參數傳入,如圖4-15所示。
TrAXFilter(Templates templates)方法代碼如圖4-16所示,在64行調用了TemplatesImpl對象的newTransformer方法,newTransformer方法中又調用getTransletInstance方法(圖4-17中410行),惡意代碼的觸發即是在該方法中。
如圖4-18所示,getTransletInstance方法中第376行調用了defineTransletClasses方法後,380行會將_class數組中的某個類實例化,
跟進defineTransletClasses方法發現有如圖4-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類庫插入的代碼塊,實現代碼執行(圖4-20)。
到這兒基本上整個Gadget的觸發流程就走完了。此處經過調用TemplatesImpl對象的newTransformer方法去間接的調用getTransletInstance方法實現代碼執行。除此以外,TemplatesImpl類中的getOutputProperties方法又調用了newTransformer,例如fastjson的反序列化中基於TemplatesImpl類的Gadget即是經過getOutputProperties方法去觸發代碼執行。
理論上只要構造特定的TemplatesImpl類對象,而後調用其getTransletInstance方法就能夠實現代碼執行。爲方便理解,我寫了一個簡單的Demo,經過反射正向構造了一個TemplatesImpl對象並調用其getTransletInstance方法來觸發代碼執行,代碼以下:
public static void main(String[] args) throws Exception{ String evilClassPath = "evil.class"; ByteArrayOutputStream ba = new ByteArrayOutputStream(); IOUtils.copy(new FileInputStream(new File(evilClassPath)),ba); byte[][] evilBytes = {ba.toByteArray()}; Class clazz = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl"); Class[] paramsTypes = {byte[][].class,String.class,Properties.class,int.class,TransformerFactoryImpl.class}; Constructor constructor = clazz.getDeclaredConstructor(paramsTypes); Method method = clazz.getDeclaredMethod("getTransletInstance"); constructor.setAccessible(true); method.setAccessible(true); TemplatesImpl templates = (TemplatesImpl)constructor.newInstance(evilBytes,"Welkin",null,0,new TransformerFactoryImpl()); method.invoke(templates); }
evil.java代碼以下:
public class evil extends AbstractTranslet { static { try { Runtime.getRuntime().exec("calc"); } catch (Exception e){ e.printStackTrace(); } } @Override public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) { } @Override public void transform(DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] handlers) throws TransletException { } }
在demo中是經過插入靜態代碼塊的方式注入惡意代碼,我看到後面defineClass對類的加載時一度覺得這樣的實現相似於fastjson中基於com.sun.org.apache.bcel.internal.util.ClassLoader類實現的POC(具體可參考文章DefineClass在Java反序列化當中的利用),在類加載的過程當中實現的static代碼塊執行,但後來調試時發現static{}中插入的惡意代碼仍然是在類實例化(即調用newInstance())時觸發。
關於類加載的過程,在《深刻理解Java虛擬機》中虛擬機類加載機制一節中有詳細的說明,類加載可分爲加載、驗證、準備、解析和初始化這五個階段。其中咱們關心的static代碼塊的執行是在初始化階段,初始化階段實際是執行類構造器<clinit>()的過程,<clinit>()是在Javac編譯過程當中生成字節碼時被添加到語法樹中。
<clinit>()方法是編譯器自動收集類中的全部類變量的賦值動做和靜態語句塊中的語句合併產生。——《深刻理解Java虛擬機》
書中還提到虛擬機規範嚴格規定了有且只有四種狀況必須當即對類進行初始化:
前面在經過TransletClassLoader中的defineClass方法加載類時僅將字節碼裝載到了JVM中,沒有執行類的初始化,而fastjson的Poc中經過Class.forName()加載類時,Class.forName()方法除了將對應的類裝載到JVM中,還會執行類構造器<clinit>()對類進行初始化,從而執行static代碼塊。Class.forName()代碼實現(JDK1.7)見圖5-1:
forName0()方法用native關鍵字修飾,說明這個方法是原生函數,非Java語言實現。可從forName()方法的註釋中看到第二個參數決定類是否會被初始化,在forName(String className)中默認爲true。以上基本解釋了我在關於注入的靜態代碼觸發位置的疑惑。
整個Gadget的調用棧見圖6-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