在拖延了几天之后,这个系列终于算正式开启了,Java安全目前的热门程度可以说是红队必知必会,我曾一度陷入开始学Java安全-学CC链-放弃-再次开始学习Java安全的怪圈,好比背单词始终都停在abandon一样,最终在看到scz前辈的一篇博客后,痛下决心,我把他的这段话摘抄在下面,希望也能给你信心:
人一般只会吹嘘自己并不擅长的东西,因为TA并不真正了解而懂得深浅进而缺乏敬畏。你见我吹嘘过SMB吗?但我经常在微博上晒英语学习打卡记录。
初始接触Java反序列化,是2019年11月,猝不及防遭遇CVE-2019-6980(Zimbra),狠狠打击了我。决定好好学学这个方向。
我是带着一种我花开罢百花杀的神经病思维开始的,当然,我们都知道,黄巢他死了。只当是个笑话。
展示一下6个月来Java反序列化学习之路,其中不少章节已在个人主页不同篇章中分享过。
如果有新手想入门Java反序列化,不妨也这样神经病一遭,或许真就入门了呢?
关于序列化和反序列化的概念不再介绍,直接来看使用。
要使某个类可以被序列化,它需要实现 Serializable
和 Externalizable
接口,前者是一个空接口:
public interface Serializable { }
它只用来标识此类可被序列化,后者继承前者。
一个Java对象的序列化的步骤:
创建一个 java.io.ObjectOutputStream
,可以包装一个其他类型的输出流
通过其 writeObject
方法写对象
一个Java对象的反序列化的步骤:
创建一个 java.io.ObjectInputStream
,可以包装一个其他类型的输入流
通过其 readObject
方法读取对象
将序列化数据写入文件,来观察一下:
可以看到其中存在一些可读的字符串,包含了类名以及一部分成员变量名和值。
使用工具:SerializationDumper[1],可以方便地还原序列化数据,比如对原始流文件:
对16进制数据:
完整输入:
对照Oracle官方文档中Object Serialization Stream Protocol[2]部分,我们来讨论以下序列化数据的结构。
最开头的地方是Magic Number以及协议版本,在 ObjectStreamConstants
接口中可以看到定义:
接着是 contents
,即一个或多个 content
,而后者由 object
或 blockdata
组成。
object
里的内容是序列化数据的核心,由下面的任一内容组成:
newObject
:对象
newClass
:类
newArray
:数组
newString:字符串
newEnum
:枚举类型
newClassDesc
:类定义
prevObject
:指向某个类型的引用
nullReference
:null
exception
:异常
TC_RESET
:重置 ReferenceID
参看文档中 newObject
的定义:
我们用上面的例子来对照, Contents
里包含了一个 newObject
,在其标识符 TC_OBJECT
之后就是 classDesc
,其中包含了类名及长度、 serialVersionUID
、属性名和长度、父类等信息。在这之后是 newHandle
以及 classdata[]
,前者即序列化数据中当前结构的唯一ID,后者是被序列化的对象中的信息。
newClassDesc
和 classDesc
并不相同,从定义上就可以看出:
classDesc
相当于 newClassDesc
的封装,它可以是一个 newClassDesc
,也可以是一个null或者指向类定义的指针。
到这里,你也许会疑惑,为什么我要看这篇枯燥的文档?怎么还不开始讲CC链?
需要详细了解序列化数据结构的原因有三:
有助于理解在序列化数据里填充垃圾字符绕过WAF这种姿势的原理
有助于后面学习JDK 8u20原生反序列化漏洞
也许你会遇到需要用其他语言来完成Java反序列化漏洞利用的情况
PHP序列化时,变量的作用域会影响到序列化数据,那么Java中是否同样存在类似的情况?
给 Person
类加两个变量:
之后观察序列化数据,发现这两个变量都不存在:
被 static
或 transient
关键字修饰的变量不会出现在序列化数据里,这是为了一些敏感数据考虑的。
但如果尝试在反序列化后调用这两个变量,可以看到 address
正常输出,而 password
为null:
这是因为 address
是静态变量,调用的是其在JVM中注册的值,而不是序列化后得到的值。
如果想序列化被 transient
关键字修饰的变量,就需要用到 Externalizable
接口:
这里的 test.raw
如果用 SerializationDumper
来解析,就会出现下面的错误:
原因是实现了 Externalizable
接口的类,其序列化通过 writeExternal
方法写入流,那么解析也必须通过相应的 readExternal
方法,所以在不提供原始类的情况下, SerializationDumper
无法解析这样的序列化数据。
ObjectStreamClass
可以用来分析JVM中加载的序列化类的序列化特征,包括字段描述信息以及 serialVersionUID
等。
ObjectStreamClass
有两个静态方法:
lookup(Class<?>cl)
在提供的类可序列化的情况下会返回 ObjectStreamClass
实例,否则返回null:
而 lookupAny(Class<?>cl)
方法不管提供的类是否可反序列化,都会返回相应实例。
获取到 ObjectStreamClass
实例后就可以调用相应方法获取信息:
getDeclaredSUID
:提取序列号
getSerialFields
:提取需要的序列化字段,如无则提取默认字段
……
resolveClass
方法接收一个 ObjectStreamClass
实例,获取其类名,再利用反射的方式返回一个此类的 Class
实例,实际上就是允许在反序列化中,返回对象之前进行替换或解析对象。
在Apache Shiro中对此方法进行了重写:
这导致在Shiro反序列化漏洞利用时会出现一些有趣的情况,具体在之后的Shiro篇会详细阐述。
重写该方法也是防御反序列化漏洞的一种手段,例如SerialKiller[3]这个项目:
通过重写 ObjectInputStream.resolveClass()
来进行黑名单或白名单方式的防御。
在反序列化时,如果因为序列化时的类与反序列化时版本不同,造成序列化类的超类与反序列化类的超类不同,或因为接收到的序列化数据不完整,或序列化数据有危害,都会对初始化对象字段值造成影响。
所以可序列化的类应定义自己的 readObjectNoData
方法,在出现上述情况时就会用 readObjectNoData
替代 readObject
。如果没有此方法,类的字段就会初始化为它们的默认值。
举个例子,使用现在的类进行序列化:
更新一下这个类,再利用之前的序列化数据进行反序列化:
这时就会调用 readObjectNoData()
来替代 readObject()
.
这是《Java安全指北》的第一篇文章,一些地方其实写的有些刻奇了,并不一定对Java安全学习有帮助,不过多了解一些总没错,对吧:)
参考链接:phith0n《Java安全漫谈》
[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