JDK8u20反序列化浅析
jdk8u20是对jdk7u21的绕过。
jdk7u21的修复方式
回顾一下jdk7u21的修复方式
首先是对AnnotationType的构造方法中对参数类型进行了判断。原先是在不是AnnotationType的情况下,会抛出一个异常。但是,捕获到异常后没有做任何事情,只是将这个函数返回了,这样并不影响整个反序列化的执行过程。
新版中,将return;修改成throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");,这样,反序列化时会出现一个异常,导致整个过程停止。这样的话整个反序列化链也被中断了。

后续版本如7u80也有在AnnotationInvocationHandler构造方法的一开始的位置,就对于this.type进行了校验。
1 | // 改之前: |
jdk8u20绕过思路
0x01 try-catch嵌套
对于以下代码
1 | public static void main(String[] args) throws Exception { |
运行结果如下:
1 | Start |
这里涉及到java的异常捕捉机制,最里层的try-catch会抛出异常的会被最外层的catch不再向外抛出异常而是忽略,这样的话程序会继续执行。
同理,我们可以找到类似的函数来绕过jdk7u21的修复。
现在我们需要寻找到一个类,满足以下条件:
- 实现
Serializable - 重写了
readObject方法 readObject方法还存在对readObject的调用,并且对调用的readObject方法进行了异常捕获并继续执行
0x02 BeanContextSupport
最终我们找到了想要的类: java.beans.beancontext.BeanContextSupport,其关键代码如下:
1 | private synchronized void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { |
1 | public final void readChildren(ObjectInputStream ois) throws IOException, ClassNotFoundException { |
0x03 序列化机制
引用机制
在序列化流程中,对象所属类、对象成员属性等数据都会被使用固定的语法写入到序列化数据,并且会被特定的方法读取;在序列化数据中,存在的对象有null、new objects、classes、arrays、strings、back references等,这些对象在序列化结构中都有对应的描述信息,并且每一个写入字节流的对象都会被赋予引用Handle,并且这个引用Handle可以反向引用该对象(使用TC_REFERENCE结构,引用前面handle的值),引用Handle会从0x7E0000开始进行顺序赋值并且自动自增,一旦字节流发生了重置则该引用Handle会重新从0x7E0000开始。
举一个简单的例子如下:
1 | import java.io.*; |
我们用SerializationDumper这个工具来查看其序列化后的数据:
1 | STREAM_MAGIC - 0xac ed |
可以注意到在最后部分出现了TC_REFERENCE块,那么反序列化时要如何处理TC_REFERENCE块呢?
1 | private Object readObject0(boolean unshared) throws IOException { |
readHandle方法代码如下:
1 | private Object readHandle(boolean unshared) throws IOException { |
这个方法会从字节流中读取TC_REFERENCE标记段,它会把读取的引用Handle赋值给passHandle变量,然后传入lookupObject(),在lookupObject()方法中,如果引用的handle不为空、没有关联的ClassNotFoundException(status[handle] != STATUS_EXCEPTION),那么就返回给定handle的引用对象,最后由readHandle方法返回给对象。
也就是说,反序列化流程还原到TC_REFERENCE的时候,会尝试还原引用的handle对象。
成员抛弃机制
在反序列化中,如果当前这个对象中的某个字段并没有在字节流中出现,则这些字段会使用类中定义的默认值,如果这个值出现在字节流中,但是并不属于对象,则抛弃该值,但是如果这个值是一个对象的话,那么会为这个值分配一个 Handle。
利用这些机制
了解了上面2个机制之后我们就可以想到一个绕过jdk7u21修复方法的机制,即:
- 使用
BeanContextSupport,利用其特殊的readChildren方法还原一个非法的AnnotationInvocationHandler对象,并留下一个Handle - 在我们
HashObject发生哈希碰撞造成RCE之前还原非法的AnnotationInvocationHandler对象,使得之后反序列化相同对象时还原引用的handle对象
exp1
这里介绍一下沈沉舟师傅的exp
详细的可以参考https://mp.weixin.qq.com/s/3bJ668GVb39nT0NDVD-3IA
对于getObject()方法中插入的三个元素,我的理解如下:add(bcs):bcs是封装有AnnotationInvocationHandler的BeanContextSupport实例;
先存放bcs,反序列化时会先反序列化bcs,然后进入bcs的readChildren运行AnnotationInvocationHandler的readObject,抛出异常,但被bcs的readChildren的catch忽略,代码继续执行
虽然没有反序列化成功,但留下恶意AnnotationInvocationHandler的Handleadd(TemplatesProxy):在hm.put("0DE2FF10", ti);后,会还原引用的handle对象
因为反序列化相同对象时还原引用的handle对象,之前已经留下恶意AnnotationInvocationHandler的Handle,所以会引用这个handle,触发TemplatesImpl.getOutputProperties,执行恶意类
而且一定要让add(TemplatesProxy)在add(ti)后面,这样才能让TemplatesProxy触发equals()
断点设置:
1 | JDK8u20Exec.main -> ois.readObject(); |
通过以上断点可以清晰看出执行流程。
对于PrivatePatch方法中字节的调换,这里引用其他师傅的理解:
将 sun.reflect.annotation.nAnnotationInvocationHandler 的 classDescFlags 由 SC_SERIALIZABLE 修改为 SC_SERIALIZABLE | SC_WRITE_METHOD
这一步其实不是通过理论推算出来的,是通过debug以及查看 pwntester的 poc 发现需要这么改
原因是如果不设置 SC_WRITE_METHOD 标志的话 defaultDataEd = true,
导致 BeanContextSupport -> deserialize(ois, bcmListeners = new ArrayList(1)) -> count = ois.readInt(); 报错,无法完成整个反序列化流程
没有 SC_WRITE_METHOD 标记,认为这个反序列流到此就结束了
标记: 7375 6e2e 7265 666c 6563 –> sun.reflect...
运行到BeanContextSupport.deserialize()中此时ois.readInt()时面对的序列化数据可能是个TC_OBJECT或者TC_NULL
为了让攻击顺利,必须让ois.readInt()面对的序列化数据是TC_BLOCKDATA包裹的整型变量00x70, 0x77, 0x04, 0x00, 0x00, 0x00, 0x00, 0x78 -> 0x77, 0x04, 0x00, 0x00, 0x00, 0x00, 0x70, 0x78
1 | TC_NULL - 0x70 // old |
1 | 【77】TC_BLOCKDATA:可选的数据块,参考后边的章节就知道,所有基础类型数据的序列化都会使用数据块的结构; |
1 | package com.alter.serialize.ysoserial.payload.jdk8u20.scz; |
exp2
这是1nhann师傅的exp。生成的ser文件需要在另一个文件中反序列化,才能执行成功。
我认为其实原理和沈沉舟师傅的原理一样,只是沈沉舟师傅把TC_NULL - 0x70移到了后面;1nhann师傅把TC_NULL - 0x70以及前面的0x78删掉了。
但是沈沉舟师傅是0x78,0x70,0x70...; 1nhann师傅是0x78,0x70,0x78,0x70...经过调试,发现可能是(不确定,后面系统的看一下java8的序列化规范)newInvocationHandlerClass中给AnnotationInvocationHandler添加writeObject方法造成的。
沈沉舟师傅的paylad删除对应字节的0x70一样能成功
1 | package com.alter.serialize.ysoserial.payload.jdk8u20.inhann; |
jdk8u20的修复方式
jdk8u25
jdk8u25的时候给 getMemberMethods() 中加了一段:
1 | validateAnnotationMethods(var1); |
定义了一个 validateAnnotationMethods(),反序列化的时候 this.type 所调用的方法做了限制。
代码如下:
1 | /** |
8u72-b12
从8u72-b12开始,AnnotationInvocationHandler.readObject()中不再调用s.defaultReadObject(),转而调用s.readFields()。反序列化流程到达443行时,并没有产生完整的AnnotationInvocationHandler实例,即使利用BeanContextSupport捕获异常也不会改变这点,利用链被破坏。
