Java CC2链复现与分析

复现环境

1
2
3
4
Mac OS Big Sur
JDK-7u6
commons-collections4-4.0
tomcat8

Ysoserial环境和Web Server环境配置和这里的配置是一样的。

CC2复现

Payload生成

1

Payload测试

2

复现成功

前置知识

Javassist

Javassist是用来处理java字节码的类库, java字节码一般存放在后缀名称为class的二进制文件中。每个二进制文件都包含一个java类或者是java接口。对于Javassist有很多知识点,这里我们仅仅说几个payload中用到的一些用法。

1
2
3
4
5
6
7
8
9
10
11
ClassPool:一个基于哈希表实现的CtClass对象容器,其中键名是类名称,值是表示该类的CtClass对象
常用方法如下
getDefault():单例获取ClassPool,主要用来修改字节码,里面存储基于二进制文件构建的CtClass对象
get():根据名称获取CtClass对象
makeClassInitializer:在当前类中创建一个静态代码块
insertBefore:在静态代码块的开头插入源代码
CtClass:一个CtClass对象可以处理一个class文件,这些CtClass对象可以从ClassPool的一些方法获得。
常用方法如下
writeFile:将生成的类写入文件
toClass:拿到生成的类
newInstance:获取实例对象

举一个例子

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
package javatest;
import javassist.*;

import java.io.IOException;

public class javas {
public static void main(String[] args) throws NotFoundException, CannotCompileException, IOException, InstantiationException, IllegalAccessException {
//获取ClassPool实例,主要用来修改字节码,里面存储基于二进制文件构建的CtClass对象
ClassPool pool = ClassPool.getDefault();
//通过get方法,获取CtClass对象,将获取到的CtClass对象赋值给cc变量
//ClassPool中有一张保存CtClass信息的HashTable,在该HashTable中Key为类名,value为类对应的CtClass对象
CtClass cc = pool.get(javas.class.getName());
//有了CtClass实例对象,就可以处理类文件,编辑或者修改类
//因为添加的内容都是完整的java源代码,因此引号要进行转义处理
String cmd = "java.lang.Runtime.getRuntime().exec(\"open -a calculator\");";
//makeClassInitializer:在当前类(javas)中创建一个静态代码块
//insertBefore:在静态代码块的开头插入源代码
cc.makeClassInitializer().insertBefore(cmd);
String randomClassName = "Elssm" + System.nanoTime();
//setName:设置类名,使用System.nanoTime为了不让类名重复
cc.setName(randomClassName);

//writeFile:将生成的类写入文件
cc.writeFile("/Users/caoyifan/IdeaProjects/WebTest/src/javatest/");
}
}

上述代码会在指定文件下生成一个新的字节码文件。我们发现,动态生成的类在原有类的基础上添加了静态代码块,如果我们加载这个类的话,静态代码块中的内容就会被执行。

3

现在我们通过toClass拿到生成的类,并通过newInstance获取实例对象

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
package javatest;
import javassist.*;

import java.io.IOException;

public class javas {
public static void main(String[] args) throws NotFoundException, CannotCompileException, IOException, InstantiationException, IllegalAccessException {
//获取ClassPool实例,主要用来修改字节码,里面存储基于二进制文件构建的CtClass对象
ClassPool pool = ClassPool.getDefault();
//通过get方法,获取CtClass对象,将获取到的CtClass对象赋值给cc变量
//ClassPool中有一张保存CtClass信息的HashTable,在该HashTable中Key为类名,value为类对应的CtClass对象
CtClass cc = pool.get(javas.class.getName());
//有了CtClass实例对象,就可以处理类文件,编辑或者修改类
//因为添加的内容都是完整的java源代码,因此引号要进行转义处理
String cmd = "java.lang.Runtime.getRuntime().exec(\"open -a calculator\");";
//makeClassInitializer:在当前类(javas)中创建一个静态代码块
//insertBefore:在静态代码块的开头插入源代码
cc.makeClassInitializer().insertBefore(cmd);
String randomClassName = "Elssm" + System.nanoTime();
//setName:设置类名,使用System.nanoTime为了不让类名重复
cc.setName(randomClassName);
//toClass:拿到生成的类
//newInstance:获取实例对象
cc.toClass().newInstance();
}
}

成功弹出计算器

4

PriorityQueue

PriorityQueue实际上是一个堆(不指定Comparator时默认为最小堆),队列既可以根据元素的自然顺序来排序,也可以根据 Comparator来设置排序规则。队列的头是按指定排序方式的最小元素。如果多个元素都是最小值,则头是其中一个元素。新建对象的时候可以指定一个初始容量,其容量会自动增加。

