什么是序列化?
- Java序列化是指把Java对象转换为字节序列的过程;
- Java反序列化是指把字节序列恢复为Java对象的过程;
反序列化目的
对象不只是存储在内存中,它还需要在传输网络中进行传输,并且保存起来之后下次再加载出来,这时候就需要序列化技术。Java的序列化技术就是把对象转换成一串由二进制字节组成的数组,然后将这二进制数据保存在磁盘或
传输网络。而后需要用到这对象时,磁盘或者网络接收者可以通过反序列化得到此对象,达到对象持久化的目的。
反序列化条件:
● 该类必须实现 java.io.Serializable 对象
● 该类的所有属性必须是可序列化的。如果有一个属性不是可序列化的,则该属性必须注明是短暂的
序列化过程
序列化过程:
● 序列化:将 OutputStream 封装在 ObjectOutputStream 内,然后调用 writeObject 即可
● 反序列化:将 InputStream 封装在 ObjectInputStream 内,然后调用 readObject 即可
反序列化出错可能原因
● 序列化字节码中的 serialVersionUID(用于记录java序列化版本)在进行反序列化时,JVM 会把传来的字节流中的 serialVersionUID 与本地相应实体类的 serialVersionUID 进行比较,如果相同就认为是一致的,可以进行反序列化,否则就抛出序列化版本不一致的异常- InvalidCastException。
ObjectOutputStream 与 ObjectInputStream类
ObjectOutputStream
先创建一个java 被反序列化对象
1 | package org.com.serial; |
创建另外一个java文件进行对于这个类的对象做一个反序列化
1 | package org.com.serial; |
FileOutputStream
1 | FileOutputStream fos = new FileOutputStream("ser.txt"); |
ObjectInputStream类
如果能找到一个对象的class文件,我们可以进行反序列化操作,调用 ObjectInputStream 读取对象的 方法:打印结果:反序列化操作就是从二进制文件中提取对象
编写反序列化代码
1 | package org.com.serial; |
漏洞成因
了解漏洞成因之前,我们首先要了解的是readobject方法。我们知道创建一个ObjectInputStream类的对象可以直接调用readobject方法,但是在这个类中,readobject方法是没有被具体实现的。而且他还有一个defaultreadobject方法,那么问题就是既然这个readobject方法没有被实现,那我们为什么还能使用他进行反序列化操作?
内部逻辑
调用
readObject
方法:当你调用ObjectInputStream
的readObject
方法时,它会读取流中的数据,了解要反序列化的对象类型。内部逻辑:虽然你没有看到具体的实现,但
ObjectInputStream
具有内部机制来处理对象的创建和字段的恢复。这包括:- 创建对象实例。
- 读取对象的字段并根据需要恢复它们的状态。
反序列化过程:在这个过程中,Java 使用反射和其他机制来确保对象的状态被正确恢复。这样,即使
readObject
方法没有用户自定义的逻辑,它仍然可以执行必要的步骤来完成反序列化。
了解了这个原因,我们就可以了解另外一个,我们可以重写readobject方法,并且我们可以在带有标记接口Serializable 的类中进行重写,那么又有一个问题既然我们的这个readobject方法是采用内部逻辑的方式去实现反序列化,那我们重写了之后我们无法利用内部逻辑,此时我们就需要刚才提到的defaultreadobject方法,我们可以在这个方法执行默认的反序列化逻辑,恢复对象的非瞬态字段。同时我们可以自己加一些逻辑代码进去。这也是我们能够利用的关键。
但是问题又来了? 既然带有标记接口Serializable的这个类,继承的接口没有readobject方法,我们为什么还能在这个继承了标记接口的类中重写readobject方法。
ObjectInputStream
能找到你自定义的 readObject
方法是因为反序列化的过程是通过类的元信息(反射)来实现的。具体来说:
类的元信息:当你反序列化一个对象时,
ObjectInputStream
会首先读取对象的类信息,包括类名和字段。查找方法:在获取到类的信息后,
ObjectInputStream
会检查该类是否定义了一个私有的readObject
方法。这个检查是通过反射进行的,Java 反射机制允许程序在运行时查询类的结构和方法。调用自定义方法:如果找到了这个自定义的
readObject
方法,ObjectInputStream
就会调用它,而不是执行默认的反序列化逻辑。这使得你可以在反序列化过程中添加自定义逻辑。
重点是java的反射机制。[[反射机制]]
实现
经过刚才的分析,我们大致已经了解了,反序列化我们为什么可以控制。就是因为java允许我们重写readobject这个方法。导致我们可以利用反射机制,在我们序列化的这个类的中找到对应的重写的readobject方法。从而执行我们的逻辑,但是要保证我们的readobject方法一定是private的。
设置类
1 | package org.com.serial; |
序列化和反序列化
1 | package org.com.serial; |
● 入口类 source (即找到重写 readObject方法,调用常见的函数,参数类型宽泛 最好 jdk 自带)
● 调用链 gadget chain (基于类的默认方式调用)
● 执行类 sink (RCE、SSRF、写文件等操作)
serialVersionUID
- 在序列化和反序列化过程中,确保对象的兼容性。当对象被序列化后,如果该类的结构发生了变化,比如添加、删除或修改了成员变量、方法等,反序列化时可能会出现问题。
- 通过显式地指定
serialVersionUID
,可以在一定程度上控制不同版本的类之间的兼容性。如果序列化和反序列化时的serialVersionUID
一致,那么 Java 虚拟机认为这两个版本是兼容的,可以进行反序列化操作
显示id
静态变量和被设置的属性不会被序列化,因此我们一般都是设置一个静态变量.作为显示id
显式地定义了一个serialVersionUID
,在类的结构发生变化时,反序列化不一定没有影响。
如果类的结构变化是向后兼容的,比如添加了新的不参与序列化的方法、添加了新的具有默认值的成员变量等,那么在反序列化时通常不会有问题。
但是,如果类的结构变化影响到了已序列化对象的数据,比如删除了成员变量、修改了成员变量的类型等,那么在反序列化时仍然可能会出现问题。
例如,假设一个类原本有一个名为name
的字符串成员变量,序列化了一个对象后,将这个类中的name
成员变量删除了,再进行反序列化时就会抛出异常。
所以,虽然定义了serialVersionUID
可以在一定程度上提高兼容性,但也不能保证在任意的类结构变化下反序列化都能成功。
隐式id
根据包名,类名,继承关系,非私有的方法和属性,以及参数,返回值等诸多因子计算得出的,极度复杂生成的一个64位的哈希字段。基本上计算出来的这个值是唯一的。但是一旦类的结构发生变化,我们将会面临很多问题.