Java 序列化是一种将对象转换为字节流的过程,以便可以将对象保存到磁盘上,将其传输到网络上,或者将其存储在内存中,以后再进行反序列化,将字节流重新转换为对象。
序列化在 Java 中是通过 java.io.Serializable 接口来实现的,该接口没有任何方法,只是一个标记接口,用于标识类可以被序列化。
当你序列化对象时,你把它包装成一个特殊文件,可以保存、传输或存储。反序列化则是打开这个文件,读取序列化的数据,然后将其还原为对象,以便在程序中使用。
序列化是一种用于保存、传输和还原对象的方法,它使得对象可以在不同的计算机之间移动和共享,这对于分布式系统、数据存储和跨平台通信非常有用。
序列化
序列化主要是将一个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链的反序列化,后面文章会进行分析。
参考文档:
https://www.runoob.com/java/java-serialization.html
Java ObjectOutputStream 类 - Java教程 - 菜鸟教程 (cainiaojc.com)