1
2
3
4
5
6
7
PriorityQueue优先级队列的函数定义
add() : 在优先级队列的队尾插入元素 , 插入失败则抛出异常.
offer() : 在优先级队列的队尾插入元素 , 插入失败则返回 null.
element() : 获取但不删除优先级队列的队首元素 , 获取失败时抛出异常.
peek() : 获取但不删除优先级队列的队首元素 , 获取失败时返回 null.
remove : 获取且删除优先级队列的队首元素 , 获取失败时抛出异常.
poll() : 获取且删除优先级队列的队首元素 , 获取失败时返回 null.

Ysoserial Payload生成分析

createTemplatesImpl

在生成CC2 Payload的时候,首先执行的是Gadgets.createTemplatesImpl(command)

5

createTemplatesImpl方法中首先会通过getProperty方法获取系统属性properXalan的值,properXalan的默认属性是false,通过调试发现走的是下面的return条件。

6

跟进发现,首先获取了TemplatesImpl的实例对象。之后通过Javassist动态修改StubTransletPayload类。

7

接着通过get方法获取abstTranslet类,并通过setSuperclass将该类作为新建类的父类,因为在前面我们获取到了StubTransletPayload类,而StubTransletPayload是继承abstTranslet类的

8

9

通过上述Javassist操作,就可以将我们写的恶意代码注入到新的字节码文件中中的静态代码块部分

之后通过CtClass.toBytecode方法获取到恶意类的字节码,并通过java反射机制将字节码填充到TemplatesImpl实例对象的_bytecodes数组中。

10

之后填充了_name_tfactory字段,并将TemplatesImpl实例对象返回

11

InvokerTransformer

返回TemplatesImpl实例对象之后,接着获取了InvokerTransformer实例对象,并做了一些初始化。

12

PriorityQueue

接着创建一个优先级队列,指定队列的初始容量和比较器。然后填充了两个1来占位初始化。

13

在优先级队列里,通过TransformingComparator(transformer)获取构造器实例对象,此时的transformer就是上面InvokerTransformer类型的transformer实例对象。

14

接着优先级队列每次比较的时候,都会调用比较器的compare方法,此时服务端就会调用TransformingComparator.compare方法,进而执行this.transformer.transform方法,这个时候就会执行InvokerTransformer.transform方法

15

setFieldValue

继续回到CC2 Payload文件中,setFieldValue方法通过反射将iMethodName修改为newTransformer

16

getFieldValue

接下来获取到优先级队列的实例对象,并修改了其字段值,将我们构造的恶意类注入

17

修改之后,优先级队列会调用比较器的compare方法去比较templates和1的值,此时会调用执行InvokerTransformer.transform(templates)方法。此时由于templatesTemplatesImpl类型的,因此实际最后会执行TemplatesImpl.newTransformer方法。

最后Ysoserial返回的优先级队列实例对象如下

18

CC2 Payload利用原理

通过Ysoserial分析可以看到Payload返回的是一个PriorityQueue对象。在对Web Server进行调试的时候,我们直接将断点打在PriorityQueuereadObject方法处。

PriorityQueue路径为rt.jar.java.util.PriorityQueue

PriorityQueue.readObject

19

readObject方法中,首先通过defaultReadObject方法反序列化数据流。之后通过readInt方法读取优先级队列的长度,接着循环读取数组queue中的内容

20

PriorityQueue.heapify

之后调用heapify方法,将无序数组还原成优先级队列。在该方法中会循环找最后一个非叶子结点,然后倒序调用siftDown方法

21

PriorityQueue.siftDown

siftDown方法中,会调用siftDownUsingComparator方法

22

PriorityQueue.siftDownUsingComparator

该方法主要用于形成最小堆

23

cc4.TransformingComparator

该方法会获取需要比较的变量。从而执行this.transformer.transform,因为我们已经将this.transformer指向了InvokerTransformer实例对象。因此会执行InvokerTransformer.transform方法,而transform中的参数就是我们传入的恶意类TemplatesImpl

24

InvokerTransformer.transform

Ysoserial分析payload生成的时候我们将iMethodName的值改为了newTransformer,因此这里会反射调用TemplatesImpl.newTransformer方法

25

TemplatesImpl.newTransformer

TemplatesImpl路径为rt.jar.com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl

在构建TemplatesImpl实例对象时,会调用getTransletInstance方法。

26

TemplatesImpl.getTransletInstance

getTransletInstance方法中,首先会判断_name是否为空,如果为空,直接return。接着会判断_class并进入defineTransletClasses方法中

27

TemplatesImpl.defineTransletClasses

该方法会对_bytecodes字段进行解析,最后将_transletIndex的值赋为0

28

newInstance

此时的_class[_transletIndex]即为_class[0]也就是我们通过啊Ysoserial构造的恶意类,调用newInstance方法后恶意类会被加载然后被实例化,最终执行命令。

29

完整的利用链如下图所示

30