文章分类

Java反序列 Jdk7u21 Payload 学习笔记

由 b1ngzz 于 2018-04-12 18:31:50 发表

0x00 简介


最近在看Java 反序列化的一些东西,在学习 ysoserial 的代码时,看到 payload list 中有一个比较特殊的 Jdk7u21,该 payload 不依赖第三方库,只需 JRE 即可完成攻击,影响 JRE versions <=7u21 的版本。在学习的过程中,觉得很有意思,这里记录一下的过程,如果有什么地方写的不准确或错误,欢迎指出

文章中的代码运行环境为JDK 7u21


0x01 知识点


在分析代码之前,我们先来了解一些相关知识,有助于后续理解


javassist

javassist :Java字节码操作库,提供了在运行时操作Java字节码的方法,如在已有 Class 中动态修改和插入Java代码,示例:在 Cat 类中添加包含恶意代码的 static block

生成的.class,反编译后的源码如下:

除了static block,也可以在constructor 或其他方法中添加代码。关于javassist 的详细介绍可以参考 http://www.cnblogs.com/hucn/p/3636912.html

在Jdk7u21 的payload 中,使用了javassist 来构造包含恶意代码的class


Java static initializer

Java Class 中定义的static 代码块被称为 staticinitializer,在class 初始化(initialized) 时会执行该语句块

对于"class 初始化",听起来比较抽象,这里通过代码来说明一下:

这里需要重点关注一下ClassLoader.defineClass()方法运行后,并不会执行 static block,而Class.newInstance()会执行,这两个地方会涉及到Jdk7u21 payload 恶意代码的具体执行点

关于Class.forName("SomeClass");和 ClassLoader.loadClass("SomeClass");,有兴趣的可以参考 https://stackoverflow.com/a/8100407/6467552


"f5a5a608"的hashCode为0

Java Object 中定义了hashCode()方法,返回一个hash 值,当两个对象equals 时,hashCode需要相同

String 类重写了该方法

有一个特殊的字符串"f5a5a608",hashCode的值为 0,在构造 Jdk7u21 payload 的过程中利用到了这一点


Dynamic Proxy

在ysoserial 的代码中,大量使用了到动态代理机制来构造payload,我们来简单了解一下

当需要增加或者修改某些已存在class的功能时,会使用动态代理机制,通过创建 proxyobject 来代理实际的对象。主要涉及接口为InvocationHandler

接口中只定义了一个方法invoke(),所有 proxy object 的方法调用都会转换为调用 invoke()方法,调用方法和参数通过method和 args来传递

来看一个代理Map 接口的例子,会在所有方法的执行之前打印start 、执行完成后打印finish


0x02 Payload分析 & 构造


TemplatesImpl

在利用payload 中,TemplatesImpl类主要的作用为:

  • 使用_bytecodes成员变量存储恶意字节码( 恶意class=> byte array )

  • 提供加载恶意字节码并触发执行的函数,加载在defineTransletClasses()方法中,方法触发为getOutputProperties()或 newTransformer()

我们来具体看一下,该类位于com.sun.org.apache.xalan.internal.xsltc.trax包中,用于xml document 的处理和转换,定义如下

TemplatesImpl 类实现了Templates和 Serializable两个接口

其中Templates接口定义如下,包含了两个方法,即之前提到触发恶意代码执行所的方法

在TemplatesImpl类中有一个private 方法 defineTransletClasses(),精简后的代码如下

在方法中,调用了ClassLoader.defineClass()方法,参数为实例变量_bytecodes内的元素,该方法会将字节数组转换为Class,并加载

也就是说,通过设置_bytecodes的内容 ,调用 defineTransletClasses() 方法即可加载指定的 Class。

在代码中,一共有三个地方调用了这个方法

  • getTransletClasses()

  • getTransletIndex()

  • getTransletInstance()

在Java static initializer 部分提到 ClassLoader.defineClass() 并不会执行 static 代码块,所以前两个方法不满足条件,再看一下 getTransletInstance()方法

defineTransletClasses() 执行后,会调用之前加载的 Class 的 newInstance() 方法来创建实例,触发 static block 和 constructor 的执行,根据方法调用关系

可以看到调用getOutputProperties()或 newTransformer()方法均可触发恶意代码的执行

理一下思路

  • 使用javassist库创建一个包含恶意代码的class,恶意代码可以在static block中,或在无参构造函数里

  • 将恶意class 的的字节码添加到TemplatesImpl 实例的_bytecodes变量中

  • 调用实例的getOutputProperties()或 newTransformer()方法触发恶意代码执行

弹出计算器的代码示例如下(程序报错可以忽略)

在上面的代码示例中,是手动调用newTransformer()来触发恶意代码的执行,因此还需要找到一个能够在反序列化过程中,自动调用 (直接或间接) 该方法的类


AnnotationInvocationHandler

在构造payload 中,利用了 AnnotationInvocationHandler提供的 equals方法的默认实现,来触发对Tempaltes接口中 getOutputProperties()或 newTransformer()的调用,来具体看一下

AnnotationInvocationHandler 位于 sun.reflect.annotation 包中,用于 Annotation 的动态代理,其定义如下

