序列化

序列化主要是将一个java对象转换为二进制字节流的过程,在学习反序列化之前,首先来学习一下序列化是什么,怎么进行,知其然,知其所以然。 序列化首先需要注意一下几点

  • 实现 Serializable 接口:序列化的对象必须要实现Serializable接口,这个接口是一个标记接口,没有任何方法,只是用于标识该类的对象可以被序列化。

  • 对象的成员变量可序列化: 如果一个类中的成员变量是基本数据类型或其他可序列化的对象,则该类的对象也是可序列化的。如果成员变量是不可序列化的对象,可以使用 transient 关键字进行标记,表示在序列化过程中不将该成员变量序列化,并且static变量正常情况下是不会被序列化的。

  • 版本控制: 在进行对象序列化时,需要注意版本控制,确保对象的类结构发生变化时不会导致序列化和反序列化的问题。可以通过 serialVersionUID 字段进行手动版本控制。

  • 对象的引用关系: 如果对象间存在引用关系,即一个对象引用了另一个对象,那么在序列化和反序列化时,需要确保所有相关的对象都可以正确地序列化和反序列化。ObjectOutputStream中的writeObject方法是java进行序列化的关键方法,下面是使用的具体方法

//创建一个FileOutputStream,其中写入ObjectOutputStream中的对象
FileOutputStream fileStream = new FileOutputStream(String file);

//创建ObjectOutputStream
ObjectOutputStream objStream = new ObjectOutputStream(fileStream);

//调用writeObject方法,该方法传入的是需要序列化的对象,并且该方法会将该对象进行序列化,写入到上面指定的输出流中:fileStream
objStream.writeObject(Object);

下面是序列化的示例 首先创建一个类,并且该类继承Serializable接口

package com.pwjcw.entity;  
  
import java.io.Serializable;  
  
public class Persion implements Serializable {  
    private String name;  
    private int age;  
  
    public Persion() {  
    }  
  
    @Override  
    public String toString() {  
        return "Persion{" +  
                "name='" + name + '\'' +  
                ", age=" + age +  
                '}';  
    }  
  
    public Persion(String name, int age) {  
        this.name = name;  
        this.age = age;  
    }  
  
    public String getName() {  
        return name;  
    }  
  
    public void setName(String name) {  
        this.name = name;  
    }  
  
    public int getAge() {  
        return age;  
    }  
  
    public void setAge(int age) {  
        this.age = age;  
    }  
}

实现对该类的序列化

@Test  
public void TestSerialize() throws IOException {  
    Persion persion = new Persion("pwjcw", 1);  
    // 创建文件输出流,用于将对象序列化后的字节流写入文件  
    FileOutputStream f = new FileOutputStream("persion.ser");  
    // 创建对象输出流,用于将对象序列化后的数据写入文件  
    ObjectOutputStream outputStream = new ObjectOutputStream(f);  
    // 将 Person 对象进行序列化并写入文件  
    outputStream.writeObject(persion);  
    // 关闭对象输出流  
    outputStream.close();  
    // 关闭文件输出流  
    f.close();  
}

序列化后的数据

将一个对象进行序列化其实是将该对象的一些属性进行序列化,并不是对该对象的类,以及相关的方法也进行序列化为二进制数据。下面是persion.ser文件的二进制视图

serialVersionUID

在Java中,serialVersionUID是一个特殊的静态变量,用于标识序列化类的版本。当一个类被序列化时,它的serialVersionUID会被序列化到流中,以确保序列化和反序列化过程中类的版本一致性,如果反序列化时的serialVersionUID版本与本地对应的serialVersionUID版本不一致,则会导致反序列化失败。

当类的结构发生变化时(例如添加新的字段或方法),通过显式地指定serialVersionUID,可以确保旧版本的序列化数据可以与新版本的类兼容。如果不指定serialVersionUID,Java会根据类的结构自动生成一个,但是当类的结构发生变化时,自动生成的serialVersionUID也会改变,可能导致旧版本的序列化数据无法被正确反序列化

关于悖论的解释

上面也已经说到,serialVersionUID是一个静态变量,而静态变量正常情况下又不参与序列化,不过serialVersionUID是一个例外,这是java设计的一个特殊情况。

反序列化

反序列化和序列化相反,主要是从二进制字节流转换为java对象,可以通过ObjectInputStream类的readObject方法将传入的二进制流进行序列化到对象 下面是具体的使用示例

@Test  
public void TestUnSerialize() throws IOException, ClassNotFoundException {  
    //从文件中读取二进制字节流  
    FileInputStream fileInputStream=new FileInputStream("persion.ser");  
    //创建ObjectInputStream对象,并且传入FileInputStream读取到的二进制字节流  
    ObjectInputStream objectInputStream=new ObjectInputStream(fileInputStream);  
    //调用readObject方法,并且转换为Persion对象  
    Persion persion= (Persion) objectInputStream.readObject();  
    //打印反序列化得到的persion对象  
    System.out.println(persion);  
}

反序列化导致rce的原因

在反序列化中,如果反序列化的目标类自定义了readObject方法,那么在反序列化时,将会调用目标类自定义的readObject方法,这是出现反序列化漏洞的根本原因。下面是一个演示示例。 目标类

package com.pwjcw.entity;  
  
import java.io.IOException;  
import java.io.ObjectInputStream;  
import java.io.Serializable;  
  
public class Persion implements Serializable {  
    private String name;  
    private int age;  
  
    public Persion() {  
    }  
  
    @Override  
    public String toString() {  
        return "Persion{" +  
                "name='" + name + '\'' +  
                ", age=" + age +  
                '}';  
    }  
  
    public Persion(String name, int age) {  
        this.name = name;  
        this.age = age;  
    }  
  
    public String getName() {  
        return name;  
    }  
  
    public void setName(String name) {  
        this.name = name;  
    }  
  
    public int getAge() {  
        return age;  
    }  
  
    public void setAge(int age) {  
        this.age = age;  
    }  
    private void readObject(java.io.ObjectInputStream objectInputStream) throws IOException, ClassNotFoundException {  
        objectInputStream.defaultReadObject();  
        Runtime.getRuntime().exec("calc.exe");  
    }  
}

那么此时进行反序列化,就会调用Persion类的readObject方法,进而调用Runtime语句。 当前并不是所有的方法都会自定义readObject方法,不过后面的HashMap类自定义了,并且通过该类造成了URLDNS链的反序列化,后面文章会进行分析。

免责声明

本文仅用于技术讨论与学习,利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,本平台和发布者不为此承担任何责任。