Java反序列化
在Java中,序列化过程分为两大部分:序列化和反序列化。
- 序列化:将对象的状态转换为可存储或传输的格式的过程。例如,将对象转换为字节流或文本格式(如 JSON、XML 等)。这样可以将对象保存到文件、数据库或者通过网络传输。
- 反序列化:将序列化后的数据恢复为对象的过程。也就是说,将字节流或文本格式的数据重新转换为内存中的对象。
这两部分共同构成了序列化过程,确保对象可以被持久化存储或远程传输,并在需要时恢复原始的对象状态。
如何实现
在Java中实现对象反序列化非常简单,实现java.io.Serializable(内部序列化)
或java.io.Externalizable(外部序列化)
接口即可被序列化。下面有几个点需要说明:
Serializable 接口
源代码如下:
1
2
|
public interface Serializable {
}
|
一个对象想要被序列化,那么它的类就要实现此接口或者它的子接口。
这个对象的所有属性(包括private属性和其引用的对象)都可以被序列化和反序列化来保存、传递。不想序列化的字段可以使用transient修饰。
由于 Serializable 对象完全以它存储的二进制位为基础来构造,因此并不会调用任何构造函数,因此Serializable类无需默认构造函数,但是当Serializable类的父类没有实现Serializable接口时,反序列化过程会调用父类的默认构造函数,因此该父类必需有默认构造函数,否则会抛异常。
使用transient关键字阻止序列化虽然简单方便,但被它修饰的属性被完全隔离在序列化机制之外,导致了在反序列化时无法获取该属性的值,而通过在需要序列化的对象的Java类里加入writeObject()方法与readObject()方法可以控制如何序列化各属性,甚至完全不序列化某些属性或者加密序列化某些属性。
在这里还需要了解一个点,那就是static修饰的字段是绑定在类上的,而不是对象上。static优先于对象存在,所以static修饰的字段不会被序列化。
Externalizable 接口
对于这个接口的使用可以参考最后面的参考文章。
源代码如下:
1
2
3
4
5
6
7
|
public interface Externalizable extends java.io.Serializable {
void writeExternal(ObjectOutput out) throws IOException;
void readExternal(ObjectInput in) throws IOException,ClassNotFoundException;
}
|
它是Serializable接口的子类,这个接口里面定义了两个抽象的方法,用户需要重载writeExternal()和readExternal()方法,用来决定如何序列化和反序列化。
因为序列化和反序列化方法需要自己实现,因此可以指定序列化哪些属性,而transient在这里无效。
对Externalizable对象反序列化时,会先调用类的无参构造方法,这是有别于默认反序列方式的。如果把类的不带参数的构造方法删除,或者把该构造方法的访问权限设置为private、默认或protected级别,会抛出java.io.InvalidException: no valid constructor异常,因此Externalizable对象必须有默认构造函数,而且必需是public的。
serialVersionUID字段
这个字段可以在序列化过程中控制序列化的版本。一般格式就是下面这个:

一个对象数据,在反序列化过程中,如果序列化串中的serialVersionUID与当前对象值不同,则反序列化失败,会报错,否则成功。
如果serialVersionUID没有显式生成,系统就会自动生成一个。属性的变化都会导致自动生成的serialVersionUID发生变化。如果序列化和反序列化的serialVersionUID不同,则会报序列化版本不同的错误。
如果我们保持了serialVersionUID的一致,则在反序列化时,对于新增的字段会填入默认值null(int的默认值0),对于减少的字段则直接忽略。
其他类

如上图,现在我们来了解一下ObjectInputStream和ObjectOutputStream。
ObjectOutputStream
这个类与序列化相关
部分源码如下:
1
2
|
public class ObjectOutputStream extends OutputStream implements ObjectOutput, ObjectStreamConstants
{}
|
java.io.ObjectOutputStream
继承自 OutputStream 类,因此可以将序列化后的字节序列写入到文件、网络等输出流中。
现在来看这个类的构造方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public ObjectOutputStream(OutputStream out) throws IOException {
verifySubclass();
bout = new BlockDataOutputStream(out);
handles = new HandleTable(10, (float) 3.00);
subs = new ReplaceTable(10, (float) 3.00);
enableOverride = false;
writeStreamHeader();
bout.setBlockDataMode(true);
if (extendedDebugInfo) {
debugInfoStack = new DebugTraceInfoStack();
} else {
debugInfoStack = null;
}
}
|
该构造方法接收一个 OutputStream 对象作为参数,并且在实例化时将变量enableOverride设置为false。
例如:
1
2
3
4
|
//这里会创建一个FileOutputStream流以写入数据到File对象所代表的文件
FileOutputStream fos = new FileOutputStream("file.txt");
//
ObjectOutputStream oos = new ObjectOutputStream(fos);
|
这里序列化想要利用就要用到ObjectOutputStream
这个类的writeObject
方法,writeObject()方法源码如下:

