【Java安全指北】1、序列化与反序列化杂谈
2022-9-6 17:42:24 Author: www.secpulse.com(查看原文) 阅读量:29 收藏

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

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

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

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

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

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

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

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

public interface Serializable {
}

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

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

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

  2. 通过其 writeObject方法写对象

image.png

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

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

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

image.png

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

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

使用工具: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的定义:

image.png

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

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

image.png

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

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

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

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

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

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

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

Person类加两个变量:

image.png

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

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

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

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

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


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

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

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

ObjectStreamClass有两个静态方法:

image.png

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

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

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

  • getDeclaredSUID:提取序列号

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

  • ……

image.png

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

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

image.png

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

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

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

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

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

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

image.png

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

image.png

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

这是《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

本文作者:白袍

本文为安全脉搏专栏作者发布,转载请注明:https://www.secpulse.com/archives/186847.html


文章来源: https://www.secpulse.com/archives/186847.html
如有侵权请联系:admin#unsafe.sh