Java反序列化: 基于CommonsCollections4的Gadget分析
来这里找志同道合的小伙伴!
本文主要分为两方面,一方面是基于PriorityQueue类的序列化对象的构造,另一方面是PriorityQueue对象在反序列化过程中恶意代码的触发原理。
随着Java应用的推广和普及,Java安全问题越来越被人们重视,纵观近些年来的Java安全漏洞,反序列化漏洞占了很大的比例。就影响程度来说,反序列化漏洞的总体影响也明显高于其他类别的漏洞。
在反序列化漏洞的利用过程中,攻击者会构造一系列的调用链以完成其攻击行为。如何高效的生成符合条件且可以稳定利用的攻击Payload成为了攻击链条中的重要一环,当前已经有很多现成的工具帮助完成Payload的生成工作。本文主要以Ysoserial工具为例分析了基于org.apache.commons.collections4类库的Gadget,其通过构造一个特殊的PriorityQueue对象,将其序列化成字节流后,在字节流反序列化的过程中触发代码执行。
更多关于Ysoserial的信息,请参考:https://github.com/frohoff/ysoserial
本文主要分为两方面,一方面是基于 PriorityQueue 类的序列化对象的构造,另一方面是 PriorityQueue 对象在反序列化过程中恶意代码的触发原理。下文将从这两方面展开描述一些细节以及实际测试时的一些问题,整体的流程如下图所示:
▲图1-1
首先,被序列化为字节流的对象实际是一个特殊的PriorityQueue对象,本小节主要分析构造该对象的过程,即图1-1的第一步。
图2-1为 ysoserial.payloads.CommonsCollections4 中 getObject 方法的代码,是用于构造该 PriorityQueue 对象的代码:
▲图2-1
上图中需要注意的有如下两点:
通过 createTemplatesImpl 方法生成 templates 对象
通过 PriorityQueue 类的比较器将构造的一系列 transformer 串联起来
1、0x0A createTemplatesImpl 方法生成攻击载荷
通过 createTemplatesImpl 方法生成 templates 对象是非常重要的一部分,因为这是实际承载恶意代码的对象。详细说一下,跟进分析 createTemplatesImpl 方法,其代码具体实现和关键点流程分别如下图2-2和图2-3所示:
▲图2-2
▲图2-3
首先生成 TemplatesImpl 实例,然后通过 javassist 类库修改 StubTransletPayload 类字节码,在其中插入执行命令的代码(这里是通过 java.lang.Runtime.getRuntime().exec() 方法执行命令,也可以插入其他利用代码,如反弹 shell 等),然后将其父类设置为 abstTranslet 类,最后将修改后的字节码通过反射写入到 TemplatesImpl 实例的 _bytecodes 变量中,这里还同时写入了 Foo.class 的字节码。除此之外,为了后续恶意代码的触发(如作者注释中所写:required to make TemplatesImpl happy),还要修改 TemplatesImpl 实例的 _name 和 _tfactory 变量,否则后面会在命令代码执行前抛出异常。 StubTransletPayload 类代码实现如图2-4所示:
▲图2-4
StubTransletPayload 类继承自 AbstractTranslet 类并实现了 Serializable 接口,通常构造一个恶意类可能会直接在 static 代码块或构造方法中写入想要执行的代码,这一步在上面通过 javassist 类库实现,关于 StubTransletPayload 类需要继承 AbstractTranslet 类的原因会在反序列化恶意代码触发时解释。 以上即为 createTemplatesImpl 方法的实现,其本质上是构造了一个特定结构的 TemplatesImpl 类实例,具体变量的值如图2-5所示:
▲图2-5
2、构造并串联 Transformer
回到图2-1,本段开始处 getObject 方法的代码中,在35行和40行分别初始化了 ConstantTransformer 对象和 InstantiateTransformer 对象,47行将两个对象构造成 Transformer 数组作为参数初始化了 ChainedTransformer 对象 chain。
而在50行,这个 ChainedTransformer 对象 chain 又是要序列化的对象 PriorityQueue 中 comparator 构造方法的参数,comparator 可以理解为在 PriorityQueue 中决定优先次序的比较器,此处用的是 TransformingComparator 对象。
在44-45行、55-57行利用 java 的反射机制和引用传递的特性修改 chain 对象中的变量,ConstantTransformer 对象中 iConstant 变量的值设为 com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter.class,InstantiateTransformer 对象中 iParamTypes 设为 javax.xml.transform.Templates.class,iArgs 设为此前构造的 templates 对象。
51、52行向队列中插入两个1,这里是为了后面堆化时触发一次堆排序。 最终构造了一个用 TransformingComparator 对象作比较器的 PriorityQueue 对象,其内存中变量示意图和抽象结构图分别如图2-6和图2-7所示:
▲图2-6
▲图2-7
接下来将分析下这个对象序列化后的字节序列如何在反序列化的过程中触发代码执行。
反序列化开始至触发代码执行的整体流程如图3-1所示:
▲图3-1
反序列化过程中首先进入 ObjectInputStream 类的 readObject 方法中,然后进入 readObject0 方法中读取字节流,其中会读取 tc 标记,然后根据 tc 标记的类型进入不同的逻辑处理函数中,标记类型可见图3-2:
▲图3-2
反序列化的是 PriorityQueue 对象,这里会进入 TC_OBJECT 的处理逻辑中,跟进到 readOrdinaryObject 方法里,其具体代码如图3-3,在1769行读取类描述信息,1780行通过类描述信息,初始化对象 obj(即 PriorityQueue 对象)。
▲图3-3
在图3-4中1793行判断是否实现 Externalizable 接口,通过 Externalizable 接口可以通过调用对象的 readExternal 方法实现自定义地完全控制某一对象及其超类的流格式和内容,这里代码进入默认的 readSerialData 方法中。
▲图3-4
在图3-5中1882行判断序列化对象是否有 readObject 方法,如果有则通过反射调用对象的 readObject 方法为成员变量赋值,接下来就进入了 PriorityQueue 对象的 readObject 方法中。
▲图3-5
图3-6为 PriorityQueue 对象的 readObject 方法。
▲图3-6
图3-7中在 defaultReadObject 方法中会调用 defaultReadFields 方法为成员变量赋值。
▲图3-7
defaultReadFields 方法中1989行会递归调用 readObject0 方法为对象的成员变量赋值直至完成,逻辑与前面描述相似,此处不再赘述,详情参见图3-8。
▲图3-8
defaultReadObject 方法执行完成后,代码流程回到 PriorityQueue 对象的 readObject 方法(图3-6)中,读取被 transient 修饰的 Object 数组 queue(此前被赋值为两个int型的数值1),这部分可以和 PriorityQueue 类的 writeObject 方法对照着看(图3-9)。
▲图3-9
然后代码流程进入图3-6中173行的 heapify 方法,PriorityQueue 本质上是一个最小堆,通过 siftDown 方法进行次序的调整实现堆化,之前往 PriorityQueue 对象中插入两个1,可以使队列的 SIZE 满足 for 循环的条件从而进入 siftDown 方法中(图3-10)。
▲图3-10
继续跟进 siftDown 方法,次序的调整必然涉及比较,在这儿此前精心构造的比较器就派上用场了,跟进 siftDownUsingComparator 方法,在图3-11中699行调用了比较器的 compare 方法。
▲图3-11
跟进 compare 方法,在比较前会先通过 transformer 的 transform 方法转换一下对象。而此处的 transformer 正是此前构造的 ChainedTransformer 对象 chain 序列化成字节流后又反序列化所得(在递归调用 readObject0 方法时实现),如图3-12所示。
▲图3-12
继续跟进到 ChainedTransformer 的 transform 方法中,此时 iTransformers 中有 ConstantTransformer 对象和 InstantiateTransformer 对象,此处代码逻辑是将 ConstantTransformer 对象中 transform 方法的返回值作为参数传入 InstantiateTransformer 对象的 transform 方法中,如图3-13所示。
▲图3-13
ConstantTransformer 对象中 transform 方法的返回 iConstant 变量,即 com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter.class,如图3-14所示。
▲图3-14
InstantiateTransformer 对象中 transform 方法反射获取构造方法后生成了 TrAXFilter 类的实例,通过 newInstance 方法进入了 TrAXFilter 类含参构造方法 TrAXFilter(Templates templates)中,并将 TemplatesImpl 实例作为参数传入,如图3-15所示。
▲图3-15
TrAXFilter(Templates templates)方法代码如图3-16所示,在64行调用了 TemplatesImpl 对象的 newTransformer 方法,newTransformer 方法中又调用 getTransletInstance 方法(图3-17中410行),恶意代码的触发便是在该方法中。
▲图3-16
▲图3-17
如图3-18所示,getTransletInstance 方法中第376行调用了 defineTransletClasses 方法后,380行会将 _class 数组中的某个类实例化:
▲图3-18
跟进 defineTransletClasses 方法发现有如图3-19所示这样一段代码:
▲图3-19
其在 for 循环里遍历 _bytecodes 数组并通过 TransletClassLoader 加载字节码,其中会判断 _class[i] 的父类是否为 ABSTRACT_TRANSLET(”com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet”),这解释了为什么 _bytecodes 中的 StubTransletPayload 类要继承自 AbstractTranslet 类,_transletIndex 变量初始化时为-1,若此处判断条件为 false,_transletIndex 的值仍为-1,则程序执行流程会进入后面 if (_transletIndex < 0)的代码块中抛出异常。构造 StubTransletPayload 类为 AbstractTranslet 类的子类即可把恶意类的索引值 i 赋值给 _transletIndex。defineTransletClasses 方法执行完成后,跳回到 getTransletInstance 方法中,将 _class[_transletIndex](即 StubTransletPayload 类)实例化触发之前通过 javassist 类库插入的代码块,实现代码执行(图3-20)。
▲图3-20
到这儿基本上整个 Gadget 的触发流程就走完了。此处通过调用 TemplatesImpl 对象的 newTransformer 方法去间接的调用 getTransletInstance 方法实现代码执行。除此之外,TemplatesImpl 类中的 getOutputProperties 方法又调用了 newTransformer,例如 fastjson 的反序列化中基于 TemplatesImpl 类的 Gadget 便是通过 getOutputProperties 方法去触发代码执行。理论上只要构造特定的 TemplatesImpl 类对象,然后调用其 getTransletInstance 方法就可以实现代码执行。为方便理解,写了一个简单的 Demo,通过反射正向构造了一个 TemplatesImpl 对象并调用其 getTransletInstance 方法来触发代码执行,代码如下:
evil.java 代码如下:
在 demo 中是通过插入静态代码块的方式注入恶意代码,可以看到后面 defineClass 对类的加载时一度以为这样的实现类似于 fastjson 中基于 com.sun.org.apache.bcel.internal.util.ClassLoader 类实现的 POC(具体可参考文章 DefineClass 在 Java 反序列化当中的利用),在类加载的过程中实现的 static 代码块执行,但后来调试时发现 static{} 中插入的恶意代码仍然是在类实例化(即调用 newInstance())时触发。 关于类加载的过程,在《深入理解 Java 虚拟机》中虚拟机类加载机制一节中有详细的说明,类加载可分为加载、验证、准备、解析和初始化这五个阶段。其中 static 代码块的执行是在初始化阶段,初始化阶段实际是执行类构造器<clinit>()的过程,<clinit>()是在 Javac 编译过程中生成字节码时被添加到语法树中。
<clinit>()方法是编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生。——《深入理解 Java 虚拟机》
书中还提到虚拟机规范严格规定了有且只有四种情况必须立即对类进行初始化:
遇到new、getstatic、putstatic、invokestatic这四条字节码指令时,如果类还没有进行过初始化,则需要先触发其初始化。生成这四条指令最常见的Java代码场景是:使用new关键字实例化对象时、读取或设置一个类的静态字段(static)时(被static修饰又被final修饰的,已在编译期把结果放入常量池的静态字段除外)、以及调用一个类的静态方法时。
使用Java.lang.refect包的方法对类进行反射调用时,如果类还没有进行过初始化,则需要先触发其初始化。
当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化。
当虚拟机启动时,用户需要指定一个要执行的主类,虚拟机会先执行该主类。
前面在通过 TransletClassLoader 中的 defineClass 方法加载类时仅将字节码装载到了 JVM 中,没有执行类的初始化,而 fastjson 的 Poc 中通过 Class.forName() 加载类时,Class.forName() 方法除了将对应的类装载到 JVM 中,还会执行类构造器 <clinit>() 对类进行初始化,从而执行 static 代码块。 Class.forName() 代码实现(JDK1.7)见图4-1:
▲图4-1
forName0() 方法用 native 关键字修饰,说明这个方法是原生函数,非 Java 语言实现。可从 forName() 方法的注释中看到第二个参数决定类是否会被初始化,在 forName(String className) 中默认为 true。以上基本解释了关于注入的静态代码触发位置的疑惑。
整个Gadget的调用栈见图5-1:
▲图5-1
反序列化时首先从 ObjectInputStream 类的 readObject 方法中进入到 PriorityQueue 类的 readObject 方法里,其 readObject 方法中会进行堆化,堆化时队列中元素大于等于2时会进行堆排序,这时会调用自定义的比较器(TransformingComparator),TransformingComparator 在比较次序时会将对象进行转换。转换时使用的 transformer 是基于 ConstantTransformer 对象和 InstantiateTransformer 对象构造的 ChainedTransformer 对象,ChainedTransformer 对象在其转换方法(transform())中会依次调用 ConstantTransformer 对象和 InstantiateTransformer 对象的 transform 方法,并将前一个对象 transform 方法的返回值作为参数传入后一个对象的 transform 方法中,InstantiateTransformer 对象中的 transform 方法会基于参数(这里即 ConstantTransformer.transform() 的返回值 com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter)新建实例,则进入了 TrAXFilter 类的构造方法中,这里调用了 TransformerImpl 实例的 newTransformer 方法,又调用了 getTransletInstance 方法,加载 _bytecodes 中修改后的 StubTransletPayload 类字节码并生成实例,从而触发代码执行。
参考
https://github.com/frohoff/ysoserial
https://stackoverflow.com/questions/39504847/why-does-class-not-invoke-the-static-block-in-a-class
https://www.freebuf.com/articles/others-articles/167932.html
推荐阅读
应对精细化运营要求,京东数据平台有哪些优化经验?
管理百万容器如何做到高效、稳定又省钱?
揭秘 | 技术方案解答智能客服如何双商俱高
如何实现百万TPS?详解JMQ4的存储设计
京东技术
---关注技术的公众号
长按识别二维码关注