前面在实例化时将enableOverride设置为false,所以这里真正起作用的是writeObject0()方法。
这个类和反序列化相关,它可以读取 ObjectOutputStream 写入的字节流,并将其反序列化为相应的对象。
部分构造函数源码如下:
1
2
3
4
5
6
7
8
9
|
public ObjectInputStream(InputStream in) throws IOException {
verifySubclass();
bin = new BlockDataInputStream(in);
handles = new HandleTable(10);
vlist = new ValidationList();
enableOverride = false;
readStreamHeader();
bin.setBlockDataMode(true);
}
|
核心方法是readObject(),源码如下:
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
|
public final Object readObject()
throws IOException, ClassNotFoundException
{
if (enableOverride) {
return readObjectOverride();
}
// if nested read, passHandle contains handle of enclosing object
int outerHandle = passHandle;
try {
Object obj = readObject0(false);
handles.markDependency(outerHandle, passHandle);
ClassNotFoundException ex = handles.lookupException(passHandle);
if (ex != null) {
throw ex;
}
if (depth == 0) {
vlist.doCallbacks();
}
return obj;
} finally {
passHandle = outerHandle;
if (closed && depth == 0) {
clear();
}
}
}
|
可以看到最后返回的是反序列化后的对象。
序列化
测试代码如下:
Test.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
package org.example;
import java.io.Serializable;
class Test implements Serializable{
private String name = "fupanc" ;
protected char height = 'A' ;
public transient String sex = "boy";
private static int age = 1111 ;
public void setName(String name){
this.name=name;
}
public String getName(){
return this.name;
}
public String getSex(){
return this.sex;
}
public int getAge(){
return this.age;
}
}
|
Main.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
package org.example;
import java.io.ObjectOutputStream;
import java.io.FileOutputStream;
class Main{
public static void main(String[] args) throws Exception {
Test p = new Test();
p.setName("haha");
ObjectOutputStream x = new ObjectOutputStream(new FileOutputStream("ser.ser"));
x.writeObject(p);
x.close();
}
}
|
成功生成ser.ser文件,十六进制打开看一下,如下:

可以看出这里只序列化了height和name,而sex和age并没有被序列化。所以在这里就可以知道正如前面说的,使用transient和static修饰的变量不会被序列化。
反序列化
反序列化对象时有如下限制:
- 被反序列化的类必须存在。
serialVersionUID
值必须一致。
同样使用上面的Test.java。这里就只给Main.java,反序列化代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
package org.example;
import java.io.FileInputStream;
import java.io.ObjectInputStream;
class Main {
public static void main(String[] args) throws Exception {
ObjectInputStream p = new ObjectInputStream(new FileInputStream("ser.ser"));
Test ctf = (Test)p.readObject();//这里由于返回类型不同,需要强制转换
System.out.println("反序列化后的name:"+ctf.getName());
System.out.println("反序列化后的sex:"+ctf.getSex());
System.out.println("反序列化后的age:"+ctf.getAge());
}
}
|
输出结果如下:
1
2
3
|
反序列化后的name:haha
反序列化后的sex:null
反序列化后的age:1111
|
解读一下结果:
- sex为null,就是因为我在Test类用transient修饰,所以在序列化时并不会将sex字段序列化,所以这里并没有值。
- age为1111,这就与static有关了,这是因为static为全局变量,在JVM中所有实例都会共享该字段。
对比一下,刚好可以再说明一个点
Test.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
package org.example;
import java.io.Serializable;
class Test implements Serializable{
private static String name = "fupanc" ; //这里添加static
public String sex = "boy"; //这里去除transient
private static int age = 1111 ;
public void setName(String name){
this.name=name;
}
public String getName(){
return this.name;
}
public String getSex(){
return this.sex;
}
public int getAge(){
return this.age;
}
}
|
Main.java (省去序列化过程,注意这里是将序列化和反序列化分开进行的)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
package org.example;
import java.io.ObjectOutputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.FileInputStream;
class Main{
public static void main(String[] args){
try{
//反序列化
ObjectInputStream y = new ObjectInputStream(new FileInputStream("ser.ser"));
Test ctf = (Test)y.readObject();
System.out.println("反序列化后的name:"+ctf.getName());
System.out.println("反序列化后的sex:"+ctf.getSex());
System.out.println("反序列化后的age:"+ctf.getAge());
} catch(Exception e){
e.printStackTrace();
}
}
}
|
输出为:
1
2
3
|
反序列化后的name:fupanc
反序列化后的sex:boy
反序列化后的age:1111
|
从这个结果可以更加说明static修饰的字段不会被序列化的特性,以及更加清楚了是否使用transient结果的不同。
但是在前一个测试过程中,发现了一个问题,
Test.java还是之前那个:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
package org.example;
import java.io.Serializable;
class Test implements Serializable{
private static String name = "fupanc" ; //这里添加static
public String sex = "boy"; //这里去除transient
private static int age = 1111 ;
public void setName(String name){
this.name=name;
}
public String getName(){
return this.name;
}
public String getSex(){
return this.sex;
}
public int getAge(){
return this.age;
}
}
|
Main.java内容如下:
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
|
package org.example;
import java.io.ObjectOutputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.FileInputStream;
class Main{
public static void main(String[] args){
try{
//序列化
Test p = new Test();
p.setName("haha");
ObjectOutputStream x = new ObjectOutputStream(new FileOutputStream("ser.ser"));
x.writeObject(p);
x.close();
//反序列化
ObjectInputStream y = new ObjectInputStream(new FileInputStream("ser.ser"));
Test ctf = (Test)y.readObject();
System.out.println("反序列化后的name:"+ctf.getName());
System.out.println("反序列化后的sex:"+ctf.getSex());
System.out.println("反序列化后的age:"+ctf.getAge());
} catch(Exception e){
e.printStackTrace();
}
}
}
|
运行结果:
1
2
3
|
反序列化后的name:haha
反序列化后的sex:boy
反序列化后的age:1111
|
注意这里的name的结果为haha,但是之前分开序列化和反序列化时的name的值为fupanc。那么为什么会出现这样的结果呢?解答如下(个人理解):

