JDK7u21原生反序列化利用链
JDK7u21原生反序列化利用链
本文主要摘抄自p师傅的java漫谈18,用于学习。
在Java7u21
版本及以前的版本(只是java7
中,java6
不一定,后面有说),存在一条不依赖第三方库的Java
反序列化利用链,在新版本的Java
中没有。
1. 核心原理
相信学习了CommonsCollections
的这些利用链后,大家心里对反序列化有自己的认识。如果问,什么是某条反序列化利用链的核心点,有的同学可能会说是readObject
或TemplatesImpl
。不过我的理解是,核心在于触发“动态方法执行”的地方,而不是TemplatesImpl
或某个类的readObject
方法。
举几个例子:
CommonsCollections
系列反序列化的核心点是那一堆Transformer
,特别是其中的InvokerTransformer
、InstantiateTransformer
CommonsBeanutils
反序列化的核心点是PropertyUtils#getProperty
,因为这个方法会触发任意对象的getter
而JDK7u21
的核心点就是 sun.reflect.annotation.AnnotationInvocationHandler
,当时只用到了这个类会触发 Map#put
、 Map#get
的特点。
AnnotationInvocationHandler
类中的equalsImpl
方法有个很明显的反射调用memberMethod.invoke(o)
,而memberMethod
来自于this.type.getDeclaredMethods()
。
也就是说,this.type.getDeclaredMethods()
这个方法是将this.type
类中的所有方法遍历并执行了。那么,假设是Templates
类,则势必会调用到其中的newTransformer()
或getOutputProperties()
方法,进而触发任意代码执行。
这就是JDK7u21
的核心原理。
2. 如何调用equalsImpl
那么,现在的任务就是通过反序列化调用equalsImpl
,equalsImpl
是一个私有方法,在AnnotationInvocationHandler#invoke
中被调用。AnnotationInvocationHandler#invoke
这个知识点,在第11篇中提到过:
作为一门静态语言,如果想劫持一个对象内部的方法调用,实现类似PHP
的魔术方法我们需要用到java.reflect.Proxy
Proxy.newProxyInstance
的第一个参数是ClassLoader
,我们用默认的即可;第二个参数是我们需要代理的对象集合;第三个参数是一个实现了InvocationHandler
接口的对象,里面包含了具体代理的逻辑。
我们回看sun.reflect.annotation.AnnotationInvocationHandler
,会发现实际上这个类实际就是一个InvocationHandler
,我们如果将这个对象用Proxy
进行代理,那么在readObject
的时候,只要调用任意方法,就会进入到 AnnotationInvocationHandler#invoke
方法中,进而触发我们的LazyMap#get
。
在使用java.reflect.Proxy
动态绑定一个接口时,如果调用该接口中任意一个方法,会执行到InvocationHandler#invoke
。执行invoke
时,被传入的第一个参数是这个proxy
对象,第二个参数是被执行的方法名,第三个参数是执行时的参数列表。
而 AnnotationInvocationHandler
方法就是一个InvocationHandler
接口的实现,我们看看它的invoke
:
1 | public Object invoke(Object proxy, Method method, Object[] args) { |
可见,当方法名等于equals
,且仅有一个Object
类型参数时,会调用到equalImpl
方法。
所以,现在的问题变成,我们需要找到一个方法,在反序列化时对proxy
调用equals
方法。
3. 找到equals方法调用链
比较Java
对象时,我们常用到两个方法:
equals
compareTo
任意Java
对象都拥有equals
方法,它通常用于比较两个对象是否是同一个引用;而compareTo
实际上是java.lang.Comparable
接口的方法,通常被实现用于比较两个对象的值是否相等。java.util.PriorityQueue
用的是compareTo
;
常见的会调用equals
的场景就是集合set
。set
中储存的对象不允许重复,所以在添加对象的时候,势必会涉及到比较操作。
HashSet使用了一个HashMap,将对象保存在HashMap的key处来做去重。
HashMap,就是数据结构里的哈希表,相信上过数据结构课程的同学应该还记得,哈希表是由数组+链表实现的——哈希表底层保存在一个数组中,数组的索引由哈希表的 key.hashCode() 经过计算得到, 数组的值是一个链表,所有哈希碰撞到相同索引的key-value,都会被链接到这个链表后面。
首先:为了触发比较操作,我们需要让比较与被比较的两个对象的哈希相同,这样才能被连接到同一条链表上,才会进行比较。
跟进下HashMap
的put
方法,变量 i
就是这个所谓的“哈希”。两个不同的对象的i
相等时,才会执行到key.equals(k)
触发前面说过的代码执行。
所以,我们接下来的目的就是为了让proxy
对象的“哈希”,等于TemplateImpl
对象的“哈希”。
实际上有师傅猜测jdk7u21
的作者frohoff
可能也是通过这样的思考最终找到了LinkedHashSet
类。
LinkedHashSet
位于 java.util
包内,是HashSet
的子类,向其添加到 set
的元素会保持有序状态,并且在LinkedHashSet.readObject()
的方法中,当各元素被放进HashMap
时,第二个元素会调用equals()
与第一个元素进行比较
4. 巧妙的Magic Number
计算“哈希”的主要是下面这两行代码:
1 | int hash = hash(key); |
将其中的关键逻辑提权出来,可以得到下面这个函数:
1 | public static int hash(Object key) { |
除了key.hashCode()
外再没有其他变量,所以proxy
对象与TemplateImpl
对象的“哈希”是否相等,仅取决于这两个对象的hashCode()
是否相等。TemplateImpl
的hashCode()
是一个Native
方法,每次运行都会发生变化,我们理论上是无法预测的,所以想让proxy
的hashCode()
与之相等,只能寄希望于proxy.hashCode()
。proxy.hashCode()
仍然会调用到AnnotationInvocationHandler#invoke
,进而调用到AnnotationInvocationHandler#hashCodeImpl
,我们看看这个方法:
1 | private int hashCodeImpl() { |
遍历memberValues
中只有一个key
和一个value
时,计算每个(127 * key.hashCode()) ^ value.hashCode()
并求和。JDK7u21
中使用了一个非常巧妙的方法:
当memberValues
中只有一个key
和一个value
时,该哈希简化成(127 * key.hashCode()) ^value.hashCode()
当key.hashCode()
等于0时,任何数异或0的结果仍是他本身,所以该哈希简化成 value.hashCode()
。
当value
就是TemplateImpl
对象时,这两个哈希就变成完全相等.
所以,我们找到一个hashCode
是0的对象作为memberValues
的key
,将恶意TemplateImpl
对象作为value
,这个proxy
计算的hashCode
就与TemplateImpl
对象本身的hashCode
相等了。
找一个hashCode
是0的对象,我们可以写一个简单的爆破程序来实现:
1 | public static void bruteHashCode() { |
跑出来第一个是f5a5a608
,这个也是ysoserial
中用到的字符串。
5. 利用链梳理
所以,整个利用的过程就清晰了,按照如下步骤来构造:
- 首先生成恶意
TemplateImpl
对象 - 实例化
AnnotationInvocationHandler
对象- 它的
type
属性是一个TemplateImpl
(为的是给equalsImpl
方法中的memberMethod
赋值) - 它的
memberValues
属性是一个Map
,Map
只有一个key
和value
,key
是字符串f5a5a608
,value
是前面生成的恶意TemplateImpl
对象
- 它的
- 对这个
AnnotationInvocationHandler
对象做一层代理,生成proxy对象 - 实例化一个
HashSet
,这个HashSet
有两个元素,分别是:TemplateImpl
对象proxy
对象
- 将
HashSet
对象进行序列化
这样,反序列化触发代码执行的流程如下:
- 触发
HashSet
的readObject
方法,其中使用HashMap
的key
做去重 - 去重时计算
HashSet
中的两个元素的hashCode()
,因为构造二者相等,进而触发equals()
方法 - 调用
AnnotationInvocationHandler#equalsImpl
方法 equalsImpl
中遍历this.type
的每个方法并调用- 因为
this.type
是TemplatesImpl
类,所以触发了newTransform()
或getOutputProperties()
方法 - 任意代码执行
其实简单来说就是AnnotationInvocationHandler#invoke
中能够调用AnnotationInvocationHandler#equalsImpl
方法,equalsImpl
能够遍历并调用this.type
的所有方法。
那么这里有三个问题:
- 怎么调用
AnnotationInvocationHandler#invoke
- 调用
invoke
后满足什么条件会调用AnnotationInvocationHandler#equalsImpl
- 调用
equalsImpl
后怎么rce
解答:
- 通过
proxy
代理,第三个参数设置为AnnotationInvocationHandler
,把AnnotationInvocationHandler
作为代理的逻辑。第二个参数是代理的对象集合,这里设置为Templates
类,具体原因参考解答3. - 调用
invoke
后,要满足是通过调用equal
方法触发的代理,进入的AnnotationInvocationHandler#invoke
,而且equal
方法只能有一个参数,参数类型为Object
- 当
this.type
是Templates
时,执行Templates
的所有方法,就会执行newTransform()
或getOutputProperties()
方法,实例化恶意类。(getOutputProperties()
方法在前,先执行)
简化的JDK7u21
代码:
1 | package com.govuln.deserialization; |
6. 修复
这个利用链俗名是JDK7u21
,可以认为它可以在7u21
及以前的版本中使用,这就牵扯出两个问题:
- 这个利用链是否影响
JDK6
和JDK8
,具体影响哪些小版本 JDK7u21
以上的版本如何修复这个问题
第一个问题,Java
的版本是多个分支同时开发的,并不意味着JDK7
的所有东西都一定比JDK6
新,所以,当看到这个利用链适配7u21
的时候,我们不能先入为主地认为JDK6
一定都受影响。Oracle JDK6
一共发布了30多个公开的版本,最后一个公开版本是6u45
,在2013年发布。此后,Oracle
公司就不再发布免费的更新了,但是付费用户仍然可以获得Java 6
的更新,最新的Java 6
版本是6u221
。
其中,公开版本的最新版6u45
仍然存在这条利用链,大概是6u51
的时候修复了这个漏洞,但是这个结论不能肯定,因为免费用户下载不到这个版本。JDK8
在发布时,JDK7
已经修复了这个问题,所以JDK8
全版本都不受影响。
我们来看看官方在JDK7u25
中是怎样修复这个问题:
在sun.reflect.annotation.AnnotationInvocationHandler
类的readObject
函数中,原本有一个对this.type
的检查,在其不是AnnotationType
的情况下,会抛出一个异常。但是,捕获到异常后没有做任何事情,只是将这个函数返回了,这样并不影响整个反序列化的执行过程。
新版中,将return;
修改成throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");
,这样,反序列化时会出现一个异常,导致整个过程停止.
后续版本如7u80
也有在 AnnotationInvocationHandler
构造方法的一开始的位置,就对于this.type
进行了校验。
1 | // 改之前: |
这个修复方式看起来击中要害,实际上仍然存在问题,这也导致后面的另一条原生利用链JDK8u20
。