【Java安全指北】1、序列化与反序列化杂谈

语言: CN / TW / HK

0x1 前言

在拖延了几天之后,这个系列终于算正式开启了,Java安全目前的热门程度可以说是红队必知必会,我曾一度陷入 开始学Java安全-学CC链-放弃-再次开始学习Java安全 的怪圈,好比背单词始终都停在 abandon 一样,最终在看到scz前辈的一篇博客后,痛下决心,我把他的这段话摘抄在下面,希望也能给你信心:

人一般只会吹嘘自己并不擅长的东西,因为TA并不真正了解而懂得深浅进而缺乏敬畏。你见我吹嘘过SMB吗?但我经常在微博上晒英语学习打卡记录。

初始接触Java反序列化,是2019年11月,猝不及防遭遇CVE-2019-6980(Zimbra),狠狠打击了我。决定好好学学这个方向。

我是带着一种我花开罢百花杀的神经病思维开始的,当然,我们都知道,黄巢他死了。只当是个笑话。

展示一下6个月来Java反序列化学习之路,其中不少章节已在个人主页不同篇章中分享过。

如果有新手想入门Java反序列化,不妨也这样神经病一遭,或许真就入门了呢?

0x2 基本使用

关于序列化和反序列化的概念不再介绍,直接来看使用。

要使某个类可以被序列化,它需要实现 SerializableExternalizable 接口,前者是一个空接口:

public interface Serializable {
}

它只用来标识此类可被序列化,后者继承前者。

一个Java对象的序列化的步骤:

  1. 创建一个  java.io.ObjectOutputStream ,可以包装一个其他类型的输出流

  2. 通过其  writeObject 方法写对象

一个Java对象的反序列化的步骤:

  1. 创建一个  java.io.ObjectInputStream ,可以包装一个其他类型的输入流

  2. 通过其  readObject 方法读取对象

0x3 序列化数据格式

将序列化数据写入文件,来观察一下:

可以看到其中存在一些可读的字符串,包含了类名以及一部分成员变量名和值。

使用工具: SerializationDumper [1] ,可以方便地还原序列化数据,比如对原始流文件:

对16进制数据:

完整输入:

对照Oracle官方文档中 Object Serialization Stream Protocol [2] 部分,我们来讨论以下序列化数据的结构。

最开头的地方是Magic Number以及协议版本,在 ObjectStreamConstants 接口中可以看到定义:

接着是 contents ,即一个或多个 content ,而后者由 objectblockdata 组成。

object 里的内容是序列化数据的核心,由下面的任一内容组成:

  • newObject :对象

  • newClass :类

  • newArray :数组

  • newString :字符串

  • newEnum :枚举类型

  • newClassDesc :类定义

  • prevObject :指向某个类型的引用

  • nullReference :null

  • exception :异常

  • TC_RESET :重置  ReferenceID

参看文档中 newObject 的定义:

我们用上面的例子来对照, Contents 里包含了一个 newObject ,在其标识符 TC_OBJECT 之后就是 classDesc ,其中包含了类名及长度、 serialVersionUID 、属性名和长度、父类等信息。在这之后是 newHandle 以及 classdata[] ,前者即序列化数据中当前结构的唯一ID,后者是被序列化的对象中的信息。

newClassDescclassDesc 并不相同,从定义上就可以看出:

classDesc 相当于 newClassDesc 的封装,它可以是一个 newClassDesc ,也可以是一个null或者指向类定义的指针。

到这里,你也许会疑惑,为什么我要看这篇枯燥的文档?怎么还不开始讲CC链?

需要详细了解序列化数据结构的原因有三:

  1. 有助于理解 在序列化数据里填充垃圾字符绕过WAF 这种姿势的原理

  2. 有助于后面学习JDK 8u20原生反序列化漏洞

  3. 也许你会遇到需要用其他语言来完成Java反序列化漏洞利用的情况

0x4 属性的影响

PHP序列化时,变量的作用域会影响到序列化数据,那么Java中是否同样存在类似的情况?

Person 类加两个变量:

之后观察序列化数据,发现这两个变量都不存在:

statictransient 关键字修饰的变量不会出现在序列化数据里,这是为了一些敏感数据考虑的。

但如果尝试在反序列化后调用这两个变量,可以看到 address 正常输出,而 password 为null:

这是因为 address 是静态变量,调用的是其在JVM中注册的值,而不是序列化后得到的值。

如果想序列化被 transient 关键字修饰的变量,就需要用到 Externalizable 接口:

这里的 test.raw 如果用 SerializationDumper 来解析,就会出现下面的错误:

原因是实现了 Externalizable 接口的类,其序列化通过 writeExternal 方法写入流,那么解析也必须通过相应的 readExternal 方法,所以在不提供原始类的情况下, SerializationDumper 无法解析这样的序列化数据。

0x5 ObjectStreamClass分析

ObjectStreamClass 可以用来分析JVM中加载的序列化类的序列化特征,包括字段描述信息以及 serialVersionUID 等。

ObjectStreamClass 有两个静态方法:

lookup(Class<?>cl) 在提供的类可序列化的情况下会返回 ObjectStreamClass 实例,否则返回null:

lookupAny(Class<?>cl) 方法不管提供的类是否可反序列化,都会返回相应实例。

获取到 ObjectStreamClass 实例后就可以调用相应方法获取信息:

  • getDeclaredSUID :提取序列号

  • getSerialFields :提取需要的序列化字段,如无则提取默认字段

  • ……

0x6 关于ObjectInputStream.resolveClass()

resolveClass 方法接收一个 ObjectStreamClass 实例,获取其类名,再利用反射的方式返回一个此类的 Class 实例,实际上就是允许在反序列化中,返回对象之前进行替换或解析对象。

在Apache Shiro中对此方法进行了重写:

这导致在Shiro反序列化漏洞利用时会出现一些有趣的情况,具体在之后的Shiro篇会详细阐述。

重写该方法也是防御反序列化漏洞的一种手段,例如 SerialKiller [3] 这个项目:

通过重写 ObjectInputStream.resolveClass() 来进行黑名单或白名单方式的防御。

0x7 readObjectNoData

在反序列化时,如果因为序列化时的类与反序列化时版本不同,造成序列化类的超类与反序列化类的超类不同,或因为接收到的序列化数据不完整,或序列化数据有危害,都会对初始化对象字段值造成影响。

所以可序列化的类应定义自己的 readObjectNoData 方法,在出现上述情况时就会用 readObjectNoData 替代 readObject 。如果没有此方法,类的字段就会初始化为它们的默认值。

举个例子,使用现在的类进行序列化:

更新一下这个类,再利用之前的序列化数据进行反序列化:

这时就会调用 readObjectNoData() 来替代 readObject() .

0x8 小结

这是《Java安全指北》的第一篇文章,一些地方其实写的有些刻奇了,并不一定对Java安全学习有帮助,不过多了解一些总没错,对吧:)

参考链接: phith0n《Java安全漫谈》

References

[1] SerializationDumper:  https://github.com/NickstaDB/SerializationDumper

[2] Object Serialization Stream Protocol:  https://docs.oracle.com/javase/8/docs/platform/serialization/spec/protocol.html

[3] SerialKiller:  https://github.com/ikkisoft/SerialKiller

本文作者:白袍