Java反序列化原理
Java反序列化原理
Serializable 接口的使用
类需要被序列化和反序列化,需要实现接口 java.io.Serializable
Java 提供的序列化接口 Serializable ,是一个空接口,没有成员方法和变量
1 | public interface Serializable { |
1 | import java.io.Serializable; |
序列化 调用
ObjectOutputStream
类的writeObject
方法反序列化 调用
ObjectInputStream
类的readObject
方法
注意:
只有实现了Serializable或者Externalizable接口的类的对象才能被序列化为字节序列,否则会抛出异常。Externalizable 接口继承自Serializable接口,实现 Externalizable接口的类完全由自身来控制序列化的行为,而仅实现 Serializable 接口的类可以采用默认的序列化方式。
- 它是Java 提供的序列化接口,是一个空接口,没有成员方法和变量
- 一个类要想序列化就必须继承java.io.Serializable接口,同时它的子类也可以序列化(不用再继承Serializable接口)
- 一个实现Serializable 接口的子类也是可以被序列化的。在反序列化过程中,它的父类如果没有实现序列化接口,那么父类将需要提供无参构造函数来重新创建对象,这样做的目的是重新初始化父类的属性,父类对应的属性不会被序列化
- 序列化只能保存对象的非静态成员变量,不能保存任何的成员方法和静态的成员变量,而且序列化保存的只是变量的值,对于变量的任何修饰符都不能保存。即序列化是保存对象的状态(对象属性)
- transient 标识的对象成员变量不参与序列化
- Serializable 在序列化和反序列化过程中大量使用了反射,因此其过程会产生的大量的内存碎片
注:JAVA会被称为八股文,原因在于它的一个小知识点里会有很多细节在里面
Person
1 | package net.Seck2y.a01; |
SerializableTest
序列化 Person 形成 ser.txt文件
1 | package net.Seck2y.a01; |
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 | D:\CTF\Web\Java\SerializationDumper-v1.13>java -jar SerializationDumper-v1.13.jar "aced0005737200156e65742e5365636b32792e6130312e506572736f6eb092c484b6d968ce0200024900036167654c00046e616d657400124c6a6176612f6c616e672f537472696e673b7870000000177400087a68616e6773616e" |
UnserializableTest
读取 ser.bin 文件 进行反序列化,再返回
但 readObject 已经被重写,便会执行重写的 readObject 终端运行命令打开 notepad
1 | package net.Seck2y.a01; |
总结
- 只有实现了
Serializable
或者Externalizable
接口的类的对象才能被序列化为字节序列,否则会抛出异常Java.io.ObjectOutputStream
代表对象输出流,它的writeObject(Object obj)
方法可对参数指定的 obj 对象进行序列化,把得到的字节序列写到一个目标输出流中Java.io.ObjectInputStream
代表对象输入流,它的readObject()
方法从一个源输 入流中读取字节序列,再把它们反序列化为一个对象,并将其返回- Java原生序列化的数据特征:16进制是以
aced00057372
开头(一般认为是aced0005);base64编码是以rO0ABXNy
开头(一般认为是rO0AB)- 反序列化产生漏洞的形式,有以下几种:
- 入口类的 readObject 直接调用危险方法
- 入口类参数中包含可控类,该类有危险方法,readObject 时调用
- 入口类参数中包含可控类,该类又调用其他有危险方法的类,readObject 时调用
- 构造函数/静态代码块等类加载时隐式执行
Serializable接口的六个特点:
它是 Java 提供的序列化接口,是一个空接口,没有成员方法和变量
一个类要想序列化就必须继承 java.io.Serializable 接口,同时它的子类也可以序列化(不用再继承Serializable 接口)
一个实现 Serializable 接口的子类也是可以被序列化的
序列化只能保存对象的非静态成员变量
transient 标识的对象成员变量不参与序列化
Serializable 在序列化和反序列化过程中大量使用了反射
serialVersionUID
反序列化时会检测 serialVersionUID 是否一致,不一致会导致异常
serialVersionUID 发生改变有三种情况:
- 手动去修改导致当前的 serialVersionUID 与序列化前的不一样
- 我们根本就没有手动去写这个 serialVersionUID 常量,那么 JVM 内部会根据类结构去计算得到这个 serialVersionUID 值,在类结构发生改变时(属性增加,删除或者类型修改了)这种也是会导致 serialVersionUID 发生变化
- 假如类结构没有发生改变,并且没有定义 serialVersionUID ,但是反序列和序列化操作的虚拟机不一样也可能导致计算出来的 serialVersionUID 不一样
JVM 规范强烈建议我们手动声明一个版本号,这个数字可以是随机的,只要固定不变就可以。同时最好是 private 和 final 的,尽量保证不变