0x00 前言
千里之行,始于足下······Java反序列化基础篇的第一篇文章。
Java反序列化基础篇-01-反序列化概念与利用 | Drunkbaby’s Blog
0x01 基础概述
序列化与反序列化是什么
简单来说:
序列化:对象->特定格式的字符串
反序列化:特定格式的字符串->对象
序列化与反序列化的作用
序列化可以把对象转换成字符串数据,而反序列化可以把这些字符串数据重新转换为对象。
为什么要把对象转换成字符串数据呢?或者说,什么情况下对象需要转换成字符串数据呢?
在数据传输过程中,我们并不能直接传输对象,而是利用反序列化把对象转换成便于传输的字符串数据,当字符串数据传输到目的进程时,再利用反序列化把这些字符串数据转换成原先的对象。
又或者存储一些对象的时候,利用字符串便于存储的特点,可以存储对象数据到文件或者数据库中。
这里我们就难免要思考字符串数据的优势:
- 利用序列化可以实现远程通信,可以在网络上传输对象的字节序列。
- 可以实现数据的持久化存储,通过序列化可以把数据永久地存储在文件或者硬盘上。
序列化和反序列化的协议
- XML&SOAP
- JSON
- Protobuf
0x02 功能实现
序列化功能实现
SerializationTest.java:

解析:
Serialize方法接受一个类型为Obejct(对象)的参数,先打开ser.bin文件并创建文件输入流,再利用ObjectOutputStream方法把对象转换成字节流并利用其writeObject方法把序列化的字符串数据写入到ser.bin文件。
反序列化功能实现
Unserialization.java:

解析:
Unserialize方法则接受文件名参数,利用该参数打开文件流,并利用该文件流的readObject方法将文件里的序列化数据转换(反序列化)成对象。
小结
从宏观上看:Serializ方法接受一个对象,并将序列化后的字符串数据存储到特定文件。Unserialize方法则接受一个文件,将文件存储的字符串数据转换成特定对象。
0x03 注意事项
Serializable接口
(1)判断一个类能否进行序列化,需要看这个类是否具有Serializable接口或者 Externalizable 接口。
Serializable接口是 Java 内置的序列化接口。由于其是一个空接口,所以我们不需要实现什么。
1 | public interface Serializable { |
如果一个类没有实现Serializable接口,那么其对象在进行序列化时会报错:

image-20241105215845587.png

(2)在反序列化过程中,如果对象的父类没有实现 Serializable 接口,那么在反序列化时将需要提供无参数构造函数来重新创建该父类的实例。。这是因为在反序列化过程中,Java 需要通过调用父类的无参数构造函数来创建对象的完整结构。
(3)如果一个类实现了Serializable接口,那么其子类可以被序列化。
(4)静态成员变量不可以被序列化。这是因为序列化针对的是对象属性,而不是类的属性——静态成员变量是属于类的属性。
静态变量的值在所有实例中是共享的,因此在序列化一个对象时,不需要保存静态变量的状态。每次反序列化时,静态变量的值可以直接从类中获取,而不依赖于任何特定实例。这就意味着静态变量在序列化时不会被包含在序列化数据中。
(5)transient标识的对象不参与序列化
transient是 Java 中的一个关键字,用于标识类中的某个成员变量在序列化过程中应被忽略。具体来说,使用transient修饰的变量不会被序列化,这意味着它的值在对象被序列化时不会被保存到字节流中。
如下图:

序列化之前输出:
com.pax.UnserializeOne.Person{name=aa', age=22}
反序列化之后输出:
com.pax.UnserializeOne.Person{name=null', age=0}
0x04 漏洞原理
重要方法
序列化和反序列化需要特别关注两个方法:writeObject 和 readObject。
这两个方法往往会经过开发者的重写并在序列化和反序列化时被调用。尤其是反序列化时,会自动调用被反序列化对象的 readObject方法。
所以从根本上说,Java反序列化的漏洞往往离不开readObject方法。
漏洞形式
(1)入口类的 readObject 直接调用危险方法
给Person类写入readObejct方法:

当Person类的对象被反序列化时,该对象的readObejct方法被自动调用,会弹出计算器,如下:

但是开发者不是傻子,如此理想的情况在实际中几乎不可能遇到。
(2) 入口类参数中包含可控类,该类有危险方法,readObject 时调用
该方法会比(1)曲折一点,不能直接利用入口类,而是通过利用入口类的参数(可控类)的危险方法,但是无论怎样,还是需要一个触发条件——readObject方法。
(3)入口类参数中包含可控类,该类又调用其他有危险方法的类,readObject 时调用
这就需要把(2)的情况多次进行,但是最后的结果都是调用执行类的危险方法。
(4) 构造函数/静态代码块等类加载时隐式执行
还没遇到,学到再补。
攻击路线
需要注意三点:
- 入口类
- 调用链
- 执行类
以HashMap类为例,简单说明一下攻击思路:
首先HashMap类必须要能被序列化和反序列化,也就是要具有Serializable 接口,HashMap类是具有该接口的:

再分析HashMap类的readObject 方法:
注意该方法的这一段代码:

跟进hash(key):

如果key不为null,则执行key的hashCode方法。跟进hashCode方法,发现hashCode 位置处于 Object 类当中,也就是任何类都具有hashCode方法。
那么我们不妨想想,有没有一个类的对象,可以作为参数key,当HashMap被反序列化后,根据程序逻辑一路执行到该对象的hashCode方法,正好这个方法可以被我们利用?
存在这么个类:URL
URLDNS
概述
URLDNS严格来说不能算是一个利用链。无论是URL类接受的参数还是其结果,都非常受限:参数只能是一个URL链接,结果也只是发起一个DNS请求。
但是正如PHP里的phpinfo()函数一样,URLDNS在漏洞检测方法有很好的效果。URLDNS的优点如下:
URL类属于Java内置类,对第三方库没有依赖。
在⽬标没有回显的时候,能够通过 DNS 请求得知是否存在反序列化漏洞 URL 类,调用
openConnection方法,到此处的时候,其实openConnection不是常见函数,就已经难以利用了
匹配
为什么要叫匹配呢?这只是笔者的叫法,但是笔者认为比较形象。在URLDNS里,HashMap的参数key类被要求具有一个可以被利用的hashCode方法,正好URL类的hashCode方法可以匹配这个要求。
下面具体看看怎么利用URL类的hashCode方法
点开URL类的hashCode方法:

实际上执行的是handler的hashCode方法,handler又是抽象类URLStreamHandler的子类,所以查看URLStreamHandler的hashCode方法:

继续跟进:

红线标注的这一行代码发送了一个DNS请求。在实际测试中,常常通过发送DNS请验证来程序是否存在反序列化漏洞——这也是URLDNS最常用的功能。
URLDNS小结
HashMap被反序列化后在一定条件下会调用参数key的hashCode方法,我们选择URL类对象作为这个key,具体代码如下:
1 | HashMap<URL,Integer> hashmap= new HashMap<URL,Integer>(); |
如此,当hashmap反序列化时,就会收到DNS请求。
先进行序列化,等等,怎么收到了一个DNS请求?
问题
细看此处代码:


初始化的hashCode就是-1,所以直接就执行了hashCode方法,并且返回一个不为-1的hashCode属性,使之后的反序列化不能发送DNS请求。
为了成功实现URLDNS,我们要对此做出调整,这就涉及到Java的反射机制,下篇文章再详细介绍该机制。
0x05 结语
路漫漫其修远兮,吾将上下而求索。