6.java反序列化

1. 反序列化

1、序列化与反序列化(见图)
Pasted image 20250418223035
序列化:将内存中的对象压缩成字节流
反序列化:将字节流转化成内存中的对象
序列化与反序列化其实就是对象与数据格式的转换。

1.1. 为什么有序列化技术

序列化与反序列化的设计就是用来传输数据的。
Pasted image 20250418223038
当两个进程进行通信的时候,可以通过序列化反序列化来进行传输。
能够实现数据的持久化,通过序列化可以把数据永久的保存在硬盘上,也可以理解为通过序列化将数据保存在文件中。

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

1.2. 常见的创建的序列化和反序列化协议

• JAVA内置的writeObject()/readObject()
• JAVA内置的XMLDecoder()/XMLEncoder
• XStream
• SnakeYaml
• FastJson
• Jackson

1.3. 反序列化利用条件:

(1) 可控的输入变量进行了反序列化操作
(2) 实现了Serializable或者Externalizable接口的类的对象
(3) 能找到调用方法的危险代码或间接的利用链引发(依赖链)

2. 反序列化案例

本文以JAVA内置的 writeObject() / readObject() 为参考

2.1. 序列化操作

//User.java
import java.io.Serializable;

public class User implements Serializable {
    private static final long serialVersionUID = 1L;
    public String name;
    public int age;
    public String gender;
    public String address;

    public User(String name, int age, String gender, String address) {
        this.name = name;
        this.age = age;
        this.gender = gender;
        this.address = address;
    }

    @Override
    public String toString() {
        return name + " " + age + " " + gender + " " + address;
    }
}

//SerializableTest.java
import java.io.*;  
  
public class SerializableTest {  
    public static void main(String[] args) throws IOException {  
  
        // 创建User对象并序列化  
        User user = new User("c1trus", 18, "man", "ChongQing");  
        SerializableTest(user);  
    }  
    public static void SerializableTest(Object obj) throws IOException {  
        //FileOutputStream()输出文件  
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.txt"));  
        //将对象Obj写入到文件 ser.txt
        oos.writeObject(obj);
    }  
  
}

运行后就会获取到一个 ser.txt 序列化数据
Pasted image 20250418230849

2.2. 反序列化操作

Tip

JAVA内置的writeObject()/readObject()内置原生写法分析:

  • writeObject():主要用于将 Java 对象序列化为字节流并写入输出流
  • readObject():主要用于从输入流中读取字节序列反序列化为 Java 对象
  • FileInputStream:其主要作用是从文件读取字节数据
  • FileOutputStream:其主要作用是将字节数据写入文件
  • ObjectInputStream:用于从输入流中读取对象,实现对象的反序列化操作
  • ObjectOutputStream:用于将对象并写入输出流的类,实现对象的序列化操作
//UnSerializable.java
import java.io.FileInputStream;  
import java.io.IOException;  
import java.io.ObjectInputStream;  
  
public class UnSerializable {  
    public static void main(String[] args) throws IOException, ClassNotFoundException {  
        Object o =UnserializableTest("ser.txt");  
        System.out.println(o);  
    }  
    public static Object UnserializableTest(String filename) throws IOException, ClassNotFoundException {  
        ObjectInputStream ois=new ObjectInputStream(new FileInputStream(filename));  
        Object o= ois.readObject();  
        return o;  
    }  
}

反序列化后就能把序列化数据 ser.txt 还原数据
Pasted image 20250418231824

3. 反序列化利用

3.1. 序列化的对象有没有重写readObject方法(危险代码)

我们在User类中重写 readObject 方法 里面加上危险的代码

private void readObject (java.io.ObjectInputStream stream) throws IOException {  
    System.out.println("这是一个危险的方法");  
    Runtime.getRuntime().exec("calc");  
}

Pasted image 20250418234528
我们先把User对象 进行序列化,然后在反序列化,成功触发了危险代码
PixPin_2025-04-18_23-46-40

原理

User类中,攻击者重写了readObject()方法
Pasted image 20250418235146
序列化的时候将User对象正常写入文件ser.txt,然后反序列化将ser.txt进行反序列化。
然后执行ois.readObject(),因为ois就是user对象,所以执行的就是user.readObject()
Pasted image 20250418235025

3.2. 看序列化的对象有没有被输出就会调用toString方法(危险代码)

原理

只要序列化对象被输出,那么就会调用 toString 方法,如果 toString 方法里面含有危险代码,那么就可以被利用

  • 如果里面有其他的方法、函数可以进行跟踪,看看会不会有危险的代码

这里我们给 user 类的 toString 方法加上一些危险代码

public String toString() {  
    System.out.println("这是toString方法");  
    try {  
        Runtime.getRuntime().exec("calc");  
    } catch (IOException e) {  
        throw new RuntimeException(e);  
    }  
    return "";  
}

Pasted image 20250418235930
然后进行序列化,再反序列化,成功触发危险代码
Pasted image 20250419000021

3.3. 其他类的readObject或toString方法(反序列化类可控)

Note

如果序列化的对象写死了。但是反序列化的二进制数据流可以被我们控制,那么就可以反序列化我们任意的类 (这里我用的是自己写的Calc类)

而这些类就包括java自带的,还有组件中的。 组件中的我们就可以通过反射 代理进行调用,这就是链的组成

我们先创建一个 Clac 类,里面写入危险代码

//calc.java
import java.io.IOException;  
  
public class Calc implements java.io.Serializable {  
    private void readObject(java.io.ObjectInputStream in) throws IOException {  
        System.out.println("Calc readObject");  
        Runtime.getRuntime().exec("calc");  
  
    }  
}

然后对其进行序列化为 sercalc.txt
如果此时我们可以控制反序列化的对象 从 ser.txt -> sercalc.txt
那么我们就会触发 Calc 类中的危险代码

入口类的readObject直接调用危险方法
(2) 入口参数中包含可控类,该类有危险方法,readObject时调用
(3) 入口类参数包含可控类,该类又调用其他有危险方法类,readObject调用
(4) 构造函数/静态代码块等类加载时隐式执行