可以看到实现了InvocationHandler和 Serializable两个接口,根据 Dynamic Proxy 部分的介绍,使用 AnnotationInvocationHandler 创建的 proxy object 的所有方法调用都会变成对 invoke 方法的调用,来看一下方法的实现

 可以看到当调用方法名为equals时,且参数个数和类型匹配,则调用内部equalsImpl方法

跟入后可以看到,首先获取typeClass 所有声明的方法,然后在参数Object o 上使用反射调用方法,因此前面所说TemplatesImpl 实例是需要作为参数传入

理一下思路

  1. 根据TemplatesImpl 部分的说明,创建一个包含恶意代码的TemplatesImpl 实例evilTemplates

  2. 使用AnnotationInvocationHandler 创建proxy object 代理Templates 接口 (会使用到反射)

  3. 调用proxy object 的equals方法,将 evilTemplates作为参数

示例代码如下,运行即可弹出计算器

这里结合了TemplatesImpl和 AnnotationInvocationHandler,将包含恶意代码的Templates 对象作为参数,手动调用equals方法来触发代码执行,所以还是需要再找到一个能够在反序列化过程中,满足这一条件的场景,我们来继续看第三个关键的类


LinkedHashSet

在利用payload 中,LinkedHashSet是最外层的类,包含恶意代码的实例和proxyobject 会作为元素添加到set 中,在反序列化过程中,会调用到前一部分所说的equals方法,来具体看一下

LinkedHashSet 位于java.util包中,是HashSet 的子类,添加到set 的元素会保持有序状态,内部实现基于 HashMap

在HashSet 的 writeObject()方法中,会依次调用每个元素的writeObject()方法来实现序列化

相应的,在反序列化过程中,会依次调用每个元素的readObject()方法,然后将其作为key(value 为固定值) 依次放入 HashMap 中

来看一下HashMap的 put()方法,首先会调用内部hash()函数计算 key的 hash 值,然后遍历所有元素,当要插入的元素的 hash 和已有entry 相同,且 key 和 Entry的key 指向同一个对象 或 二者equals时,则认为 key是否已经存在,返回oldValue,否则调用 addEntry()添加元素

代码中将已有元素的 key 值作为参数 (k 变量),调用了插入key 的 equals 方法来判断而这是否相等,这里我们只要反序列化过程中让 proxy object 先添加,然后再添加包含恶意代码的实例 (序列化时添加要顺序相反),正好是我们在 AnnotationInvocationHandler小节最后,提到的部分

理一下思路

  • 创建一个LinkedHashSet

  • 先将包含恶意代码的Templates 对象添加到hashSet 中

  • 将使用AnnotationInvocationHandler 创建的proxyobject (代理Templaes 接口) 添加到 hashSet 中,在反序列化过程中,会调用 proxy 的 equals 方法 (包含恶意代码的Templates 对象作为参数),触发恶意代码执行

在反序列化过程中,需要保证HashSet 内的 entry保持有序,这也是为什么使用LinkedHashSet的原因

根据代码分析,在执行到equals()之前,需要满足两个条件

  1. e.hash == hash

  2. (k = e.key) != key

条件2 比较两个变量是否指向同一个对象,这里满足(一个为包含恶意代码的templates 实例,一个为proxy object),条件1判断的是 hash 值是否相等,来看一下 hash 值是如何计算的

可以看到,计算结果只受k.hashCode()的影响

  • 对于普通对象,返回的是就是 k.hashCode()

  • 对用proxy object,因为会统一调用inove(),而AnnotationInvocationHandler在 inove()方法中提供了 hashCode()的实现,代码如下,内部调用了hashCodeImpl()

hashCodeImpl() 代码如下 ,这里稍微修改了下代码,便于理解

for 循环内调用了memberValueHashCode()函数,其精简代码如下

如果Entry 的 value 的 Class 不为 Array,则 memberValueHashCode() 函数返回 value.hashCode(),在这里相当于

127 * key.hashCode() ^value.hashCode();

为了让最后返回的result和 value.hashCode()相同,这就要求

  • memberValues 仅有一个 entry,否则 for 循环内每次计算的结果会累加

  • key.hashCode() 的值为0,从而 127 * key.hashCode() = 0,0 和 任何数异或还是原值

  • value 和之前添加到hashset 的对象相同, (利用代码中该值为包含恶意代码的 templates 对象)

前面提到字符串f5a5a608的hashCode 为 0,所以这里只要让 AnnotationInvocationHandler的 memberValues内只放一个key 为字符串 f5a5a608,value 为包含恶意代码的 templates 对象即可

到这里,就可以写出完整的利用代码

反序列化过程的方法调用链如下

完整的代码,可以参考 ysoserial 的 Class Jdk7u21 的代码


0x 03 修复方案


在jdk > 7u21 的版本,修复了这个漏洞,看了下7u79 的代码,AnnotationInvocationHandler的构造方法,增加了对参数的校验,type必须为Annotation,所以会导致原有payload 执行失败

修复前:

修复后:

   

0x 04 参考资料

 

  • Java7u21 Security Advisory

  • java反序列化工具ysoserial分析

  • Java动态编程初探——Javassist