Java反序列化Commons-Collections——CC3
Java反序列化Commons-Collections——CC3
0x01 前言
CC3与之前CC1和CC6存在较大不同,主要体现在了命令执行的方式上。在CC1和CC6中都是通过调用InvokerTransformer#transform
方法,通过Runtime.getRuntime().exec()
来执行命令的,而CC3则是通过类加载,加载自己构建的.class
文件来实现,可以简单认为CC1和CC6是命令执行,而CC3是代码执行。
在研究CC3之前,这里先补充一下Java类加载的相关知识。
0x02 Java动态类加载
一、类加载器ClassLoader
:
一切的Java类都必须记过JVM记载后才能运行,ClassLoader
的作用就是Java类文件的加载。在JVM类加载器中最顶层的是BootStrapClassLoader(引导类加载器)
、ExtensionClassLoader(扩展类加载器)
、AppClassLoader(系统类加载器)
,AppClassLoader
是默认的类加载器,如果在类加载时不指定类加载器,则默认会使用AppClassLoader
,ClassLoader#getSystemClassLoader
返回的系统类加载器也是AppClassLoader
。
二、ClassLoader
加载.class
文件
这里参考P神的Java安全漫谈系统中谈到的URLClassLoader
,URLClassLoader
实际上是AppClassLoader
的父类,所以这里分析下URLClassLoader
的工作流程就是在分析Java类加载的实际工作流程。
正常情况下,Java会根据配置项sun.boot.class.path
和java.class.path
中列举到的基础路径来寻找.class
文件来加载,这个基础路径分为三种情况:
- URL不以
/
结尾,则认为是一个jar文件,使用JarLoader
来寻找类,即在Jar包中寻找类。 - URL以斜杠
/
结尾,且协议名为file
,使用FileLoader
来寻找类,即在本地文件系统中寻找.class
文件。 - URL以斜杠
/
结尾,且协议名不为file
,使用最基础的Loader
来寻找类。
在正常开发是通常使用的是前两种方式,想要使用最基础的Loader
实现加载可以通过http
协议:
1 | public class Hello { |
这里写一个Hello
生成对应的.class
文件,用于远程加载,生成Hello.class
文件可以直接通过 idea 生成, 也可以通过javac
命令生成。
1 | javac Hello.java |
可以看到成功请求到对应的.class
文件,并且成功加载了Hello.class
文件以及初始化。
那么如果远程加载的地址是可控的,那么攻击者就可以利用远程加载的方式执行任意代码了。
三、defineClass
加载字节码
了解了ClassLoader
加载类,那么具体是如何加载字节码文件呢?下面继续来看:
不管是加载.class
还是.jar
文件,Java都会经历以下过程:
1 | ClassLoader#loadClass -> ClassLoader#findClass -> ClassLoader#defineClass |
loadClass
:是从已加载的类缓存、父加载器等位置寻找类(双亲委派机制),在前面没有找到的情况下执行findClass
。findClass
:是根据基础URL指定的方式来加载类的字节码,可能会在本地系统、jar包或者远程http服务器上读取字节码,然后交给defineClass
。defineClass
:是处理findClass
提供的字节码,将其处理为真正的java类。
分析完上面的整个流程,可见整个流程中的核心是defineClass
,它决定了一段字节流如何转变为一个Java类,并且ClassLoader#defineClass
最终是调用了一个Java的Natinve方法,其逻辑在JVM中通过C语言代码实现。
这里借鉴P神的代码来演示一下
1 | public static void helloDefineClass() throws Exception { |
1 | 获取字节码的base64字符串 |
需要注意的是,在defineClass
被调用的时候,类对象是不会被初始化的,只有这个对象显式地调用该类的构造函数,初始化代码才能执行。并且,即使将初始化代码写在static
代码块中,在defineClass
时也无法被直接调用到。所以,如果我们要使用defineClass
来实现代码执行,就需要想办法调用到构造函数。
这里因为系统的defineClass
是一个protected
属性,所以我们是无法直接在外部访问的,需要通过反射的形式来调用。在实际场景中,因为defineClass
方法的作用域是不对外开放的,所以攻击者很少能直接利用,但它却是一个常用攻击链的基石——TemplatesImpl
。
四、代码执行顺序
明确一下类的初始化和对象的初始化
-
类的初始化:
指的是当类被加载到JVM中时执行的过程。在这个过程中,JVM会执行静态初始化块和静态变量的初始化。这些静态成员只会初始化一次,在类加载过程中完成。静态初始化块中的代码会在类加载时执行,这意味着在第一次创建类的对象之前执行 。
-
对象的初始化:
指的是创建类的对象时执行的过程。在对象初始化的过程中,JVM会为对象分配内存,并执行非静态初始化块、实例变量的初始化以及构造函数。
这里通过一段代码来验证一下整个相关的执行过程:
1 | public class Initialize { |
可以看到确实是静态代码块先于对象的初始化。
并且,在仅进行类加载时 ,静态代码块中的代码就可以执行,当然 ,这里要看下Class.forName()
可以看到Class#forName
是有两个实现,但两个实现都是调用了Class#forName0
的Native方法,其中Class#forName0
方法参数initialize
,表示在类加载时,是否对类进行初始化,
而上述代码中调用的Class#forName
默认 initialize=true
,即在加载类时进行初始化,
所以直接通过Class.forName(String className)
会执行static
中的代码。
0x04 TemplatesImpl分析
在了解完类加载之后,接下来开始分析这条CC3链中的核心TemplatesImpl
:
在上述说到,可以通过加载.class
文件来实现命令执行,这里先准备一个calc.class
文件,用以验证命令执行。
在类加载中有提到,defineClass
是加载类的核心,这里直接找到ClassLoader#defineClass
,然后findUsages,因为defineClass
默认属性是protected
,所以我们希望可以找一个public
属性的重载,或者通过某种方式被外部调用的重载。
最终也是找到了一个default
属性的重载
default
类型的方法可以被内部类调用,直接在文件中搜索看哪个方法调用了
该类下仅有这个defineTransletClasses
方法调用了defineClass
,分析一下这个方法,想要满足调用defineClass
,必须满足_bytecodes != null
,并且这个方法是private
属性,还不符合我们的期望,再看这个方法被谁调用了
getTransletInstance
想要满足调用defineTransletClasses
,必须满足_name!=null
,并且_class==null
,并且在调用完defineTransletClasses
后,该方法还会执行newInstance
方法,即初始化_class[_transletIndex]
,该方法就非常符合我们对反序列化链构造的预期,但该方法仍然是private
属性,继续找
终于找到一个public
属性的方法,并且该方法直接就可以去调用getTransletInstance
。
找到了完整的调用过程如下:
1 | TemplatesImpl#newTransformer |
这里直接写一个exp
构想的整个调用链如上,在满足所有条件后,会通过调用newTransformer
方法实现代码执行,但这里还没有满足执行所需的条件,所有下面继续看:
newTransformer
方法中没有需要满足的条件直接就可以调用下一个方法,直接看getTransletInstance
方法
这里想要调用defineTransletClass
,需要满足 _name!=null && _class==null
这两个都是TemplatesImpl
的成员变量,然后去看TemplatesImpl
的构造方法,看在初始化对象的时候是否进行了这两个成员变量的初始化
这里我们调用的构造方法是空,所以就需要自己赋值:
1 | public static void exp1() throws Exception { |
然后继续断点调试
发现成功满足_name!=null&_class=null
,会进入到defineTransletClasses
然后看想要调用defineClass
需要满足的条件:
首先是需要满足_bytecodes != null
,并且_tfactory
调用了一个方法,所以_tfactory
也不能为null
。
可以看到_bytecodes
是一个byte[][]
,另外_tfactory
被transient
修饰,即不序列化这个成员变量,这里先给_bytecodes
赋值,_tfactory
等下再分析。可以在defineTransletClasses
中发现,_bytecodes
其实就是我们要加载的字节码,并且是通过一个for循环逐个通过defineClass
加载一个byte[]
,如下:
那么其实这里我们只需要传入一个一维的字节数组即可
同样断点调试,发现在_tfactory
处出现了空指针异常,因为现在_tfactory=null
,所以这里要给_tfactory
进行赋值
那么我们就要看_tfactory
正常是如何进行赋值的:
可以看到,_tfactory = new TransformerFactoryImpl()
,这里我们同样通过反射的方式来对_tfactory
进行赋值:
1 | public static void exp1() throws Exception { |
同样断点执行:
发现在_auxClasses
处出现了空指针错误,虽然这里执行了defineClass
,但是没有实例化对象,仍然不能执行命令,所以这里必须要正常结束defineTransletClasses
的调用去执行getTransletInstance
方法中的newInstance
实例化我们加载的字节码。
分析下如何才能避免空指针异常
1 | if (superClass.getName().equals(ABSTRACT_TRANSLET)) { |
这里,要么对_auxClasses
进行赋值,要么令superClass.getName().equals(ABSTRACT_TRANSLET)
为true
,这里需要选择后者,因为如果走到else
时,_transleetIndex不会被赋值
,那么
而_transletIndex
初始化值为-1
则又会抛出异常,所以这里需要去走到if
下
就需要我们的_class[i].getSuperClass()==ABSTRACT_TRANSLET
,ABSTRACT_TRANSLET
的值为:
也就是说,传入的字节码的类需要继承这个ABSTRACT_TRANSLET
因为AbstractTranslet
是一个抽象类,所以需要实现相应的抽象方法
然后再将这个类编译一下,再执行我们的exp
也是成功执行了我们的恶意代码。
0x05 TemplatesImpl
利用
上面所分析的其实只是一种命令执行的方式,还需要配合来构建完整的反序列化链,例如与CC1和CC6相结合
下面给出CC1利用TemplatesImpl
执行命令的exp:
1 | TemplatesImpl templates = new TemplatesImpl(); |
因为CC6和CC1前半段链是相同的,所以不再讨论。
0x06 CC3分析
接着TemplatesImpl
的分析,是成功分析到newTransform
,直接在newTransformer
上findUsages

这里找到4个,这里分析下为什么会选择TrAXFilter
:
这个TrAXFilter
没有继承Serializable
,但是这个构造方法中_transformer = (TransformerImpl) templates.newTransformer();
,直接通过调用TrAXFilter
的构造方法就可以实现命令执行。
然后来看下CC3使用的InstantiateTransformer
:
InstantiateTransformer#transform
如果传入的Class不为空,则调用该类的构造方法并进行实例化,调用这个transform
即为:
input(iArgs)
,通过构造函数传入TemplatesImple
,然后transform
传入TrAXFilter
即:
1 | InstantiateTransformer instantiateTransformer = new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates}); |
调用了transform
就可以和CC1或CC6前半段链结合,这里以CC1为例:
1 | public static void exp2() throws Exception { |
成功命令执行。
0x07 总结
CC3是为了绕过一些规则对InvokerTransformer
和Runtime
的限制。