Java反序列化基础篇-01-反序列化概念与利用

0x01 序列化与反序列化

1. 什么是序列化与反序列化

序列化:对象 -> 字符串
反序列化:字符串 -> 对象

2. 为什么我们需要序列化与反序列化

序列化与反序列化的设计就是用来传输数据的。

当两个进程进行通信的时候,可以通过序列化反序列化来进行传输。

序列化的好处

(1) 能够实现数据的持久化,通过序列化可以把数据永久的保存在硬盘上,也可以理解为通过序列化将数据保存在文件中。

(2) 利用序列化实现远程通信,在网络上传送对象的字节序列。

序列化与反序列化应用的场景

(1) 想把内存中的对象保存到一个文件中或者是数据库当中。
(2) 用套接字在网络上传输对象。
(3) 通过 RMI 传输对象的时候。

3. 几种创建的序列化和反序列化协议

XML&SOAP
JSON
Protobuf

0x02 序列化与反序列化代码实现

1. 代码展示

  • 类文件:Person.java
package Ser_01;

import javax.print.DocFlavor;
import java.io.IOException;
import java.io.Serializable;
import java.util.HashMap;

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 +
'}';
}
public void study(String s) {
System.out.println("学习中..." + s);
}
// public void reflect() {
// System.out.println("弹弹弹!");
// }
private String sleep(int age) {
System.out.println("睡眠中..." + age);
return "sleep";
}

// private void readObject(java.io.ObjectInputStream ois) throws ClassNotFoundException, IOException {
// ois.defaultReadObject();
// Runtime.getRuntime().exec("calc");
// }

}

  • 序列化文件 SerializationTest.java
package Ser_01;  


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("P4tt0n",22);
System.out.println(person);
serialize(person);
}
}
  • 反序列化文件 UnserializeTest.java
package Ser_01;  

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);
}
}

2. 序列化与反序列化的代码讲解

基本实现

  • SerializationTest.java

这里我们将代码进行了封装,将序列化功能封装进了 serialize 这个方法里面,在序列化当中,我们通过这个 FileOutputStream 输出流对象,将序列化的对象输出到 ser.bin 当中。再调用 oos 的 writeObject 方法,将对象进行序列化操作。

ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));  
oos.writeObject(obj);
  • UnserializeTest.java进行反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));  
Object obj = ois.readObject();

Serializable 接口

(1) 序列化类的属性没有实现 Serializable 那么在序列化就会报错

只有实现 了Serializable 或者 Externalizable 接口的类的对象才能被序列化为字节序列。(不是则会抛出异常)

Serializable 接口是 Java 提供的序列化接口,它是一个空接口,所以其实我们不需要实现什么。

public interface Serializable {
}

Serializable 用来标识当前类可以被 ObjectOutputStream 序列化,以及被 ObjectInputStream 反序列化。如果我们此处将 Serializable 接口删除掉的话,会导致如下结果。

image-20241219202645127

(2) 在反序列化过程中,它的父类如果没有实现序列化接口,那么将需要提供无参构造函数来重新创建对象。
(3)一个实现 Serializable 接口的子类也是可以被序列化的。
(4) 静态成员变量是不能被序列化

序列化是针对对象属性的,而静态成员变量是属于类的。

(5) transient 标识的对象成员变量不参与序列化

这里我们可以动手实操一下,将 Person.java 中的 name 加上 transient 的类型标识。加完之后再跑我们的序列化与反序列化的两个程序,修改过程与运行结果如图所示。

image-20241219202715749

0x03 为什么会产生序列化的安全问题

1. 引子

  • 序列化与反序列化当中有两个 “特别特别特别特别特别” 重要的方法 ———— writeObjectreadObject

这两个方法可以经过开发者重写,一般序列化的重写都是由于下面这种场景诞生的。

举个例子,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 直接调用危险方法

image-20241219204207285

先运行序列化程序 ———— “SerializationTest.java“,再运行反序列化程序 ———— “UnserializeTest.java

这时候就会弹出计算器,也就是 calc.exe,是不是帅的飞起哈哈。

这是黑客最理想的情况,但是这种情况几乎不会出现。

(2) 入口参数中包含可控类,该类有危险方法,readObject 时调用

(3) 入口类参数中包含可控类,该类又调用其他有危险方法的类,readObject 时调用

(4) 构造函数/静态代码块等类加载时隐式执行

3. 产生漏洞的攻击路线

首先的攻击前提:继承 Serializable

入口类:source (重写 readObject 调用常见的函数;参数类型宽泛,比如可以传入一个类作为参数;最好 jdk 自带)

找到入口类之后要找调用链 gadget chain 相同名称、相同类型

执行类 sink (RCE SSRF 写文件等等)比如 exec 这种函数