做安全的有谁不喜欢这两串符号呢?
接上一篇 CommonsCollections 的分析,我们注意到有几个利用链只依赖于 CC 本身的版本,这种利用链是很香的,在实战中也相对容易成功。那有没有一种利用链,它一个库都不依赖,只要装了 Java 就能反序列化呢?这便是两个至今仍然强力的反序列化 Gadget —— 7u21
和 8u20
。他们非常罕见的只与 Java 版本有关的 Gadget,也就说在没有防护的状态下,只要 Java 版本符合这两个 Gadget 的要求并存在可以反序列化的点就可以直接 RCE,可以说是非常的 Amazing。这种利用链的挖掘难度非常大,整个利用过程环环相扣,各种小技巧令人咋舌,我读来感觉获益匪浅。今天我以一种鉴赏艺术的心情记录一下这两个利用链。
7u21
依赖
- JRE.main == 6 && JRE <= ?? (未调研,到某个版本就修了)
- JRE.main == 7 && JRE <= 7u21
利用链
|
|
关键点
为了方便理解,我将这个利用链拆成了两部分来看,第一部分:
|
|
当调用 proxy.equals(tpl)
时,由于 proxy 是个动态代理,实际调用的是 InvocationHandler 中的这段代码 invoke 函数,并将调用逐级传递下去最终调用了 TemplatesImpl.newTransformer
|
|
那么只要接下来能够触发上面的 proxy.equals
就可以完成整个利用链的构造了。在这里先补充一个小 trick,**f5a5a608 **和空字符串的 hashcode 都是 0:
|
|
接下来便是这个利用链的主角了 LinkedHashSet
,这个是基于 HashMap
封装的一个 有序 Set,这个有序是个关键点之一,需要保证反序列化时 hash add 的顺序固定。在 HashMap add 时,实际调用的是这段代码:
|
|
注意到这里有个 key.equals
,如果我们能设法将这里的 key 变为 proxy,k 变为 tpl,那么就能实现上面的 proxy.equals(tpl)
,稍加观察就会发现 for 循环里的其实就是碰撞处理的代码,只要 hash 值一样,那么就一定会碰撞,如何制造碰撞呢?这不得不佩服前辈们的睿智
|
|
将上面的代码稍微调整下,map 的 key 从 foo
改为 f5a5a608
,同时给 LinkedHashSet
加上了两个元素,这样一运行就可以触发前面的利用逻辑。我们刚说到制造 hash 一致的碰撞是关键,那这里的 tpl 和 proxy 为何会 hash 一致呢?
tpl 的 hashcode 调用的就是 Object.hashcode() 是 jvm 返回的,没有什么特殊逻辑。proxy 调用 hashcode 时,调用链是这样的
|
|
可以看到 AnnotationInvocationHandler
和 memberValues 所有的 key 和 value 有关,可以简化为:
|
|
如果想要这个结果是 tpl 的 hashcode,有个技巧就是让 k.hashcode()
为 0,v 是 tpl,而 f5a5a608
这个字符串的 hashcode 刚好为 0,这样一来,tpl 和 proxy 的 hashcode 就相同了,后面的流程就和 proxy.equals
一样了。
最后,map 的 readObject 时会进行 add 操作,这就将整个流程连起来了,我们自上而下梳理下关键点:
- map.readObject 触发 add(put) 操作
- 第一个元素是 tpl,hash 计算返回的的是
tpl.hashcode()
,由 jvm 动态计算生成 - 第二个元素是 proxy, hash 计算时调用 invoke 函数最终会根据
AnnotationInvocationHandler
的内置 map 的 k,v 去计算,如果我们让 k 是f5a5a608
,v 是 tpl,就可以让proxy.hashcode()
的返回值和tpl.hashcode()
一致 - map put 出现碰撞,调用 equals 函数去深度判断是否相等,最后调用了 invoke 函数,进而调用了
TemplateImpl.newTransformer()
完成利用
整条利用链一气呵成,非常优美。
修复方式
很快 JDK 在 Ju25 中就修了这个链,修复方式为:
|
|
这里在 defaultReadObject
之后对 this.type
做了一个类型判断,如果不是 AnnotationType
就会直接异常退出,我们传入的 type 是 Templates.class
,必然会异常,导致这条链就断裂了。如此神奇的利用链却只有少数几个 Java 版本可用着实令人惋惜,难道 7u21 的光辉就此而止了吗?
8u20
依赖
- JRE.main == 6 && JRE = ?? (未调研)
- JRE.main == 7 && JRE > 7u21 && JRE < ?? (未调研)
- JRE.main == 8 && JRE <= 8u20
利用链
核心逻辑与 7u21 几乎一致,仅多了异常处理的部分
关键点
我们观察针对 7u21 的修复,可以发现对 type
的判断发生在 defaultReadObject
之后,也就是说在判断时其实已经完成了反序列化,而这就是 8u20 这个链存在的基础,8u20 其实就是通过手动构造反序列化数据流绕过了这里的限制,关键点有两个:
- 如果有办法将 7u21 的 exception catch 住,就可以避免 exception 打断反序列化流程
- Java 反序列化流存在
TC_REFERENCE
这个字段,可以直接引用一个已经存在 Object 减少流数据冗余
Try Exception
先说第一个,如果存在一个类的 readObject 类似这样,我们就可以将 AnnotationInvocationHandler
设法放在 try 块内,就可以避免 exception 的向上抛出 :
|
|
8u20 用的是 BeanContextSupport
,这个类的 readObject 实现如下:
|
|
我们要做的就是按照其流程合理的设置一些字段的值使其能不报错的序列化完我们定义的 AnnotationInvocationHandler
即可。
TC_REFERENCE
Java 反序列化存在引用机制,避免我们重复写入完全相同的元素,比如这里的一个数组中两个元素指向的是同一个:
|
|
其对应的反序列化流为:
|
|
注意到第二个元素引用了第一个元素的 handle 0x00 7e 00 02
而没有再重写完整写入。handle 的值是从 0x7e0001 开始递增的,且不能向后引用,只能引用当前流状态中已经分配过的 handle 值。
Chain
我们将上面两条结合起来看,如果将 AnnotationInvocationHandler
合理的放置在 BeanContextSupport
中使其异常被处理掉,由于在异常出现之前反序列化实际已经完成,反序列化时已经被分配了一个有效的 handle 值可以被后续引用,那么后面就可以直接通过 TC_REFERENCE
来引用已经序列化好的 AnnotationInvocationHandler
从而可以继续走完 Jdk7u21 的利用链。
这个利用链难点不在原理,而在于需要手动构造反序列化流,因为这种流不是标准流,没法通过原生序列化操作直接生成。我也有注意到有大佬自行做了一个 SerialWriter,可以自己去组合流中的数据。在我看来这类工具适合在手工非常熟练之后再去用,如果对反序列化流的构成都不清楚,上来就用工具类只会无从下手,况且借助一些工具手写反序列化流真的不复杂,反而比较有趣。我这里不去展开如何一点点构造这条反序列化链,只说两个关键点:
- 通过为
classDescFlags
增加SC_WRITE_METHOD
可以在 ClassAnnotation 或 ObjectAnnotation 部分增加自定义数据,我自己写的利用链中BeanContextSupport
就是找了一个比较靠前的位置写在了 HashSet 的 ClassAnnotation 里。 - 虽然 exception 按预期会被 try 住,但内层的 readObject 实际没有正常结束,导致
java.io.ObjectInputStream#skipCustomData
没有被调用,结果就是流中会多一个TC_ENDBLOCKDATA
,生成的时候要把这个去掉。
我本地测试过的写法如下,测试过的环境包括: 6u_191、 8u20
|
|
修复方式
针对 8u20,我看到的有两种修复方式,分别是 jdk7 和 8 中的,其中 7 中增加了对 memberMethods 的验证:
|
|
validateAnnotationMethods
内对 methods 有一堆校验,利用流程是走不通的。也就是尽管实例化过程没报错,但后续触发利用的逻辑被拦了。而8中的修复方式相对更治标一些:
|
|
使用 readFields
替换了之前的 defaultReadObject
,这样一来就没有可以引用的东西了,8u20 自然无从谈起。
总结
总体来看 7u21 这条链始于 Hashset,终于 TemplatesImpl,由 AnnotationInvocationHandler
承上启下。8u20 是对 Jdk7u21 生命的延续,利用链完全一致,但通过手动构造对象引用,绕过了 exception 的限制。值得一提的是,8u20 最终的反序列化流可以变成和 7u21 仅一字(byte)之差,如果将我写的 8u20 的实现去掉里面 // TC_ENDBLOCKDATA,
处的注释,那么就可以完美的变成 7u21。差之毫厘谬以千里,大概就是这种感觉。不过这也意味着,8u20 并不是 7u21 的超集,就因为这一个字节的差异,两个 Gadget 支持的环境范围是不存在交集的。类似的,我将 8u20 这个利用链也加入了我的 ysoserial 中,可以比较方便的生成使用 https://github.com/zema1/ysoserial
我们回看一下 8u20 的修复方式,8中的修复方式没有什么可以突破的了。相比下对7的修复似乎没那么绝情。如果我们有办法控制 memberMethods
的内容,在反序列化时就设置好,这样就不会进入函数校验的函数。可惜这一项是 transient
修饰的,我们手动设置的属性不会生效。我觉得有一种理想情况是,找到一个类会在 readObject 时会通过反射设置一些属性,且该属性和属性的值都是我们可控的,这样就可以通过反射来填充 memberMethods
从而绕过限制,估计不太能找到了。