java反序列化


什么是序列化?

  • 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package org.com.serial;  

public class Employee implements java.io.Serializable{
private String name;
private int old;
// 无法被反序列化属性
public transient int address;
public Employee(String name,int old){
this.name=name;
this.old=old;
}
@Override
public String toString(){
return "Your name:"+this.name+"Your old:"+this.old;
}
}

创建另外一个java文件进行对于这个类的对象做一个反序列化

1
2
3
4
5
6
7
8
9
10
11
package org.com.serial;
import java.io.*;

public class SerializeDemo {
public static void main(String[] args) throws Exception{
Employee test=new Employee("tom",12);
ObjectOutputStream output=new ObjectOutputStream(new FileOutputStream("d:/1.ser"));
output.writeObject(test);
System.out.println(test);
}
}

image.png

FileOutputStream
1
2
3
4
FileOutputStream fos = new FileOutputStream("ser.txt");
String data = "Hello, World!";
fos.write(data.getBytes());
fos.close();
ObjectInputStream类

如果能找到一个对象的class文件,我们可以进行反序列化操作,调用 ObjectInputStream 读取对象的 方法:打印结果:反序列化操作就是从二进制文件中提取对象

编写反序列化代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package org.com.serial;  
import java.io.*;

public class SerializeDemo {
public static void main(String[] args) throws Exception{
Employee test=new Employee("tom",12);
ObjectOutputStream output=new ObjectOutputStream(new FileOutputStream("d:/1.ser"));
output.writeObject(test);
output.close();
ObjectInputStream input=new ObjectInputStream(new FileInputStream("d:/1.ser"));
Object NEW =input.readObject();
System.out.println(NEW);

}
}

漏洞成因

了解漏洞成因之前,我们首先要了解的是readobject方法。我们知道创建一个ObjectInputStream类的对象可以直接调用readobject方法,但是在这个类中,readobject方法是没有被具体实现的。而且他还有一个defaultreadobject方法,那么问题就是既然这个readobject方法没有被实现,那我们为什么还能使用他进行反序列化操作?

内部逻辑

  • 调用 readObject 方法:当你调用 ObjectInputStreamreadObject 方法时,它会读取流中的数据,了解要反序列化的对象类型。

  • 内部逻辑:虽然你没有看到具体的实现,但 ObjectInputStream 具有内部机制来处理对象的创建和字段的恢复。这包括:

    • 创建对象实例。
    • 读取对象的字段并根据需要恢复它们的状态。
  • 反序列化过程:在这个过程中,Java 使用反射和其他机制来确保对象的状态被正确恢复。这样,即使 readObject 方法没有用户自定义的逻辑,它仍然可以执行必要的步骤来完成反序列化。

了解了这个原因,我们就可以了解另外一个,我们可以重写readobject方法,并且我们可以在带有标记接口Serializable 的类中进行重写,那么又有一个问题既然我们的这个readobject方法是采用内部逻辑的方式去实现反序列化,那我们重写了之后我们无法利用内部逻辑,此时我们就需要刚才提到的defaultreadobject方法,我们可以在这个方法执行默认的反序列化逻辑,恢复对象的非瞬态字段。同时我们可以自己加一些逻辑代码进去。这也是我们能够利用的关键。

但是问题又来了? 既然带有标记接口Serializable的这个类,继承的接口没有readobject方法,我们为什么还能在这个继承了标记接口的类中重写readobject方法。

ObjectInputStream 能找到你自定义的 readObject 方法是因为反序列化的过程是通过类的元信息(反射)来实现的。具体来说:

  1. 类的元信息:当你反序列化一个对象时,ObjectInputStream 会首先读取对象的类信息,包括类名和字段。

  2. 查找方法:在获取到类的信息后,ObjectInputStream 会检查该类是否定义了一个私有的 readObject 方法。这个检查是通过反射进行的,Java 反射机制允许程序在运行时查询类的结构和方法。

  3. 调用自定义方法:如果找到了这个自定义的 readObject 方法,ObjectInputStream 就会调用它,而不是执行默认的反序列化逻辑。这使得你可以在反序列化过程中添加自定义逻辑。

重点是java的反射机制。[[反射机制]]

实现

经过刚才的分析,我们大致已经了解了,反序列化我们为什么可以控制。就是因为java允许我们重写readobject这个方法。导致我们可以利用反射机制,在我们序列化的这个类的中找到对应的重写的readobject方法。从而执行我们的逻辑,但是要保证我们的readobject方法一定是private的。

设置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package org.com.serial;

import java.io.ObjectInputStream;

public class Employee implements java.io.Serializable{
private String name;
private int old;
// 无法被反序列化属性
public transient int address;
public String cmd="calc.exe";
public Employee(String name,int old){
this.name=name;
this.old=old;
}
@Override
public String toString(){
return "Your name:"+this.name+"Your old:"+this.old+"cmd"+this.cmd;
}
private void readObject(ObjectInputStream stream) throws Exception {
stream.defaultReadObject();
Runtime.getRuntime().exec(this.cmd);
}
}


序列化和反序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package org.com.serial;
import java.io.*;

public class SerializeDemo {
public static void Serial(Object obj) throws Exception{
ObjectOutputStream output=new ObjectOutputStream(new FileOutputStream("d:/1.ser"));
output.writeObject(obj);
output.close();
}
public static void Deserial() throws Exception{
ObjectInputStream input=new ObjectInputStream(new FileInputStream("d:/1.ser"));
Object NEW =input.readObject();
input.close();
System.out.println(NEW);
}
public static void main(String[] args) throws Exception{
Employee test=new Employee("tom",12);
Serial(test);
Deserial();
}
}

● 入口类 source (即找到重写 readObject方法,调用常见的函数,参数类型宽泛 最好 jdk 自带)
● 调用链 gadget chain (基于类的默认方式调用)
● 执行类 sink (RCE、SSRF、写文件等操作)

serialVersionUID

  • 在序列化和反序列化过程中,确保对象的兼容性。当对象被序列化后,如果该类的结构发生了变化,比如添加、删除或修改了成员变量、方法等,反序列化时可能会出现问题。
  • 通过显式地指定serialVersionUID,可以在一定程度上控制不同版本的类之间的兼容性。如果序列化和反序列化时的serialVersionUID一致,那么 Java 虚拟机认为这两个版本是兼容的,可以进行反序列化操作
显示id

静态变量和被设置的属性不会被序列化,因此我们一般都是设置一个静态变量.作为显示id

显式地定义了一个serialVersionUID,在类的结构发生变化时,反序列化不一定没有影响。
如果类的结构变化是向后兼容的,比如添加了新的不参与序列化的方法、添加了新的具有默认值的成员变量等,那么在反序列化时通常不会有问题。
但是,如果类的结构变化影响到了已序列化对象的数据,比如删除了成员变量、修改了成员变量的类型等,那么在反序列化时仍然可能会出现问题。
例如,假设一个类原本有一个名为name的字符串成员变量,序列化了一个对象后,将这个类中的name成员变量删除了,再进行反序列化时就会抛出异常。
所以,虽然定义了serialVersionUID可以在一定程度上提高兼容性,但也不能保证在任意的类结构变化下反序列化都能成功。

隐式id

根据包名,类名,继承关系,非私有的方法和属性,以及参数,返回值等诸多因子计算得出的,极度复杂生成的一个64位的哈希字段。基本上计算出来的这个值是唯一的。但是一旦类的结构发生变化,我们将会面临很多问题.


文章作者: K1T0
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 K1T0 !
  目录