java反序列化入口方法介绍

1、概念

1.1、序列化

序列化就是将内存对象转换为字节流的过程(使用transient关键字可以不让字段被序列化)。

1.1.1、jdk原生反序列化

jdk原生的反序列化指的是java自带的二进制序列化机制。jvm在序列化一个对象时,序列化当前类的同时,也会沿着继承链把父类的"类数据"逐层写入流中。"类数据"指的是类名、类的描述、类中定义的可被序列化的字段及字段的值。

例如:

继承链是:TTUser------>Custom------>Person

java 复制代码
class Person implements Serializable {
    private static final long serialVersionUID = 5652464866930818765L;
}

class Custom extends Person {
    private static final long serialVersionUID = 5652464866930818765L;
}

class TTUser extends Custom{
    private static final long serialVersionUID = 5652464866930818765L;
}

我对TTUser类的对象,进行序列化时,会同时序列化Custom和Person的类数据。

java 复制代码
// 对TTUser对象进行反序列化
ObjectOutputStream out = new ObjectOutputStream(
                new FileOutputStream("ttuser.ser"));
        out.writeObject(new TTUser());
        out.close();

可以使用HxD工具查看ttuser.ser里的内容,可以看出ttuser.ser里包含了TTUser、Custom、Person的类信息。

HxD下载地址:HxD - Freeware Hex Editor and Disk Editor | mh-nexus

如果类中有定义writeObject()方法,那么在序列化该类时,就调用类中自定义的writeObject()方法。如果类没有自定义writeObject()方法,那么在序列化该类时,就调用ObjectOutputStream.writeObject()。

1.2、反序列化

jdk原生的反序列化就是使用jdk自带的ObjectInputStream.readObject()方法把字节流还原为对象。在这个过程中可以执行一些代码逻辑,如果执行到了危险的代码逻辑,就会造成反序列化漏洞。

如果类中有定义readObject()方法,那么在序列化该类时,就调用类中自定义的readObject()方法。如果类没有自定义readObject()方法,那么在序列化该类时,就调用ObjectInputStream.readObject()。

2、jdk原生反序列化方法

2.1、readObject()

如果某个类中定义了readObject()方法,那么在反序列化该类的字节流.ser时,会执行该类内部的readObject()方法。

java 复制代码
package com.wlc.seralizeTest.jdkSink;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

/**
 * @author: Wulc
 * @createTime: 2026-03-28
 * @description:
 * @version: 1.0
 */

public class ReadObjectTest {
    public static void main(String[] args) throws Exception {
        //先对Student进行序列化
        Student student=new Student("张三",20);
        ObjectOutputStream out = new ObjectOutputStream(
                new FileOutputStream("student.ser"));
        out.writeObject(student);
        out.close();

        //再对student.ser进行反序列化
        ObjectInputStream in = new ObjectInputStream(
                new FileInputStream("student.ser"));
        in.readObject();   // 触发点
        in.close();
    }
}

