• 本文作者: 
  • |
  • 2020年6月4日
  • |
  • 未分类
  • |

Java 8u20反序列化漏洞分析

一、前言

JDK7u21中反序列化漏洞修补方式是在AnnotationInvocationHandler类对type属性做了校验,原来的payload就会执行失败,在8u20中使用BeanContextSupport类对这个修补方式进行了绕过。

二、Java序列化过程及数据分析

在8u20的POC中需要直接操作序列化文件结构,需要对Java序列化数据写入过程、数据结构和数据格式有所了解。

先看一段代码

运行A类main方法会生成a.ser文件,以16进制的方式打开看下a.ser文件内容

 

跟下ObjectOutputStream类,来一步步分析下这些代码的含义

java.io.ObjectOutputStream#writeStreamHeader 写入头信息

image-20200602134009099

java.io.ObjectStreamConstants 看下具体值

image-20200602134135011

STREAM_MAGIC 16进制的aced固定值,是这个流的魔数写入在文件的开始位置,可以理解成标识符,程序根据这几个字节的内容就可以确定该文件的类型。

STREAM_VERSION 这个是流的版本号,当前版本号是5。

在看下out.writeObject(object)是怎么写入数据的,会先解析class结构,然后判断是否实现了Serializable接口,然后执行java.io.ObjectOutputStream#writeOrdinaryObject方法

image-20200602140603087

1426行写入TC_OBJECT,常量TC_OBJECT的值是(byte)0x73,1427行调用writeClassDesc方法,然后会调用到java.io.ObjectOutputStream#writeNonProxyDesc方法

image-20200602141317264 TC_CLASSDESC的值是(byte)0×72,在调用java.io.ObjectStreamClass#writeNonProxy方法。

image-20200602141617673

721行先写入对象的类名,然后写入serialVersionUID的值,看下java.io.ObjectStreamClass#getSerialVersionUID方法

image-20200602142011776

默认使用对象的serialVersionUID值,如果对象serialVersionUID的值为空则会计算出一个serialVersionUID的值。

接着调用out.writeByte(flags)写入classDescFlags,可以看见上面判断了如果是实现了serializable则取常量SC_SERIALIZABLE 的0×02值。然后调用out.writeShort(fields.length)写入成员的长度。在调用out.writeByteout.writeUTF方法写入属性的类型和名称。

然后调用bout.writeByte(TC_ENDBLOCKDATA)方法表示一个Java对象的描述结束。TC_ENDBLOCKDATA常量的值是(byte)0×78。在调用writeClassDesc(desc.getSuperDesc(), false)写入父类的结构信息。

接着调用writeSerialData(obj, desc)写入对象属性的值,调用java.io.ObjectOutputStream#writeSerialData

image-20200602154727162

可以看见slots变量的值是父类在前面,这里会先写入的是父类的值。

java.io.ObjectOutputStream#defaultWriteFields

image-20200602154248739

这里可以总结下,在序列化对象时,先序列化该对象类的信息和该类的成员属性,再序列化父类的类信息和成员属性,然后序列化对象数据信息时,先序列化父类的数据信息,再序列化子类的数据信息,两部分数据生成的顺序刚好相反。

分析Java序列化文件,使用SerializationDumper工具可以帮助我们理解,这里使用SerializationDumper查看这个序列化文件看下

 

三、漏洞分析及POC解读

8u20是基于7u21的绕过,不熟悉7u21的可以先看这篇文章了解下,看下7u21漏洞的修补方式。

sun.reflect.annotation.AnnotationInvocationHandler#readObject

 

AnnotationType.getInstance方法里对this.type类型有判断,需要是annotation类型,原payload里面是Templates类型,所以这里会抛出错误。可以看到在readObject方法里面,是先执行var1.defaultReadObject()还原了对象,然后在进行验证,不符合类型则抛出异常。漏洞作者找到java.beans.beancontext.BeanContextSupport类对这里进行了绕过。

看下BeanContextSupport类

 

 

可以看到在readChildren方法中,在执行ois.readObject()时,这里try catch了,但是没有把异常抛出来,程序会接着执行。如果这里可以把AnnotationInvocationHandler对象在BeanContextSupport类第二次writeObject的时候写入AnnotationInvocationHandler对象,这样反序列化时,即使AnnotationInvocationHandler对象 this.type的值为Templates类型也不会报错。

反序列化还有两点就是:

1.反序列化时类中没有这个成员,依然会对这个成员进行反序列化操作,但是会抛弃掉这个成员。

2.每一个新的对象都会分配一个newHandle的值,newHandle生成规则是从0x7e0000开始递增,如果后面出现相同的类型则会使用TC_REFERENCE结构,引用前面handle的值。

下面直接来看pwntester师傅提供的poc吧

 

这里直接构造序列化的文件结构和数据,可以看到注释分为6个步骤:

1.构造LinkedHashSet的结构信息

2.写入payload中TemplatesImpl对象

3.构造Templates Proxy的结构,这里定义了一个虚假的dummy成员,虚假成员也会进行反序列化操作,虽然会抛弃掉这个成员,但是也会生成一个newHandle的值。

4.这里为了BeanContextSupport对象反序列化时能走到readChildren方法那,需要设置serializable要>0并且父类 beanContextChildPeer成员的值为当前对象。BeanContextChildSupport对象已经出现过了,这里直接进行TC_REFERENCE引用对应的Handle

5.前面分析过在readChildren方法中会再次进行ois.readObject(),这里把payload里面的AnnotationInvocationHandler对象写入即可。这里try catch住了,并没有抛出异常,虽然dummy是假属性依然会进行反序列化操作,目的就是完成反序列化操作生成newHandle值,用于后面直接进行引用。

6.这里就是原JDK7u21里面的payload,把AnnotationInvocationHandler对象引用至前面的handle地址即可。

四、总结

JDK7u21和8u20这两个payload不依赖第三方的jar,只需要满足版本的JRE即可进行攻击,整条链也十分巧妙,在8u20中的几个trick也让我对Java序列化机制有了进一步的认识。

五、参考链接

https://github.com/pwntester/JRE8u20_RCE_Gadget

https://www.anquanke.com/post/id/87270

https://www.freebuf.com/vuls/176672.html

https://xz.aliyun.com/t/7240#toc-3

https://blog.csdn.net/silentbalanceyh/article/details/8183849

 

 

 

 

Written by