Java反序列化原理

Serializable 接口的使用

类需要被序列化和反序列化,需要实现接口 java.io.Serializable

Java 提供的序列化接口 Serializable ,是一个空接口,没有成员方法和变量

1
2
public interface Serializable { 
}
1
2
3
4
5
6
import java.io.Serializable;

public class Example implements Serializable{
private String name;
....
}

序列化 调用 ObjectOutputStream 类的 writeObject 方法

反序列化 调用 ObjectInputStream 类的 readObject 方法

注意:

只有实现了Serializable或者Externalizable接口的类的对象才能被序列化为字节序列,否则会抛出异常。Externalizable 接口继承自Serializable接口,实现 Externalizable接口的类完全由自身来控制序列化的行为,而仅实现 Serializable 接口的类可以采用默认的序列化方式。

  1. 它是Java 提供的序列化接口,是一个空接口,没有成员方法和变量
  2. 一个类要想序列化就必须继承java.io.Serializable接口,同时它的子类也可以序列化(不用再继承Serializable接口)
  3. 一个实现Serializable 接口的子类也是可以被序列化的。在反序列化过程中,它的父类如果没有实现序列化接口,那么父类将需要提供无参构造函数来重新创建对象,这样做的目的是重新初始化父类的属性,父类对应的属性不会被序列化
  4. 序列化只能保存对象的非静态成员变量,不能保存任何的成员方法和静态的成员变量,而且序列化保存的只是变量的值,对于变量的任何修饰符都不能保存。即序列化是保存对象的状态(对象属性)
  5. transient 标识的对象成员变量不参与序列化
  6. Serializable 在序列化和反序列化过程中大量使用了反射,因此其过程会产生的大量的内存碎片

注:JAVA会被称为八股文,原因在于它的一个小知识点里会有很多细节在里面

Person

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package net.Seck2y.a01;

import java.io.IOException;
import java.io.ObjectInputStream;
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;
}

// 重写 toString,规定打印格式,不规定会输出类和地址
@Override
public String toString() {
return "Person{"+
"name='" + name + "'"+
", age='" + age + "'}";
}


// 入口类的 readObject 直接调用危险方法,造成反序列化漏洞
// 运行反序列化操作,弹出记事本

private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ois.defaultReadObject();
Runtime.getRuntime().exec("notepad");
}
}

SerializableTest

序列化 Person 形成 ser.txt文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package net.Seck2y.a01;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class SerializableTest {
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos= new ObjectOutputStream(new FileOutputStream("ser.txt"));
oos.writeObject(obj);
oos.close();
}

public static void main(String[] args) throws Exception{
Person s = new Person("zhangsan",23);
// System.out.println(s); 打印输出以 toString方法 格式输出
serialize(s);
}
}

ser.txt 乱码,这是必然的,因为java原生反序列化产生的就是字节序列

1
 sr net.Seck2y.a01.Person皰膭顿h� I ageL namet Ljava/lang/String;xp   t zhangsan

转16进制

原生类16进制是以 aced00057372 开头

1
aced0005737200156e65742e5365636b32792e6130312e506572736f6eb092c484b6d968ce0200024900036167654c00046e616d657400124c6a6176612f6c616e672f537472696e673b7870000000177400087a68616e6773616e

转base64

原生类base64是以 rO0ABXNy 开头

1
rO0ABXNyABVuZXQuU2VjazJ5LmEwMS5QZXJzb26wksSEttlozgIAAkkAA2FnZUwABG5hbWV0ABJMamF2YS9sYW5nL1N0cmluZzt4cAAAABd0AAh6aGFuZ3Nhbg==

SerializationDumper

将序列化的16进制转码格式输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
D:\CTF\Web\Java\SerializationDumper-v1.13>java -jar SerializationDumper-v1.13.jar "aced0005737200156e65742e5365636b32792e6130312e506572736f6eb092c484b6d968ce0200024900036167654c00046e616d657400124c6a6176612f6c616e672f537472696e673b7870000000177400087a68616e6773616e"

STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
Contents
TC_OBJECT - 0x73
TC_CLASSDESC - 0x72
className
Length - 21 - 0x00 15
Value - net.Seck2y.a01.Person - 0x6e65742e5365636b32792e6130312e506572736f6e
serialVersionUID - 0xb0 92 c4 84 b6 d9 68 ce
newHandle 0x00 7e 00 00
classDescFlags - 0x02 - SC_SERIALIZABLE
fieldCount - 2 - 0x00 02
Fields
0:
Int - I - 0x49
fieldName
Length - 3 - 0x00 03
Value - age - 0x616765
1:
Object - L - 0x4c
fieldName
Length - 4 - 0x00 04
Value - name - 0x6e616d65
className1
TC_STRING - 0x74
newHandle 0x00 7e 00 01
Length - 18 - 0x00 12
Value - Ljava/lang/String; - 0x4c6a6176612f6c616e672f537472696e673b
classAnnotations
TC_ENDBLOCKDATA - 0x78
superClassDesc
TC_NULL - 0x70
newHandle 0x00 7e 00 02
classdata
net.Seck2y.a01.Person
values
age
(int)23 - 0x00 00 00 17
name
(object)
TC_STRING - 0x74
newHandle 0x00 7e 00 03
Length - 8 - 0x00 08
Value - zhangsan - 0x7a68616e6773616e

UnserializableTest

读取 ser.bin 文件 进行反序列化,再返回

但 readObject 已经被重写,便会执行重写的 readObject 终端运行命令打开 notepad

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package net.Seck2y.a01;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

public class UnserializableTest{
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
ois.close();
return obj;
}

public static void main(String[] args) throws Exception {
Person s = (Person)unserialize("ser.txt");
// System.out.println(s);
}
}

总结

  1. 只有实现了 Serializable 或者 Externalizable 接口的类的对象才能被序列化为字节序列,否则会抛出异常
  2. Java.io.ObjectOutputStream代表对象输出流,它的writeObject(Object obj)方法可对参数指定的 obj 对象进行序列化,把得到的字节序列写到一个目标输出流中
  3. Java.io.ObjectInputStream代表对象输入流,它的readObject()方法从一个源输 入流中读取字节序列,再把它们反序列化为一个对象,并将其返回
  4. Java原生序列化的数据特征:16进制是以aced00057372 开头(一般认为是aced0005);base64编码是以rO0ABXNy开头(一般认为是rO0AB)
  5. 反序列化产生漏洞的形式,有以下几种:
  • 入口类的 readObject 直接调用危险方法
  • 入口类参数中包含可控类,该类有危险方法,readObject 时调用
  • 入口类参数中包含可控类,该类又调用其他有危险方法的类,readObject 时调用
  • 构造函数/静态代码块等类加载时隐式执行

Serializable接口的六个特点:

  1. 它是 Java 提供的序列化接口,是一个空接口,没有成员方法和变量

  2. 一个类要想序列化就必须继承 java.io.Serializable 接口,同时它的子类也可以序列化(不用再继承Serializable 接口)

  3. 一个实现 Serializable 接口的子类也是可以被序列化的

  4. 序列化只能保存对象的非静态成员变量

  5. transient 标识的对象成员变量不参与序列化

  6. Serializable 在序列化和反序列化过程中大量使用了反射

serialVersionUID

反序列化时会检测 serialVersionUID 是否一致,不一致会导致异常

serialVersionUID 发生改变有三种情况:

  1. 手动去修改导致当前的 serialVersionUID 与序列化前的不一样
  2. 我们根本就没有手动去写这个 serialVersionUID 常量,那么 JVM 内部会根据类结构去计算得到这个 serialVersionUID 值,在类结构发生改变时(属性增加,删除或者类型修改了)这种也是会导致 serialVersionUID 发生变化
  3. 假如类结构没有发生改变,并且没有定义 serialVersionUID ,但是反序列和序列化操作的虚拟机不一样也可能导致计算出来的 serialVersionUID 不一样

JVM 规范强烈建议我们手动声明一个版本号,这个数字可以是随机的,只要固定不变就可以。同时最好是 private 和 final 的,尽量保证不变

⬆︎TOP