class Human implements Serializable{
    private String name;
    private Integer age;
    public Human() {

    }
    public Human(String name,Integer age){
        this.name=name;
        this.age=age;
    }
    private void readObject(ObjectInputStream in) throws Exception{
        System.out.println(">>> Human readObject triggered!");
        try {
            Runtime.getRuntime().exec("calc"); // sink点
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
class Student extends Human{
    private String sclass;
    private Integer sno;

    public Student(String sclass,Integer sno){
        super();
        this.sclass=sclass;
        this.sno=sno;
    }

    private void readObject(ObjectInputStream in) throws Exception{
        System.out.println(">>> Student readObject triggered!");
        try {
            Runtime.getRuntime().exec("calc"); // sink点
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

2.2、readObjectNoData()

当jdk反序列化某个.ser字节流时,如果反序列化到某个类时,发现没有该类的数据,如果该类中自定义了readObjectNoData()方法,那么就会触发该类自定义的readObjectNoData()方法。

如果该类没有自定义的readObjectNoData()方法,那么jvm就什么都不会做,直接跳过该类。

readObjectNoData通常在反序列化涉及到继承链的情况下,会被触发的多一些。比如Custom类,其父类是Person类。在反序列化Custom类的.ser文件时,.ser文件被修改过了,导致里面没有Person类的字节码,所以反序列化Custom类的.ser文件时,会触发Person类中的readObjectNoData()方法。

java 复制代码
class Person implements Serializable {
    private static final long serialVersionUID = 5652464866930818765L;
    private void readObjectNoData() throws ObjectStreamException{
        System.out.println(">>> Person 的 readObjectNoData 被触发!");
    }
    private void readObject(ObjectInputStream in) throws Exception{
        System.out.println(">>> Person 的 readObject 被触发!");
        try {
            Runtime.getRuntime().exec("calc"); // sink点
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

class Custom extends Person {
    private static final long serialVersionUID = 5652464866930818765L;
    private void readObjectNoData() throws ObjectStreamException{
        System.out.println(">>> Custom 的 readObjectNoData 被触发!");
    }
    private void readObject(ObjectInputStream in) throws Exception{
        System.out.println(">>> Custom 的 readObject 被触发!");
        try {
            Runtime.getRuntime().exec("calc"); // sink点
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
复制代码
Step1、先对Person进行序列化,生成序列化文件person.ser
java 复制代码
ObjectOutputStream out = new ObjectOutputStream(
                new FileOutputStream("person.ser"));
        out.writeObject(new Person());
        out.close();
复制代码
Step2、使用HxD修改person.ser文件,把里面的Person类名替换为Custom,注意替换的类名要等长,这是为了不破坏.ser文件内容。Custom类继承自Person类,且两个类的序列化id是一样的。

Step3、对修改好后的person.ser文件进行反序列化,因为readObjectNoData会在"某个类被反序列化,但没有对应数据"时触发。

java 复制代码
//再对person.ser进行反序列化
ObjectInputStream in = new ObjectInputStream(
new FileInputStream("person.ser"));
in.readObject();   // 触发点
in.close();
复制代码
jvm反序列化一个对象时,会先处理当前类Custom,Custom有数据,所以正常被反序列化,不会调用readObjectNoData()。再去处理父类Person,Person没有数据,反序列化Person时会调用readObjectNoData()。

2.3、readExternal()

当一个类实现了 Externalizable 接口时,在反序列化过程中,JVM 不会调用 readObject(),而是直接调用该类的 readExternal() 方法。同理,在序列化的时候直接调用该类的 writeExternal() 方法。

java 复制代码
public class ReadExternalTest {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Food food=new Food("红色");
        //先对person进行序列化为person.ser
        ObjectOutputStream out = new ObjectOutputStream(
                new FileOutputStream("food.ser"));
        out.writeObject(food);
        out.close();

        ObjectInputStream in=new ObjectInputStream(new FileInputStream("food.ser"));
        in.readObject();
        in.close();
    }

}

class Food implements Externalizable, Serializable {
    private String colour;

    public Food() {
    }

    public Food(String colour) {
        this.colour = colour;
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        System.out.println(">>> Food 的 writeExternal 被触发!");
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        System.out.println(">>> Food 的 readExternal 被触发!");
    }
}

2.4、readResolve()

readResolve方法是在反序列化的最后一步执行,通常是跟着readObject()方法之后执行的,用于返回一个新的对象,替换原来反序列化的对象。

(1)不使用readResolve()方法

java 复制代码
class Fruit implements Serializable {
    private String shape;

    public Fruit(String shape) {
        this.shape = shape;
    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        System.out.println(">>> Fruit的readObject 被调用");
        // 从 classdata 中恢复字段值,不调用这个方法的化,类中所有字段都是默认值
        in.defaultReadObject();
    }

    @Override
    public String toString() {
        return "Fruit{" +
                "shape='" + shape + '\'' +
                '}';
    }
}
java 复制代码
Fruit fruit=new Fruit("圆");
        ObjectOutputStream outputStream=new ObjectOutputStream
                (new FileOutputStream("fruit.ser"));
        outputStream.writeObject(fruit);
        outputStream.close();

        ObjectInputStream inputStream=new ObjectInputStream
                (new FileInputStream("fruit.ser"));
        Object obj=inputStream.readObject();
        Fruit fruit1=(Fruit)obj;
        System.out.println(fruit1.toString());
        inputStream.close();

反序列化后,得到的还是原来的Fruit对象。

(2)使用readResolve()方法

java 复制代码
class Fruit implements Serializable {
    private String shape;

    public Fruit(String shape) {
        this.shape = shape;
    }
    private Object readResolve() throws ObjectStreamException {
        System.out.println(">>> Fruit的readResolve 被调用");
        // 返回一个"替代对象"
        return new Food("红色");
    }
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        System.out.println(">>> Fruit的readObject 被调用");
        // 从 classdata 中恢复字段值,不调用这个方法的化,类中所有字段都是默认值
        in.defaultReadObject();
    }

    @Override
    public String toString() {
        return "Fruit{" +
                "shape='" + shape + '\'' +
                '}';
    }
}
java 复制代码
Fruit fruit=new Fruit("圆");
        ObjectOutputStream outputStream=new ObjectOutputStream
                (new FileOutputStream("fruit.ser"));
        outputStream.writeObject(fruit);
        outputStream.close();

        ObjectInputStream inputStream=new ObjectInputStream
                (new FileInputStream("fruit.ser"));
        Object obj=inputStream.readObject();
        Food food=(Food)obj;
        System.out.println(food.toString());
        inputStream.close();

反序列化后,得到的是新的Food对象。

readResolve方法,通常会用在单例模式中,即在反序列化一个类对象时,不产生新的对象,还是返回原来的那个对象。

2.5、validateObject()

validateObject在是整个反序列化流程的最后一步,通常是用于在整个对象反序列化完成之后,对对象进行最终的一致性验证或修复。

java 复制代码
class Shop implements ObjectInputValidation, Serializable {
    private String address;

    public Shop() {
    }

    public Shop(String address) {
        this.address = address;
    }
    private void readObject(ObjectInputStream in) throws Exception {
        System.out.println(">>> Shop的readObject 被调用");
        in.defaultReadObject();
        // 注册回调validateObject方法,this回调对象为当前对象;0表示优先级
        in.registerValidation(this, 0);
    }
    @Override
    public void validateObject() throws InvalidObjectException {
        System.out.println(">>> Shop的validateObject 被调用");
        // 如果对象的address属性为空,则抛出异常
        if(this.address==null || this.address.equals("")){
            throw new InvalidObjectException("address不能为null");
        }
    }
    private Object readResolve() throws ObjectStreamException {
        System.out.println(">>> Shop的readResolve 被调用");
        // 返回一个"替代对象"
        return new Food("红色");
    }
}
java 复制代码
//        Shop shop=new Shop("大街");
        Shop shop=new Shop();
        ObjectOutputStream outputStream=new ObjectOutputStream
                (new FileOutputStream("shop.ser"));
        outputStream.writeObject(shop);
        outputStream.close();

        ObjectInputStream inputStream=new ObjectInputStream
                (new FileInputStream("shop.ser"));
        Food food=(Food)inputStream.readObject();
        System.out.println(food);
        inputStream.close();

因此反序列化的一般执行顺序是:readObject()------>readObjectNoData(没有类数据)------>readExternal(如果实现了Externalizable接口)------>readResolve()------>validateObject

3、其他反序列化入口

除了jdk原生的反序列化入口方法,还有其他流行的java反序列化协议提供的接口,例如Hession协议的Map.put()等。

3.1、T3/IIOP

中间件协议,基于网络的jdk反序列化,即远程调用协议RMI。在传输对象时自动触发jdk反序列化。

T3是WebLogic的私有协议。WebLogic是一款java应用服务器。IIOP是一款通信协议。

客户端发送请求(请求数据中包含了序列化对象)给服务器,服务器通过T3/IIOP协议解析请求数据,提取对象字节流反序列化,然后就会触发readObject()、readResolve()等一系列入口方法。入口方法同jdk原生的入口方法一样。

3.2、Hessian

Hessian是一种二进制序列化协议,http://hessian.caucho.com/ 主要用于RPC远程调用,dubbo等。

XML 复制代码
<dependency>
      <groupId>com.caucho</groupId>
      <artifactId>hessian</artifactId>
      <version>4.0.66</version>
    </dependency>
java 复制代码
public class HessianTest {
    public static void main(String[] args) throws IOException {
        HashMap<Object,Object> hashMap=new HashMap<>();
//        构造map,map.put()方法会触发hashCode方法。
        hashMap.put(new SUser("zhangsan","123"),"SUser");
        // 序列化SUser对象
        HessianOutput hessianOutput=new HessianOutput(new FileOutputStream("suser.ser"));
        hessianOutput.writeObject(hashMap);
        // hessianOutput.writeObject(new SUser());
        hessianOutput.close();

        // 反序列化suser.bin,反序列化时Hessian创建新的HashMap时,会用到map.put()方法会触发hashCode方法
//        Hessian 在反序列化 Map 时,会调用 put() ,put() 会触发 hashCode()
        // HashMap在put一个新元素时,需要 hashCode 来决定存储位置,如果发生hash冲突,还会触发equals方法
        HessianInput hessianInput=new HessianInput(new FileInputStream("suser.ser"));
        Object object=hessianInput.readObject();
        System.out.println(object);
    }
}

class SUser implements Serializable {
    private String username;
    private String password;

    public SUser() {
    }

    public SUser(String username, String password) {
        this.username = username;
        this.password = password;
    }

    @Override
    public int hashCode(){
        System.out.println(">>> SUser 的 hashCode 被触发!");
        return 1;
    }
    @Override
    public String toString() {
        return "SUser{" +
                "username='" + username + '\'' +
                ", password='" + password + '\'' +
                '}';
    }
}

注意,这里第一个hashCode方法,是在hashMap.put(new SUser("zhangsan","123"),"SUser");时会触发。第二个hashCode方法是Hessian 在反序列化 Map 时,会调用 put() ,put() 再触发 hashCode()。

3.3、Hessian-lite

Hessian的精简版。

3.4、Hessian-sofa

Hessian的增强版。

3.5、xstream

xstream是一个将java对象与xml相互转换的序列化框架。

XML 复制代码
    <!-- Source: https://mvnrepository.com/artifact/com.thoughtworks.xstream/xstream -->
    <dependency>
      <groupId>com.thoughtworks.xstream</groupId>
      <artifactId>xstream</artifactId>
      <version>1.4.15</version>
      <scope>compile</scope>
    </dependency>
    <!-- Source: https://mvnrepository.com/artifact/xpp3/xpp3 -->
    <dependency>
      <groupId>xpp3</groupId>
      <artifactId>xpp3</artifactId>
      <version>1.1.4c</version>
      <scope>compile</scope>
    </dependency>
java 复制代码
public class XstreamTest {
    public static void main(String[] args) {
        XStream xStream=new XStream();
        Pet pet=new Pet("Tom","白色");
        //序列化:对象------>xml
        String xml=xStream.toXML(pet);
        System.out.println(xml);

        //反序列化:xml------>对象
        Object object=xStream.fromXML(xml);
        System.out.println(object);

        HashMap<Object,Object> hashMap=new HashMap<>();
        hashMap.put(pet,"pet");
        String mapXml=xStream.toXML(hashMap);
        Object objMap=xStream.fromXML(mapXml);
        System.out.println(objMap);
    }
}

class Pet implements Serializable {
    private String name;
    private String color;

    public Pet() {
    }

    public Pet(String name, String color) {
        this.name = name;
        this.color = color;
    }

    private void readObject(ObjectInputStream in) throws Exception{
        System.out.println(">>> Pet readObject triggered!");
        in.defaultReadObject();
    }

    @Override
    public int hashCode(){
        System.out.println(">>> Pet 的 hashCode 被触发!");
        return 1;
    }
    @Override
    public String toString() {
        return "Pet{" +
                "name='" + name + '\'' +
                ", color='" + color + '\'' +
                '}';
    }
}

4、总结

最近在搞反序列化漏洞检测的研究,先梳理一下常见的java反序列化入口方法。

5、参考资料

https://ieeexplore.ieee.org/document/10646692

相关推荐
yxm26336690812 小时前
洛谷P1217回文质数
java·开发语言
量子炒饭大师2 小时前
【C++模板进阶】——【非类型模板参数 / 模板的特化 / 模板分离编译】
开发语言·c++·dubbo·模板·非类型模板·模板的特化·模板分离编译
雨师@2 小时前
python包uv使用介绍
开发语言·python·uv
吴声子夜歌2 小时前
JavaScript——异步编程
开发语言·前端·javascript
武藤一雄2 小时前
C# 核心技术解析:Parse vs TryParse 实战指南
开发语言·windows·microsoft·微软·c#·.netcore
一直都在5722 小时前
Java并发面经(二)
java·开发语言·spring
小雷君2 小时前
SpringBoot 接口开发5个高频踩坑总结
java·spring boot·后端·面试
aloha_7892 小时前
软考高项-第二章-信息技术发展
java·人工智能·python·学习
寒秋花开曾相惜2 小时前
(学习笔记)3.8 指针运算(3.8.5 变长数组)
java·c语言·开发语言·笔记·学习