反序列化基础
推荐一个b站视频,讲的非常好
序列化与反序列化
1. 什么是序列化与反序列化
在PHP中也有反序列化漏洞,所以想必大家都对序列化有所了解。
这里便简单概括一下
序列化:对象 -> 字符串
反序列化:字符串 -> 对象
2. 序列化的好处
- 持久化存储
- 数据的网络传输
- 跨平台/跨语言交互
3. 序列化在JAVA中场景
(1) 想把内存中的对象保存到一个文件中或者是数据库当中。
(2) 用套接字在网络上传输对象。
(3) 通过 RMI 传输对象的时候。
4. 序列化的几种方式/协议
- XML&SOAP
- JSON
- Protobuf
- 当今 Java 原生当中的序列化与反序列化其实用的比较少吧,但是我们最开始讲起的话还是从原生开始讲起。
序列化与反序列化的代码实现
1. 代码展示
- Person.java
package src; // 修改成自己的 Package 路径
import java.io.Serializable;
public class Person implements Serializable {
private String name;
private int age;
public Person(){
}
// 构造函数
public Person(String name, int age){
this.name = name;
this.age = age;
}
@Override
public String toString(){
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
- 序列化文件 SerializationTest.java
package src;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
public 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
package src;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
public 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);
}
}
运行SerializationTest.java

输出的原始数据
运行UnserializeTest.java

反序列化回来的数据
我们可以看到反序列化后结果与最初一样。
2.代码讲解
首先是序列化时所用函数:
public static void serialize(Object obj) throws IOException{
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
创建了一个对象输出流的对象,在初始化的时候传递了一个文件输出流的对象当作参数。作用就是讲输出内容输出到ser.bin文件,然后调用oos的writeObject方法进行序列化。
然后是反序列化:
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
创建对象输入流,和文件输入流,读取指定文件内容,然后调用ois的readObject方法进行反序列化,并用Object类的引用接收反序列化的对象。
3. Serializable接口
还有一点需要注意的便是,在Person类的定义时有如下内容
implements Serializable
这是实现Serializable接口的意思,只有实现了这个接口这个类才能被序列化。
我们可以去掉后对比一下。

可以发现直接报错了说明不能序列化。
我们可以去接口的定义看看。

发现它其实并没有内容。这是一个规范,只有加了这个接口的实现我们才能进行序列化。
4. 注意事项
1. 静态成员变量是不能被序列化
2. transient 标识的对象成员变量不参与序列化
安全问题
1. 导引
其实序列化与反序列化直接关系到两个方法:
writeObject
readObject
这两个方法其实可以被开发者拿来重写:
举个例子,MyList 这个类定义了一个 arr 数组属性,初始化的数组长度为 100。在实际序列化时如果让 arr 属性参与序列化的话,那么长度为 100 的数组都会被序列化下来,但是我在数组中可能只存放 30 个数组而已,这明显是不可理的,所以这里就要自定义序列化过程啦,具体的做法是重写以下两个 private 方法:
private void writeObject(java.io.ObjectOutputStream s)throws java.io.IOException
private void readObject(java.io.ObjectInputStream s)throws java.io.IOException, ClassNotFoundException
只要服务端反序列化数据,客户端传递类的 readObject 中代码会自动执行,基于攻击者在服务器上运行代码的能力。
所以从根本上来说,Java 反序列化的漏洞的与 readObject 有关。
2. 可能存在的几种利用形式
(1)入口类的readObject直接调用危险方法。
这种情况是最理想下的场景,在实际开发场景中几乎不会出现。
举个例子:
我们在Person.java类中自定义readObject方法
import java.io.Serializable;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.io.FileInputStream;
import java.io.ObjectInputStream;
import java.lang.Runtime;
public class Person implements Serializable {
private String name;
private int age;
public Person(){
}
// 构造函数
public Person(String name, int age){
this.name = name;
this.age = age;
}
@Override
public String toString(){
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ois.defaultReadObject();
Runtime.getRuntime().exec("calc");
}
}
然后再运行一遍序列化与反序列化,便会发现成功弹出了计算器。

这是黑客最理想的情况,但是这种情况几乎不会出现。
(2)入口参数中包含可控类,该类有危险方法,readObject 时调用
(3)入口类参数中包含可控类,该类又调用其他有危险方法的类,readObject 时调用
3. 一般的攻击步骤
首先的攻击前提:继承 Serializable
入口类:source (重写 readObject 调用常见的函数;参数类型宽泛,最好是可以传入Object类作为参数;最好 jdk 自带)
找到入口类之后要找调用链 gadget chain 相同名称、相同类型
执行类 sink (RCE SSRF 写文件等等)比如 exec 这种函数
4. 找到入门类(以hashMap为例子)
以hashMap为例子,带大家过一遍什么样的类才可能被利用。
首要前提便是继承了Serializable接口,不然没法进行序列化/
我们看一下hashMap的实现类,发现确实继承了Serializable接口

然后便是要参数类型广泛,我们可以看到K,V可以传任意的类所以自然满足。
再接下来便是重写readObject方法。
我们打开Structure(我的idea里这个在左下角侧边栏),找到readObject方法。

发现确实是有自己重写的readOject方法。
还有一点很重要便是调用常见的函数,我们看看readObject方法中是怎么实现的。

我们看到第 1416 行与 1418 行中,Key 与 Value 的值执行了 readObject 的操作,再将 Key 和 Value 两个变量扔进 hash 这个方法里,我们再跟进(ctrl+鼠标左键即可) hash 当中。

发现再hash方法中又调用了hashCode函数,我们接着跟进。

发现这个hashCode方法是Object类里自带的,也就是所有的类都有的方法。这就满足我们调用常见的函数这一条件。所以hashMap便是一个很好的入门类。
至于后面的如何找链子,以及如何利用我会以实战的方式来讲解。
比如接下来要讲的便是第一个经典的链子——URLDNS链。其利用的便是反射与反序列化的内容。