序列化与反序列化简介
序列化是将对象的状态信息转化为可以存储或者传输的形式的过程,一般将一个对象存储到一个存储媒介,在网络传输过程中,可以是字节或者是XML等格式,而字节或者XML格式的可以还原成完全相等的对象,这个过程称之为反序列化。
java提供了一种对象序列化的机制,在这种机制下,一个对象可以被表示为一个字节序列,该字节序列包括该对象的数据、有关对象的类型的信息和存储在对象中数据的类型。
将序列化对象写入文件之后,可以从文件中读取出来,并且对它进行反序列化。类 ObjectInputStream 和 ObjectOutputStream是高层次的数据流,它们包含反序列化和序列化对象的方法。
其中ObjectOutputStream类包含很多写方法来写各种数据类型,除了writeObject方法,writeObject方法用于序列化一个对象,并将它发送到输出流。相反对于ObjectOutputStream类中包含一个readObject方法,该方法从流中取出下一个对象,并将对象反序列化,返回值为Object。
总的来说可以总结为在java中,序列化与反序列化的处理需要以下三步
ObjectOutputStream类中的writeObject方法用来处理需要序列化的对象。ObjectInputStream类中的readObject方法用来处理反序列化。- 被序列化的类要实现
java.io.Serializable接口。
序列化及反序列化相关接口及类如下
1 | java.io.Serializable |
实验环境
- java环境:jdk1.8.0_191
- 电脑系统:macOS Big Sur 11.4
- IDE:IntelliJ IDEA
安全问题
只要服务端反序列化数据,客户端传递类的readObject中代码会自动执行,就会给攻击者在服务器上运行代码的能力。
问题1
入口类的readObject直接调用危险方法。
定义一个Person类,并在类中重写readObject方法。
1 | import java.io.Externalizable; |
首先执行序列化代码,其中ObjectOutputStream是对象的序列化流,它的作用是把对象转成字节数据之后输出到文件中保存,对象的输出过程称之为序列化,可以实现对象的持久存储,FileOutputStream被称为文件字节输出流,将输入的内容写入ser.bin文件中,之后通过writeObject来处理对象的序列化。代码如下
1 | import java.io.*; |
在java反序列化传送的包中,一般有两种传送方式,通过这两种方式的流量分析则可判断是否存在java反序列化
- TCP:在16进制流中存在
ac ed 00 05 - HTTP:base64编码之后存在
rO0AB

之后执行反序列化代码,其中ObjectInputStream是反序列化流,目的是将之前使用 ObjectOutputStream序列化的原始数据恢复为对象,以流的方式读取对象。 FileInputStream被称为文件字节输入流,读取ser.bin中的内容,之后通过readObject实现对象的反序列化。代码如下:
1 | import java.io.FileInputStream; |
由于我们传入了一个Person类,并在Person类中重写了readObject方法,因此在反序列化的时候就会执行person类中重写的readObject代码,从而达到命令执行的效果。

URLDNS反序列化漏洞学习
URLDNS是反序列化时经常会用到的链,通常用于快速检测是否存在反序列化漏洞。当我们想确认服务器是否存在反序列化时,可以通过URL dns解析,如果被解析的话,则可以判断该服务器存在反序列化。在学习该漏洞之前,需要了解一些java反射相关知识。
Java反射
一般情况下,当我们使用某一个类的时候,我们一定是清楚这个类的作用,因此才会对这个类进行实例化。之后使用实例化之后的类对象进行操作。这样的操作可以理解为是“正射”。
Java的反射(reflection)机制是指在程序的运行状态中,可以构造任意一个类的对象,可以了解任意一个对象所属的类,可以了解任意一个类的成员变量和方法,可以调用任意一个对象的属性和方法。这种动态获取程序信息以及动态调用对象的功能称为Java语言的反射机制。
java反射举例
一个简单的反射例子
1 | import java.lang.reflect.Constructor; |
java反射API

URLDNS链路分析
https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/URLDNS.java
1 | Gadget Chain: |
这里选择HashMap作为入口类的原因是因为HashMap已经继承了Serializable,并且HashMap的类型较为宽泛。
首先查看HashMap的readObject方法,发现在最后读取了key和value,对key调用了hash函数,重新计算key的哈希值。


跟进hash函数,发现当Object类型的key不为空时,就会调用key的hashCode函数。

Java中与HTTP相关的是URL类,通过查看URL类发现继承了java.io.Serializable接口,故而考虑是否可以反序列化。

正常发起请求是通过URLConnection类中的openConnection方法实现,通过分析发现在openConnection之后的执行过程中并不存在反序列化,因此可以从一个最常见的函数hashCode函数开始。在hashCode函数中首先会判断hashCode是否等于-1,如果不等于就返回。如果等于-1就会调用handler的hashCode函数。

跟进handler的hashCode函数,发现该函数做了一个域名解析的工作,这样可以得到一个DNS请求,从而帮助我们验证是否存在漏洞。

因此总结下来的调用链就是
1 | HashMap.readObject() |
实验
首先用Burp生成一个接收DNS请求的URL


构造序列化hashmap代码
1 | import java.io.*; |
正常情况下在序列化的过程中应该什么也不会发生,但是我们发现实际上在序列化的过程中已经接收到了请求。

分析发现当调用hashmap.put的时候,为了保证键的唯一,会调用hash方法,从而执行了hashCode方法。因此在序列化之前,就已经发起了DNS请求。因为这里的key是URL,而在URL的hashCode函数中,hashCode变量的初始值是-1,并且是私有属性。因此会执行handler.hashCode操作,进而发起DNS请求。DNS请求结束之后,hashCode变量的值已经是url经过哈希之后的值,所以这个值肯定不是-1,所以对于序列化之后的反序列化操作并没有用。



总之,这个序列化操作是在hashCode不等于-1的状态下进行的,因此在后面反序列化的时候也不会执行相关命令。
这里我们在执行反序列化操作之后发现并没有收到请求。

为了达到攻击效果,我们要做的就是在hashmap.put之前将hashcode的值改为不是-1,这样就会直接返回hashcode从而不会发起DNS请求,在序列化之前将hashcode的值改为-1,这样就会执行后续的handler.hashCode操作。从而在反序列化的时候重新计算hashcode的值,进而发起DNS请求,因此我们可以使用反射的方法来改变已有对象的属性。
序列化代码如下,执行之后发现并没有收到DNS请求。
1 | import java.io.*; |
反序列化代码如下
1 | import java.io.FileInputStream; |
执行反序列化代码之后,收到DNS请求。

debug调试反序列化的代码发现hashcode的值已经修改成功,这样就验证了URLDNS反序列化漏洞。

反序列化漏洞如何防范
类白名单校验
在
ObjectInputStream类中的resolveClass方法中只是进行了class是否能被加载,因此可以自定义ObjectInputStream,之后重载resolveClass方法,对className进行白名单校验。禁止JVM执行外部命令
Runtime.exec通过扩展
SecurityManager实现
参考资料
- https://www.runoob.com/java/java-serialization.html
- https://www.liaoxuefeng.com/wiki/1252599548343744/1298366845681698
- https://www.codemonster.cn/2019/01/24/java-serialize-vuln0/
- https://m0nit0r.top/2020/06/04/java-deserialize-learn1/
- https://www.bilibili.com/video/BV16h411z7o9?p=2&spm_id_from=pageDriver
- https://www.freebuf.com/articles/web/275842.html