个人理解如下,对于序列化,它只序列化对象的的状态,而方法属于类的定义部分,不属于对象的状态部分,所以方法并不是被序列化,所以,如果我们想要再次利用这个Test类,需要引入包,从而使得可以对应到方法来利用。
两种不同结果的利用方法最大的不同在于一个地方,JVM加载进程。
在分开序列化和反序列化时,是分别运行了两次,即进行了两次JVM加载,但是这个static静态加载是存在于“当前”进程的,并且看前面序列化后的文件内容,是不存在static关键字修饰的变量的,static修饰的变量是绑定在对象上的,而是直接存在于内存中的,所以在第二次加载时会直接将内存中存在的fupanc赋值给name。
而序列化和反序列化一起,对比一下,基本就清楚了,同时进行,在序列化之前将static修饰的name改为haha,并加载进了内存中,然后在反序列化时直接在内存中找到了这个值赋值给了name。
综上,造成这个差异的主要有两个点:
一个知识点
这里需要注意的一个点,我们可以通过在待序列化或反序列化的类中定义readObject
和writeObject
方法,来实现自定义的序列化和反序列化操作,当然前提是,被序列化的类必须有此方法,并且方法的修饰符必须是private
。代码参考如下:
Test.java
1
2
3
4
5
6
7
8
9
10
|
package org.example;
import java.io.Serializable;
class Test implements Serializable{
public String cmd;
private void readObject(java.io.ObjectInputStream stream) throws Exception{
stream.defaultReadObject();//调用ObjectInputStream默认反序列化方法
Runtime.getRuntime().exec(cmd);
}
}
|
Main.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
package org.example;
import java.io.*;
class Main{
public static void main(String[] args) throws Exception{
Test haha = new Test();
haha.cmd = "calc";
ObjectOutputStream obj = new ObjectOutputStream(new FileOutputStream("haha.ser"));
obj.writeObject(haha);
obj.close();
ObjectInputStream ceshi = new ObjectInputStream(new FileInputStream("haha.ser"));
ceshi.readObject();
ceshi.close();
}
}
|
成功弹出计算机

这样就确实自定义了反序列化的方法,序列化同理。
ysoserial工具
ysoserial集合了各种java反序列化的利用链。
利用链也叫"gadget chains",我们通常称为gadget。
直接下载编译好的jar文件就能用:
1
|
https://github.com/frohoff/ysoserial
|
使用很简单,如下简单POC:
1
|
java -jar ysoserial-master.jar CommonsCollections1 "id"
|
参考文章:
https://javasec.org/javase/JavaDeserialization/Serialization.html
https://blog.csdn.net/mocas_wang/article/details/107621010