0x00、前言
针对CC链的分析,主要还是以逆向思维为主(从结果分析过程),从中尽可能学习出问题的地方在哪,哪里调用的这个带问题的地方,一步步思考。
这次的学习顺序是先PriorityQueue优先级队列——>CC2链学习分析——>javassist字节码增强类——>ClassLoader#defineClass定义类——>TemplatesImpl模板转换——>cc2-yso链学习与分析
0x01、Apache Commons Collections描述
引用CC1链分析中的描述
CC链即Commons Collections利用链,主要针对Commons Collections组件发现的利用链。
Apache Commons是Apache软件基金会的项目。Commons的目的是提供可重用的、开源的Java代码。
Apache Commons提供了很多工具类库,他们几乎不依赖其他第三方的类库,接口稳定,集成简单,可以大大提高编码效率和代码质量。
Apache Commons Collections 是对 java.util.Collection 的扩展。
Commons Collections包为Java标准的Collections API提供了相当好的补充。在此基础上对其常用的数据结构操作进行了很好的封装、抽象和补充。让我们在开发应用程序的过程中,既保证了性能,同时也能大大简化代码。
目前 Collections 包有两个 commons-collections 和commons-collections4,commons-collections 最新版本是3.2.2,3系列版本也是只有3.2.2是安全的,不支持泛型,目前官方已不在维护。collections4 目前最新版本是4.4,其中4.0是存在漏洞,最低要求 Java8 以上。相对于 collections 来说完全支持 Java8 的特性并且支持泛型,该版本无法兼容旧有版本,于是为了避免冲突改名为 collections4。推荐直接使用该版本。(注:两个版本可以共存,使用时需要注意)
0x02、环境准备
java版本:jdk8u66(要求java8以上)
Commons Collections:4.0(漏洞版本)(在3版本中TransformingComparator
类未实现序列化接口所以不存在该利用链,4版本才实现)
javassist:3.20.0-GA
maven项目pom.xml文件中添加依赖
<dependency> |
在idea访问Commons Collections
组件的文件时候点击上方的下载源代码就可以看到对应文件的.java文件了
0x03、利用链基础前提
javassist字节码增强类
Java 字节码以二进制的形式存储在 class 文件中,每一个 class 文件包含一个 Java 类或接口。Javaassist 就是一个用来处理 Java 字节码的类库。
引用官网的描述:
Javassist(Java编程助手)使Java字节码操作变得简单。它是一个用于在Java中编辑字节码的类库;它使Java程序能够在运行时定义一个新类,并在JVM加载时修改类文件。与其他类似的字节码编辑器不同,Javassist提供了两级API:源代码级和字节码级。如果用户使用源代码级API,他们可以在不知道Java字节码规范的情况下编辑类文件。整个API仅使用Java语言的词汇表设计。您甚至可以以源文本的形式指定插入的字节码;Javassist实时编译。另一方面,字节码级API允许用户像其他编辑器一样直接编辑类文件。
由于java
运行通常由java
文件编译成class
文件供jvm
运行,更改代码内容需要重新编写java
文件再编译成class
文件运行。Javassist
作用就是动态修改.class
文件内容,且不需要知道jvm
相关指令调用,javassist
的引用能更简单快速的修改class
文件的内容。
实现的效果有点类似反射的调用方式,不过一个是更改字节文件,反射是调用类,目的不同。
涉及的类
ClassPool
ClassPool:
基于哈希表(Hashtable)实现的CtClass对象容器,其中键是类名称, 值是表示该类的CtClass对象,同HashMap实现的Map接口,但不同于哈希表(Hashtable)的键名不能为null。
常用方法:
常用方法 | 描述 |
---|---|
getDefault() | 返回默认的类池(默认的类池搜索系统搜索路径,通常包括平台库、扩展库以及由-classpath选项或CLASSPATH环境变量指定的搜索路径) |
insertClassPath(java.lang.String pathname) | 在搜索路径的开头插入目录或jar(或zip)文件 |
insertClassPath(ClassPath cp) | 在搜索路径的开头插入类对象,当用户系统存在多个类加载器,默认加载getDefault()搜索不到加载类可使用该方法添加路径 |
getClassLoader() | 获得类加载器 |
get(java.lang.String classname) | 从源中读取类文件,并返回对CtClass 表示该类文件的对象的引用 |
getOrNull(java.lang.String classname) | 从源中读取类文件,并返回对CtClass 表示该类文件的对象的引用,未找到该文件返回null,不抛出异常 |
appendClassPath(ClassPath cp) | 将ClassPath对象附加到搜索路径的末尾 |
makeClass(java.lang.String classname) | 创建一个新的public类 |
实现方式:
public static void main(String[] args) throws Exception { |
CtClass
CtClass表示类, 一个CtClass(编译时类)对象可以处理一个class文件, 这些CtClass对象可以从ClassPool获得。
常用方法:
常用方法 | 描述 |
---|---|
setSuperclass(CtClass clazz) | 添加父类 |
setInterfaces | 添加父类接口 |
toClass(java.lang.invoke.MethodHandles.Lookup lookup) | 将此类转换为java.lang.Class对象 |
toBytecode() | 将该类转换为类文件,对象类型为byte[] |
writeFile() | 将由此CtClass 对象表示的类文件写入当前目录 |
writeFile(java.lang.String directoryName) | 将由此CtClass 对象表示的类文件写入本地磁盘 |
makeClassInitializer() | 制作一个空的类初始化程序(静态构造函数),对象类型为CtConstructor |
detach | 将CtClass对象从ClassPool池中删除 |
freeze | 冻结一个类,使其变为不可修改状态 |
isfreeze | 判断该类是否存于冻结状态 |
prune | 删除类不必要的属性,减少内存占用 |
deforst | 解冻一个类,使其变为可修改状态 |
addField | 添加字段 |
addMethod | 添加方法 |
addConstructor | 添加构造器 |
addInterface | 添加接口 |
实现方式:
public static void main(String[] args) throws Exception { |
CtMethods
表示类中的方法
常用方法 | 描述 |
---|---|
insertBefore | 在方法起始位置插入代码 |
insterAfter | 在方法的所有 return 语句前插入代码以确保语句能够被执行,除非遇到exception |
insertAt | 在指定位置插入代码 |
setBody | 将方法的内容设置为要写入的代码,当方法被 abstract修饰时,该修饰符被移除 |
make | 创建一个方法 |
addParameter | 添加参数 |
setName | 设置方法名 |
注:插入的代码必须是完整的代码语句,包括分号结束语。
实现方法:
public static void main(String[] args) throws Exception { |
CtConstructor
表示类中的构造函数
实现方法:
public static void main(String[] args) throws Exception { |
生成字节码toBytecode()
生成字节码:CtClass.toBytecode()
//生成字节码 |
生成对象toClass()
反射调用对象:CtClass.toClass()
//通过反射调用对象 |
简单实现demo
实例类:
public class People { |
实现类:
public static void main(String[] args) throws Exception { |
实现更改字节码效果:
触发恶意代码效果demo:
通过向构造函数插入执行代码,通过反射调用CtClass对象并转换为反射类对象,进行实例化的时候调用构造函数触发恶意代码。
public static void main(String[] args) throws Exception { |
PriorityQueue优先级队列
PriorityQueue类在Java1.5中引入。PriorityQueue是基于优先堆的一个无界队列,这个优先队列中的元素可以默认自然排序或者通过提供的Comparator(比较器)在队列实例化的时排序。要求使用Java Comparable和Comparator接口给对象排序,并且在排序时会按照优先级处理其中的元素。
顾名思义,它是队列的一种实现方式,但不同于普通队列Queue(先进先出),它可通过比较器Comparator实现数据之间谁排在前面谁排在后面(上沉该元素)。
PriorityQueue队列常用方法:
- add():添加数组元素,添加失败会抛出异常。
- offer():添加数组元素,添加失败会返回false。
- poll():取出队顶数组元素,并删除该元素,失败会抛出异常。
- peek():查询队顶数组元素,但不删除该元素。
- remove(): 取出队顶数组元素,并删除该元素,失败会返回null。
PriorityQueue队列实现用法:
- PriorityQueue
queue=new PriorityQueue<>(); //默认优先级队列,规则从小到大 - PriorityQueue
queue=new PriorityQueue<>( (a,b)->(b-a)); //设置比较器,从大到小排列
实现方法:
public static void main(String[] args) throws Exception { |
自定义使用方法:
- 类实现了Comparable接口,定义了比较方法,直接使用即可PriorityQueue<类名> queue=new PriorityQueue<>();
- 当传递对象元素未实现Comparable接口,可自定义新Comparable
PriorityQueue<People> pq=new PriorityQueue<>(new Comparator<People>() {
public int compare(People o1, People o2) {
////判断数值从大到小,如果第一个元素数值比第二个大,返回1标识顺序正确,否则返回-1
return o1.getAge()-o2.getAge()>0?-1:1;
}
});
其中进行判断的比较器参数o1,o2,其中o1表示新插入的元素,o2表示被比较的元素也就是插入的前面一个元素。其中最后return 1表示当前比较的两个元素顺序正确,-1表示顺序不正确,不正确后将新插入的元素进行上沉操作(也就是向前排),然后循环比较上沉后的前面一个元素,继续判断直到顺序正确。
简单实现效果:
public static void main(String[] args) throws Exception { |
ClassLoader#defineClass
ClassLoader为类加载器,可以将字节码文件(.class文件),通过loadClass函数加载类名,返回一个Class对象,同时ClassLoader类下面存在defineClass方法,可以将byte[]字节数组信息还原成一个Class对象,在学javassist中,了解到javassist可以动态生成字节码文件,包括了一些恶意代码文件,可进而通过ClassLoader类加载器将这些恶意的字节码文件转化为java类进行调用,达到执行恶意代码的目的
其中类加载阶段:
ClassLoader#loadClass(类加载,从类缓或父加载器等位置寻找类) |
由于ClassLoader#defineClass方法为protect修饰,因此可通过反射进行调用
简单实现
public static void main(String[] args) throws Exception { |
TemplatesImpl
TemplatesImpl这个类简述功能就是对xsl格式文件生成的字节码转换成XSLTC模板对象,期间会处理字节码,因此重写了defineClass方法,具体描述可查看TemplatesImpl了解
重写了defineClass
方法,并且没有定义域,可以在其他类进行调用使用,而ClassLoader#defineClass
定义域是受保护的,在很多情况中调用受限,因此这也是TransletClassLoader#defineClass
作为很多序列化漏洞入口,而不是使用ClassLoader#defineClass
但该defineClass()
的调用并不会实例化,需要通过newInstance()
进行实例化。
依次看调用情况
defineTransletClasses()
defineClass
在defineTransletClasses
方法中被调用
其中需要的满足条件:
_bytecodes
不能为空,为空会直接抛出异常if (_bytecodes == null) {
ErrorMsg err = new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR);
throw new TransformerConfigurationException(err.toString());
}_tfactory
需要实例化
创建的TransletClassLoader
(Translet类的加载器)对象,其中_tfactory.getExternalExtensionsMap()
中的_tfactory
对象为TransformerFactoryImpl
类对象,等同于调用TransformerFactoryImpl.getExternalExtensionsMap()
方法,但其中_tfactory
对象初始赋值为null,直接执行会报错,因此需要将_tfactory
进行实例化,才能调用TransformerFactoryImpl.getExternalExtensionsMap()
方法。TransletClassLoader loader = (TransletClassLoader)
AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
return new TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap());
}
});两种实现方法都可以实例化,第一种通过
TransformerFactoryImpl()
构造方法实现实例化对象,第二种通过反射直接实现实例化对象。setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
setFieldValue(templates, "_tfactory", TransformerFactoryImpl.class.newInstance());父类需要为ABSTRACT_TRANSLET
类
通过for循环对字节文件类进行循环定义并赋值给class数组,其中会判断当前获取的字节类的父类是否为ABSTRACT_TRANSLET
类,是的话会讲该类序号赋值给_transletIndex
,否则不是的话会抛出异常(表意为只有存在父类为ABSTRACT_TRANSLET
类的translet
类才能被实例化),从而在getTransletInstance
类中的AbstractTranslet
实例化步骤将父类为ABSTRACT_TRANSLET
的类进行实例化for (int i = 0; i < classCount; i++) {
_class[i] = loader.defineClass(_bytecodes[i]);
final Class superClass = _class[i].getSuperclass();
// Check if this is the main class
if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
_transletIndex = i;
}_bytecodes
字节码需要设置为byte[][]数组,_bytecodes
变量声明为byte[][]类型,如果直接通过javassist toBytecode()生成byte[]数组运行会报错。
因此需要将一维数组转化为二维数组。byte[] bytecode=ct.toBytecode();
byte[][] bytecodes=new byte[][]{bytecode};
接着查看defineTransletClasses
方法的上层调用情况
其中有三处对该方法进行了调用,其中只用getTransletInstance
方法有上层调用,其他两个方法没有被其他方法进行调用。
getTransletInstance()
其中需要的满足条件:
_name
不能为空,为空会直接返回null
if (_name == null) return null;
_class
必须为空,才能调用defineTransletClasses
方法if (_class == null) defineTransletClasses();
最后通过创建AbstractTranslet
对象将class文件类进行实例化
AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance(); |
接着查看getTransletInstance
方法的上层调用情况
newTransformer()
方法进行了调用
newTransformer()
在调用构造函数的时候调用了getTransletInstance
方法,返回Translet类的实例,其中没有需要的满足条件。
接着查看newTransformer()
方法的上层调用情况
其中有5处显示进行了调用,但只有getOutputProperties
方法调用的本类的newTransformer()
方法,其他四种都是调用的其他类重写的newTransformer()
方法。
getOutputProperties()
该方法直接执行了newTransforme
方法,没有其他条件限制,查询getOutputProperties
的上层调用,没有在本类发现其调用,因此最后的执行方法就在getOutputProperties
完整链
到newTransformer
方法执行就能达到触发了,他上层getOutputProperties
方法也进行了调用,也可以算进去当作延伸出来的链。
getOutputProperties() |
实现demo
public static void main(String[] args) throws Exception { |
其中的空指针的报错原因是在赋值传递变量时没有指向对象,然后在调用时直接通过该变量去调用对应的方法导致报错,但不影响执行效果。
0x04、CC2利用链分析
逆推调用链
分析CC1链的时候知道目的是调用InvokerTransformer.transform()
方法,CC2链的后半段利用链跟CC1链相同,那就从不同的地方接着分析。
在搜寻InvokerTransformer.transform()
方法的时候,CC2链中TransformingComparator.compare()
方法对其进行了调用。
但该方法在cc1链中也调用了为啥不行,因为在Commons Collections4
版本中TransformingComparator
类实现了序列化接口,而CC1链中依赖Commons Collections3
版本,3版本中TransformingComparator
类未实现序列化接口,因此入口不成立。
Commons Collections4
版本中TransformingComparator
类实现了序列化接口。
其中由构造函数传入transformer
对象可控
简单实现demo:
public static void main(String[] args) throws Exception { |
接下来看在哪调用了compare()
方法
有很多类都进行了调用,其中cc2链中的调用为PriorityQueue
类
PriorityQueue
类中siftUpUsingComparator
、siftDownUsingComparator
两个方法都进行了调用,再分别查看这两个方法在哪进行了调用。
siftUpUsingComparator:
先是在同类的siftUp
方法中进行了调用
offer
、removeAt
这两个方法调用了
追溯调用offer
、removeAt
者两个方法,发现这两个方法被调用的情况有很多,直观上也没有找到对应的入口,就先看另外一个方法
siftDownUsingComparator:siftDownUsingComparator
方法在siftDown
方法中被调用
继续向上,siftDown
方法有三个方法进行了调用,其中heapify
方法在反序列化readObject
方法中调用
正向分析调用链
找到了反序列化入口,开始分析下调用情况
通过注释也不难明白,通过for循环读取序列化数据存在队列中,最后进行堆排序heapify()
堆排列中通过循环数组长度(size),获取每个元素并执行siftDown
方法,其中的长度验证可以调试查看是否为数组长度
为数组长度接下来进行堆排序heapify()
private void heapify() { |
其中来理解size >>> 1
, >>>
为无符号右移,将size的值转换成二进制并将二进制数值整体向右移动>>>
后面数字的位数
比如这里size为3,二进制为0000…0000 0011 (整数类型有32位的长度,为了方便省略中间重复的0),>>> 1
无符号右移1位
对应的二进制为0000…0000 0001,即10进制为1。此时size大小为1。
所以当size小于2时,for循环中i的值就为0-1=-1
,直接跳出循环,不会执行siftDown
方法,设计意义也是为了有2个及以上的队列元素才进行排序操作。
因此满足此条件,必须给queue队列添加2个及以上元素,才能执行siftDown
方法。
接下来到siftDown
方法
private void siftDown(int k, E x) { |
首先会判断是否存在comparator
比较器,比较器存在就调用siftDownUsingComparator
方法,不存在比较器就调用siftDownComparable
方法,按逆推调用链的话就必须存在比较器,才能去调用siftDownUsingComparator
方法
最后在siftDownUsingComparator
方法中调用比较器的compare
方法
其中针对PriorityQueue
类中的各个方法源码含义
可查看相关方法注解:https://www.cnblogs.com/freeorange/p/11405227.html
完整利用链
在cc1后半段基础上补充cc2的利用链,就形成完整利用链
ObjectInputStream.readObject() |
POC构造
利用链后半段用的cc1的后半段,可直接拿过来
Transformer[] transformers = new Transformer[]{ |
然后创建比较器,为了调用TransformingComparator
的comare
方法
TransformingComparator cp=new TransformingComparator(transformerChain); |
比较器创建好了,再创建PriorityQueue队列,引入上面的TransformingComparator
比较器,这里有两种利用方式
//方法一:直接new PriorityQueue对象时候添加构造器 |
然后给PriorityQueue
队列添加元素,分析过程的时候提到队列数组size大小必须大于等于2,所以这里添加元素最少要添加2个,才能触发利用链的方法
pq.add(1); |
最后序列化PriorityQueue
对象,再反序列化触发漏洞。
最后POC:
public static void main(String[] args) throws Exception { |
执行实现
运行报错原因:
这里提示有报错,原因是添加的元素无法通过指定的比较器进行比较,从而抛出的异常(添加的元素为数字,比较器为ChainedTransformer对象,两者没有关联关系,自然无法通过该比较器对数值进行比较),但利用链能全部调用,达到执行的效果。
调试报错原因:
在调试过程中发现,第一种方式无法进行反序列化步骤的调试,调试会直接抛出异常,因为在声明PriorityQueue对象时先传递入比较器,在添加元素时,会自动引入比较器进行比较,因为无法比较,所以编译文件时直接抛出异常,无法进行调试。
而第二种是先声明PriorityQueue对象,添加元素 再通过反射去调用设置比较器,因此可以调试到反序列化的步骤,到最后再进行比较的时候出错再抛出异常。
(引用反射的知识,动态调用在用到该反射的时候出错才会抛出异常,而普通用法会在编译的时候会直接调用出错就抛出异常)
0x05、CC2-yso利用链分析
分析简述
在yso利用链中,并没有使用上文cc2链分析的方法,加入了javassist
和TemplatesImpl
,通过javassist
生成恶意字节码,再通过TemplatesImpl
加载字节码返回类对象并实例化,通过上文基础知识对TemplatesImpl的了解,重点就在找到一个方法可以调用getOutputProperties()
或者newTransformer()
方法,去执行恶意代码。
到这也只有正向分析poc来查找调用关系
yso链的主要利用就是通过PriorityQueue优先级队列设置比较器,将InvokerTransformer(String methodName)方法作为比较器传递入PriorityQueue队列,其中在InvokerTransformer(String methodName)构造函数中传递newTransformer()方法作为参数。
构造POC
主体部分,javassist生成字节码,然后TemplatesImpl
填充对应满足条件,可以直接先把上文中TemplatesImpl
基础知识的实现demo搬过来
public static void main(String[] args) throws Exception { |
下一步,主体构造有了,但是需要有入口能调用TemplatesImpl
对象的newTransformer()
方法
通过InvokerTransformer(String methodName)
方法调用newTransformer()
方法或者getOutputProperties()
方法都行
//通过反射调用InvokerTransformer的带参构造方法,参数为执行的方法名,因此传递类型为String.class |
getOutputProperties()
方法名传递进invokerTransformer
对象了,接下来就是跟CC2链分析的调用情况一样,设置比较器,将invokerTransformer
作为比较器赋值给TransformingComparator
对象
//设置比较器,目的调用compare方法,再通过,执行getOutputProperties方法 |
比较器有了,现在创建PriorityQueue队列,通过反射设置size大小以及比较器,由CC2分析的时候知道调用compare方法是关键,因为跟上文的cc2调用情况不一样,上文调用了ChainedTransformer
类把恶意代码执行串起来了,因此队列add进去的参数无所谓
但这个cc2-yso调用是先设置InvokerTransformer(String methodName)
构造函数传递入getOutputProperties
方法,最后再通过compare传递入TemplatesImpl对象来调用,相当于执行InvokerTransformer.transform(TemplatesImpl)
,最后执行效果为TemplatesImpl.getOutputProperties()
达到目的
//设置比较器,目的调用compare方法去执行invokerTransformer.transform(),执行getOutputProperties方法 |
结合以上得到最后的poc:
public static void main(String[] args) throws Exception { |
实现demo:
其中报错跟上文cc2分析和TemplatesImpl
类学习的报错一样就不再赘述了
0x06、总结
总的来说cc2-yso链比普通链复杂了很多,其中也学习了javassist生成字节码,再到TemplatesImpl去读取字节码实例化,最后结合优先级队列实现命令执行。
绕来绕去这中间确实花了不少时间,有些调用细节还是需要多看一下才能发现精髓(也确实费时间),但对调用的很多类都还没有从jdk源码上真正明白他们每个类的每个方法是实现怎样的作用,只从利用链角度去了解了调用到的链的类的相关原理,后面尽量多去了解那些类的原理。
0x07、参考链接
https://www.cnblogs.com/nice0e3/p/13811335.html
https://www.cnblogs.com/hlkawa/p/15383289.html
https://www.jianshu.com/p/cb591a12f50c
https://paper.seebug.org/1242/#commonscollections-2
https://www.anquanke.com/post/id/247044?from=timeline#h2-0