Java反序列化

分类: 约彩365app下载 时间: 2025-07-20 23:29:41 作者: admin 阅读: 2755 点赞: 335
Java反序列化

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()方法。

ObjectInputStream

这个类和反序列化相关,它可以读取 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。那么为什么会出现这样的结果呢?解答如下(个人理解):

有想过为什么在反序列化的时候要引入包吗,

同时可以看给出来的ser.ser文件的十六进制表示,其中并没有序列化方法,并且只存在sex(这是因为其他两个变量都被我设置为了static,所以不会被序列化),那么是否想过为什么在序列化的时候没有序列化方法。

个人理解如下,对于序列化,它只序列化对象的的状态,而方法属于类的定义部分,不属于对象的状态部分,所以方法并不是被序列化,所以,如果我们想要再次利用这个Test类,需要引入包,从而使得可以对应到方法来利用。

两种不同结果的利用方法最大的不同在于一个地方,JVM加载进程。

在分开序列化和反序列化时,是分别运行了两次,即进行了两次JVM加载,但是这个static静态加载是存在于“当前”进程的,并且看前面序列化后的文件内容,是不存在static关键字修饰的变量的,static修饰的变量是绑定在对象上的,而是直接存在于内存中的,所以在第二次加载时会直接将内存中存在的fupanc赋值给name。

而序列化和反序列化一起,对比一下,基本就清楚了,同时进行,在序列化之前将static修饰的name改为haha,并加载进了内存中,然后在反序列化时直接在内存中找到了这个值赋值给了name。

综上,造成这个差异的主要有两个点:

序列化的特性

“JVN加载特性”

一个知识点

这里需要注意的一个点,我们可以通过在待序列化或反序列化的类中定义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

相关推荐