序列化/反序列化原理
- 简单来说就是:
- writeObject()方法序列化
- readObject()方法反序列化
- 一个对象要想被序列化,该对象所属的类必须实现Serializable接口
- 序列化:对象 -> 字符串
- 反序列化:字符串 -> 对象
对象的序列化主要有两种用途:
把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中(持久化对象);
在网络上传送对象的字节序列(网络传输对象)。
当两个进程在进行远程通信时,彼此可以发送各种类型的数据。无论是何种类型的数据,都会以二进制序列的形式在网络上传送。发送方需要把这个Java 对象转换为字节序列,才能在网络上传送;接收方则需要把字节序列再恢复为 Java 对象。
概述
-
Java序列化是指把Java对象转换为字节序列的过程;而Java反序列化是指把字节序列恢复为Java对象的过程。
-
序列化分为两大部分:序列化和反序列化。序列化是这个过程的第一部分,将数据分解成字节流,以便存储在文件中或在网络上传输。反序列化就是打开字节流并重构对象。对象序列化不仅要将基本数据类型转换成字节表示,有时还要恢复数据。恢复数据要求有恢复数据的对象实例。
- 为什么需要序列化与反序列化化
- 我们知道,当两个进程进行远程通信时,可以相互发送各种类型的数据,包括文本、图片、音频、视频等, 而这些数据都会以二进制序列的形式在网络上传送。那么当两个Java进程进行通信时,能否实现进程间的对象传送呢?答案是可以的。如何做到呢?这就需要Java序列化与反序列化了。换句话说,一方面,发送方需要把这个Java对象转换为字节序列,然后在网络上传送;另一方面,接收方需要从字节序列中恢复出Java对象。
- 当我们明晰了为什么需要Java序列化和反序列化后,我们很自然地会想Java序列化的好处。其好处一是实现了数据的持久化,通过序列化可以把数据永久地保存到硬盘上(通常存放在文件里),二是,利用序列化实现远程通信,即在网络上传送对象的字节序列。
- 想把内存中的对象保存到一个文件中或者数据库中时候;
- 想用套接字在网络上传送对象的时候;
- 想通过RMI传输对象的时候
- 一些应用场景,涉及到将对象转化成二进制,序列化保证了能够成功读取到保存的对象。
- 为什么需要序列化与反序列化化
-
几种常见的序列化和反序列化协议
XML&SOAP
XML
是一种常用的序列化和反序列化协议,具有跨机器,跨语言等优点SOAP
(Simple Object Access protocol
) 是一种被广泛应用的,基于 XML 为序列化和反序列化协议的结构化消息传递协议,它使应用使用http协议来交换信息
JSON
(Javascript Object Notation
)Protobuf
-
为什么会产生安全问题
- 只要服务端反序列化数据,客户端传递类的
readObject
中的代码会自动执行,给予攻击者在服务器上运行代码是能力。
- 只要服务端反序列化数据,客户端传递类的
-
序列化实现
- 只有实现了
Serializable
或者Externalizable
接口的类的对象才能被序列化为字节序列。(不是则会抛出异常) Serializable
接口- 是 Java 提供的序列化接口,它是一个空接口
- 只有实现了
1. public interface Serializable {
2. }
-
Serializable 用来标识当前类可以被 ObjectOutputStream 序列化,以及被 ObjectInputStream 反序列化。
-
Serializable 接口的基本使用
- 通过 ObjectOutputStream 将需要序列化数据写入到流中,因为 Java IO 是一种装饰者模式,因此可以通过 ObjectOutStream 包装 FileOutStream 将数据写入到文件中或者包装 ByteArrayOutStream 将数据写入到内存中。同理,可以通过 ObjectInputStream 将数据从磁盘 FileInputStream 或者内存 ByteArrayInputStream 读取出来然后转化为指定的对象即可。
-
Serializable 接口的特点
- 序列化类的属性没有实现 Serializable 那么在序列化就会报错
- 在反序列化过程中,它的父类如果没有实现序列化接口,那么将需要提供无参构造函数来重新创建对象。
- 一个实现 Serializable 接口的子类也是可以被序列化的。
- 静态成员变量是不能被序列化
- 序列化是针对对象属性的,而静态成员变量是属于类的。
- transient 标识的对象成员变量不参与序列化
-
序列化ID
- 可以看到,我们在进行序列化时,加了一个serialVersionUID字段,这便是序列化ID
- private static final long serialVersionUID = 1L;
- 这个序列化ID起着关键的作用,它决定着是否能够成功反序列化!java的序列化机制是通过判断运行时类的serialVersionUID来验证版本一致性的,在进行反序列化时,JVM会把传进来的字节流中的serialVersionUID与本地实体类中的serialVersionUID进行比较,如果相同则认为是一致的,便可以进行反序列化,否则就会报序列化版本不一致的异常。
- 即序列化ID是为了保证成功进行反序列化
-
可能的形式
- 入口类的readObject直接调用危险函数
- 入口类参数中包括可控类,该类有危险方法,readObject时调用
- 入口类中包含可控类,该类又调用其他危险函数的类,readObject时调用。
- 比如类型定义为Object,调用equals/hashcode/toString(相同类型,相同函数)
-
构造函数/静态代码块等在类加载的时候隐式执行
共同条件
- 继承Serialize
- 入口类 source (重写readObject 参数类型宽泛 最好JDK自带) Map
- HashMap继承了serialize,满足参数类型宽泛,也是JDK自带的,也重写了readObject,是一个很完美的入口类
- 会进行数据传输,所以继承了serialize
- 本身就是键值对,满足参数类型宽泛
- 因为hashmap需要保证键的唯一性,就要计算键的hashcode,如果键是对象的话,在不同的JVM实现中计算得出的hash值可能是不同的,所以他必须重新实现他的readobject方法
- HashMap继承了serialize,满足参数类型宽泛,也是JDK自带的,也重写了readObject,是一个很完美的入口类
- 调用类 gadget chain
- 执行类 sink (rec ssrf 写文件等) 最重要的部分
● /封装serializepublic static void serialize(Object object) throws IOException {
● ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
● oos.writeObject(object);
● }
● //封装unserializepublic static Object unserialize(String Filename) throws IOException, ClassNotFoundException {
● ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
● Object obj = ois.readObject();
● return obj;
● }