JAVA安全学习笔记--反序列化基础

反序列化基础

推荐一个b站视频,讲的非常好

白日梦组长的视频 Java反序列化漏洞专题

序列化与反序列化

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

在PHP中也有反序列化漏洞,所以想必大家都对序列化有所了解。

这里便简单概括一下

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

2. 序列化的好处

  1. 持久化存储
  2. 数据的网络传输
  3. 跨平台/跨语言交互

3. 序列化在JAVA中场景

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

4. 序列化的几种方式/协议

  1. XML&SOAP
  2. JSON
  3. 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

image-20250906161131487

输出的原始数据

运行UnserializeTest.java

image-20250906161153434

反序列化回来的数据

我们可以看到反序列化后结果与最初一样。

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接口的意思,只有实现了这个接口这个类才能被序列化。

我们可以去掉后对比一下。

image-20250906162431742

可以发现直接报错了说明不能序列化。

我们可以去接口的定义看看。

image-20250906162518543

发现它其实并没有内容。这是一个规范,只有加了这个接口的实现我们才能进行序列化。

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

然后再运行一遍序列化与反序列化,便会发现成功弹出了计算器。

image-20250906164329069

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

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

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

3. 一般的攻击步骤

首先的攻击前提:继承 Serializable

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

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

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

4. 找到入门类(以hashMap为例子)

以hashMap为例子,带大家过一遍什么样的类才可能被利用。

首要前提便是继承了Serializable接口,不然没法进行序列化/

我们看一下hashMap的实现类,发现确实继承了Serializable接口

image-20250906165340367

然后便是要参数类型广泛,我们可以看到K,V可以传任意的类所以自然满足。

再接下来便是重写readObject方法。

我们打开Structure(我的idea里这个在左下角侧边栏),找到readObject方法。

image-20250906165649998

发现确实是有自己重写的readOject方法。

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

image-20250906170004517

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

image-20250906170057191

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

image-20250906170158113

发现这个hashCode方法是Object类里自带的,也就是所有的类都有的方法。这就满足我们调用常见的函数这一条件。所以hashMap便是一个很好的入门类。

至于后面的如何找链子,以及如何利用我会以实战的方式来讲解。

比如接下来要讲的便是第一个经典的链子——URLDNS链。其利用的便是反射与反序列化的内容。

点赞

发表回复

电子邮件地址不会被公开。必填项已用 * 标注