最近在看Java 反序列化的一些东西,在学习 ysoserial 的代码时,看到 payload list 中有一个比较特殊的 Jdk7u21 ,该 payload 不依赖第三方库,只需 JRE 即可完成攻击,影响 JRE versions <=7u21 的版本。在学习的过程中,觉得很有意思,这里记录一下的过程,如果有什么地方写的不准确或错误,欢迎指出
文章中的代码运行环境为JDK 7u21
在分析代码之前,我们先来了解一些相关知识,有助于后续理解
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 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
Java Object 中定义了hashCode()方法,返回一个hash 值,当两个对象equals 时,hashCode需要相同
String 类重写了该方法
有一个特殊的字符串"f5a5a608",hashCode的值为 0,在构造 Jdk7u21 payload 的过程中利用到了这一点
在ysoserial 的代码中,大量使用了到动态代理机制来构造payload,我们来简单了解一下
当需要增加或者修改某些已存在class的功能时,会使用动态代理机制,通过创建 proxyobject 来代理实际的对象。主要涉及接口为InvocationHandler
接口中只定义了一个方法invoke(),所有 proxy object 的方法调用都会转换为调用 invoke()方法,调用方法和参数通过method和 args来传递
来看一个代理Map 接口的例子,会在所有方法的执行之前打印start 、执行完成后打印finish
在利用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()来触发恶意代码的执行,因此还需要找到一个能够在反序列化过程中,自动调用 (直接或间接) 该方法的类
在构造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 实例是需要作为参数传入
理一下思路
根据TemplatesImpl 部分的说明,创建一个包含恶意代码的TemplatesImpl 实例evilTemplates
使用AnnotationInvocationHandler 创建proxy object 代理Templates 接口 (会使用到反射)
调用proxy object 的equals方法,将 evilTemplates作为参数
示例代码如下,运行即可弹出计算器
这里结合了TemplatesImpl和 AnnotationInvocationHandler,将包含恶意代码的Templates 对象作为参数,手动调用equals方法来触发代码执行,所以还是需要再找到一个能够在反序列化过程中,满足这一条件的场景,我们来继续看第三个关键的类
在利用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()之前,需要满足两个条件
e.hash == hash
(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 的代码
在jdk > 7u21 的版本,修复了这个漏洞,看了下7u79 的代码,AnnotationInvocationHandler的构造方法,增加了对参数的校验,type必须为Annotation,所以会导致原有payload 执行失败
修复前:
修复后:
Java7u21 Security Advisory
java反序列化工具ysoserial分析
Java动态编程初探——Javassist