序列化与反序列化简介
序列化是将对象的状态信息转化为可以存储或者传输的形式的过程,一般将一个对象存储到一个存储媒介,在网络传输过程中,可以是字节或者是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