1. 概述
Java 反序列化是将字节流转换回对象的过程,通常用于网络传输或持久化存储。Java 提供了 ObjectOutputStream
和 ObjectInputStream
类来实现序列化和反序列化。然而,反序列化过程中可能存在安全漏洞,尤其是在处理不受信任的数据时。
反序列化漏洞是黑客常利用的一种攻击方式,攻击者可以通过精心构造的恶意序列化数据执行任意代码。特别是在 Web 应用、分布式系统、缓存等场景中,反序列化漏洞可能导致严重的安全问题。
2. 序列化与反序列化的基本概念
2.1 序列化
序列化是将对象的状态转换为字节流的过程,以便可以将其存储在文件中或通过网络传输。Java 中的序列化通过实现 Serializable
接口来实现。
2.2 反序列化
反序列化是将字节流转换回对象的过程,恢复对象的状态。Java 中的反序列化通过 ObjectInputStream
类来实现。
反序列化过程中,Java 会依据序列化数据中的类信息来重新构建对象。这是一个高效的操作,但若反序列化数据来源不可信,攻击者可以利用该过程构造恶意数据,从而执行任意代码。
3. 代码示例解析
3.1 用于序列化的对象代码
package com.example.serializationdemo;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
// UserInfo 类需要实现 Serializable 接口,才能被序列化
// Serializable 接口是一个标记接口,没有任何方法,只是用来标识类的对象可以被序列化
publicclassUserInfoimplementsSerializable{
// 成员变量
public String name = "name"; // 用户姓名,默认值为 "name"
publicint age = 18; // 用户年龄,默认值为 18
public String gender = "man"; // 用户性别,默认值为 "man"
// 带参数的构造函数,用于初始化 UserInfo 对象的成员变量
publicUserInfo(String name, int age, String gender){
this.name = name; // 设置用户姓名
this.age = age; // 设置用户年龄
this.gender = gender; // 设置用户性别
}
// 重写 toString 方法,返回 UserInfo 对象的字符串表示形式
public String toString(){
return"UserInfo{" +
"name='" + name + // 返回用户姓名
"'" +
", age=" + age + // 返回用户年龄
", gender='" + gender + // 返回用户性别
"'" + '}';
}
}
关键点:
-
UserInfo
类实现了Serializable
接口,这是序列化的必要条件。 -
Serializable
是一个标记接口,没有方法,仅用于标识类的对象可以被序列化。 -
toString
方法被重写,以便在打印对象时提供有意义的输出。
3.2 序列化代码
package com.example.serializationdemo;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
publicclassSerializationDemo{
publicstaticvoidmain(String[] args)throws IOException {
// 创建一个 UserInfo 对象,传入姓名、年龄和性别
UserInfo u = new UserInfo("ph4uIt", 18,"man");
// 调用序列化方法,将对象序列化并保存到文件中
SerializationTest(u);//ser.txt就是对象u序列化后的字节流数据
}
/**
* 序列化测试方法
* 将传入的对象序列化并保存到文件中
*
* @param obj 需要序列化的对象
* @throws FileNotFoundException 如果文件不存在或无法创建文件时抛出
* @throws IOException 如果写入文件时发生 I/O 错误时抛出
*/
publicstaticvoidSerializationTest(Object obj)throws FileNotFoundException, IOException {
// 创建一个文件输出流,指定输出文件为 "ser.txt"
// ObjectOutputStream 用于将对象序列化并写入文件
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.txt"));
// 将对象写入输出流,完成序列化
oos.writeObject(obj);
// 关闭输出流,释放资源
oos.close();
}
}
关键点:
-
ObjectOutputStream
用于将对象序列化并写入文件。 -
FileOutputStream
用于创建文件输出流,指定输出文件为ser.txt
。 -
writeObject
方法将对象写入输出流,完成序列化。 -
序列化后的字节流数据存储在 ser.txt
文件中。
3.3 反序列化代码
package com.example.serializationdemo;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.ObjectInputStream;
/**
* 反序列化演示类
* 该类用于从文件中读取序列化的对象,并将其反序列化为 Java 对象。
*/
publicclassUnserDemo{
/**
* 主方法,程序的入口
*
* @param args 命令行参数(未使用)
* @throws IOException 如果文件读取失败或文件不存在
* @throws ClassNotFoundException 如果反序列化的类未找到
*/
publicstaticvoidmain(String[] args)throws IOException, ClassNotFoundException {
// 调用反序列化方法,从 "ser.txt" 文件中读取对象
Object obj = UnserTest("ser.txt");
// 打印反序列化后的对象
System.out.println(obj);
}
/**
* 反序列化方法
* 从指定的文件中读取序列化的对象,并将其反序列化为 Java 对象。
*
* @param filename 文件名,表示存储序列化对象的文件路径
* @return 反序列化后的对象
* @throws IOException 如果文件读取失败或文件不存在
* @throws ClassNotFoundException 如果反序列化的类未找到
*/
publicstatic Object UnserTest(String filename)throws IOException, ClassNotFoundException {
// 创建文件输入流,用于读取指定文件
FileInputStream fis = new FileInputStream(filename);
// 创建对象输入流,用于从文件输入流中读取序列化的对象
ObjectInputStream ois = new ObjectInputStream(fis);
// 从对象输入流中读取对象,并将其反序列化为 Java 对象
Object o = ois.readObject();
// 关闭对象输入流,释放资源
ois.close();
// 返回反序列化后的对象
return o;
}
}
关键点:
-
ObjectInputStream
用于从文件中读取序列化的对象。 -
FileInputStream
用于创建文件输入流,指定输入文件为ser. txt
。 -
readObject
方法从输入流中读取对象,并将其反序列化为 Java 对象。 -
反序列化后的对象被打印出来。
4. 反序列化利用链
4.1 入口类的 readObject
直接调用危险方法
反序列化漏洞的一个常见原因是 readObject
方法中直接调用不安全的代码。这通常是因为攻击者可以通过精心构造恶意的序列化数据来执行不受控制的操作。
例如,以下代码展示了如何在反序列化过程中执行系统命令:
privatevoidreadObject(ObjectInputStream ois)throws IOException, ClassNotFoundException {
ois.defaultReadObject(); // 恢复对象的状态
// 反序列化后执行系统命令,启动计算器应用程序
Runtime.getRuntime().exec("calc"); // 存在潜在的安全风险
}
这段代码是一个自定义的
readObject
方法,用于在 Java 反序列化过程中执行特定的逻辑。下面我们详细解析这段代码的每一部分及其作用。代码结构
privatevoidreadObject(ObjectInputStream obj)throws IOException, ClassNotFoundException {
obj.defaultReadObject(); // 默认的反序列化操作
Runtime.getRuntime().exec("calc"); // 执行系统命令
}1.
readObject
方法
readObject
是 Java 中用于自定义反序列化逻辑的方法。当一个对象被反序列化时(例如从文件或网络中读取序列化数据并还原为对象),Java 会调用该对象的readObject
方法(如果存在)。
方法签名:
privatevoidreadObject(ObjectInputStream obj)throws IOException, ClassNotFoundException
ObjectInputStream obj
:用于读取序列化数据的输入流。throws IOException, ClassNotFoundException
:可能抛出的异常,分别表示输入输出错误和类未找到错误。2.
obj.defaultReadObject()
defaultReadObject
是ObjectInputStream
类的一个方法,用于执行默认的反序列化操作。它会按照 Java 默认的序列化规则,从输入流中读取对象的字段值并赋值给当前对象。
作用:
如果没有调用 defaultReadObject
,反序列化时对象的字段将不会被正确初始化。调用 defaultReadObject
是自定义readObject
方法时的常见操作。3.
Runtime.getRuntime().exec("calc")
Runtime.getRuntime().exec("calc")
是 Java 中用于执行系统命令的代码。
**
Runtime.getRuntime()
**:
获取当前 Java 运行时环境的 Runtime
对象。Runtime
类提供了与 Java 运行时环境交互的方法。**
exec("calc")
**:
exec
方法用于执行指定的系统命令。"calc"
是 Windows 系统中的计算器程序。当执行这段代码时,系统会启动计算器。代码的执行流程
反序列化触发:
当一个对象被反序列化时(例如通过 ObjectInputStream.readObject()
),Java 会检查该对象的类是否定义了readObject
方法。如果定义了,Java 会调用该类的 readObject
方法。默认反序列化:
在 readObject
方法中,首先调用obj.defaultReadObject ()
,完成对象的默认反序列化操作(即读取并赋值字段)。执行系统命令:
接着,代码调用 Runtime.getRuntime (). exec ("calc")
,启动 Windows 计算器。代码的潜在问题
安全问题
这段代码存在严重的安全隐患。如果这个类的对象被反序列化,它会执行 Runtime.getRuntime (). exec ("calc")
,这可能会导致任意代码执行。攻击者可以通过构造恶意的序列化数据,触发这段代码的执行,从而执行任意系统命令(例如删除文件、启动恶意程序等)。 反序列化漏洞
这种代码是典型的反序列化漏洞示例。反序列化漏洞通常发生在应用程序接受不受信任的序列化数据时。 攻击者可以通过篡改序列化数据,利用 readObject
方法中的逻辑执行恶意操作。
4.2 入口参数中包含可控类,该类有危险方法,readObject
时调用
反序列化漏洞的原因:
-
入口参数中包含可控类: 反序列化过程中,如果入口参数中包含可控类(即攻击者可以控制的类),攻击者可以通过构造恶意对象来触发危险操作。 -
危险方法的存在: 如果这些可控类中存在危险方法(如 Runtime.getRuntime().exec
),在反序列化时调用这些方法,可能会导致任意代码执行。
示例:
publicclassUserInfoimplementsSerializable{
private String name;
privateint age;
private String gender;
// 重写 toString 方法
@Override
public String toString(){
try {
Runtime.getRuntime (). exec ("calc"); // 危险操作
} catch (IOException e) {
thrownew RuntimeException (e);
}
return"UserInfo{" +
"name='" + name + ''' +
", age=" + age +
", gender='" + gender + ''' +
'}';
}
}
该代码展示了一个简单的
UserInfo
类,它实现了Serializable
接口,并重写了toString
方法,增加了一个潜在的安全漏洞。下面是对该代码的反序列化漏洞分析:Serializable 接口和反序列化
UserInfo
类实现了Serializable
接口,这意味着该类的对象可以被序列化并反序列化。反序列化是将字节流还原为对象的过程。在某些情况下,攻击者可以通过反序列化未经过滤的数据来触发漏洞。toString 方法中的危险操作
toString
方法中存在一个危险的操作:Runtime.getRuntime().exec("calc");
。该代码尝试执行系统命令calc
,即启动计算器应用。这是一个典型的命令注入漏洞示例。
潜在风险:如果该类的对象在反序列化过程中触发了 toString
方法,那么在反序列化对象时,攻击者可以通过精心构造的序列化数据,导致toString
被调用,进而执行恶意命令。反序列化触发 toString 方法
在反序列化过程中,Java 会对对象进行重新构造,而在某些情况下,Java 会调用
toString
方法(例如,在System.out.println()
、日志记录或者字符串拼接时)。如果攻击者能够控制反序列化的过程,那么他们可能在反序列化期间触发toString
方法,从而执行其中的恶意代码。
攻击者的攻击链:攻击者可以通过精心构造一个 UserInfo
对象的序列化数据,诱使目标系统反序列化该数据。由于toString
方法包含危险的命令执行逻辑,反序列化后可能会触发calc
命令,启动计算器,甚至更危险的操作。漏洞利用场景
反序列化漏洞一般出现在数据未经验证或消毒的情况下,例如通过不受信任的网络请求、反序列化的存储文件等。攻击者可以通过提供一个恶意构造的 UserInfo
对象,触发toString
方法中的Runtime.getRuntime().exec("calc")
,执行任意命令。解决方案
禁止执行危险操作:最直接的修复方法是不要在
toString
方法中执行任何危险操作。toString
方法应仅仅用于返回对象的字符串表示,而不应包含任何副作用。使用安全的反序列化方法:可以考虑使用
ObjectInputStream
的resolveClass()
方法来对反序列化的类进行严格校验,或者使用像Jackson
这样的库来控制反序列化过程,避免执行恶意代码。验证输入数据:确保任何反序列化的对象都来源于可信任的源。可以通过加密或签名序列化数据,防止被篡改。
4.3 入口类参数中包含可控类,该类又调用其他有危险方法的类,readObject
时调用
在反序列化过程中,如果一个类的构造函数或 readObject
方法中包含对其他有危险方法的调用,那么攻击者可以通过控制反序列化输入来利用该漏洞。攻击者可以通过精心构造的序列化数据,在反序列化过程中触发不安全的代码。
示例代码
import java.io.*;
import java.util.*;
classDangerousClass{
// 有危险的静态代码块
static {
try {
System.out.println("Executing dangerous operation...");
Runtime.getRuntime().exec("calc"); // 执行危险操作
} catch (IOException e) {
e.printStackTrace();
}
}
}
publicclassControlledInputClassimplementsSerializable{
private String name;
privateint age;
// readObject 方法在反序列化时会调用
privatevoidreadObject(ObjectInputStream ois)throws IOException, ClassNotFoundException {
ois.defaultReadObject();
// 触发 DangerousClass 类的静态代码块
new DangerousClass(); // 这里可能会触发危险的操作
}
publicControlledInputClass(String name, int age){
this.name = name;
this.age = age;
}
@Override
public String toString(){
return"ControlledInputClass{name='" + name + "', age=" + age + "}";
}
publicstaticvoidmain(String[] args)throws Exception {
// 创建一个 ControlledInputClass 对象
ControlledInputClass obj = new ControlledInputClass("Alice", 25);
// 序列化该对象
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("obj.dat"));
oos.writeObject(obj);
oos.close();
// 反序列化该对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("obj.dat"));
ControlledInputClass deserializedObj = (ControlledInputClass) ois.readObject();
System.out.println(deserializedObj);
}
}
解释:
-
DangerousClass
类:该类有一个静态代码块,在类加载时执行。静态代码块中的操作会在类加载时触发,这里调用了Runtime.getRuntime().exec("calc")
,启动计算器。这是一个危险的操作,可能被利用来执行恶意代码。 -
ControlledInputClass
类:该类实现了Serializable
接口,并且在readObject
方法中调用了new DangerousClass()
,从而触发了DangerousClass
类的静态代码块。当反序列化ControlledInputClass
对象时,readObject
方法会被调用,进而触发静态代码块中的危险操作。 -
序列化和反序列化过程:在
main
方法中,首先创建了一个ControlledInputClass
对象并进行了序列化。接着,在反序列化该对象时,由于readObject
方法中的代码,会触发DangerousClass
类的静态代码块,进而执行危险操作(在这里是启动计算器)。
漏洞分析:
-
反序列化时的危险:反序列化过程中,通过
readObject
方法,恶意代码得以执行。如果攻击者能够控制反序列化输入(例如,发送伪造的序列化数据),则有可能触发危险操作。 -
静态代码块的隐式执行:
DangerousClass
类的静态代码块在类加载时自动执行,这种隐式执行的特性使得它在不经过任何显式调用的情况下就能造成安全问题。
4.4 构造函数/静态代码块等类加载时隐式执行
反序列化时,某些类的构造函数或者静态代码块会在类加载过程中隐式执行。如果这些代码中包含有害操作,攻击者可以利用反序列化漏洞在反序列化期间触发这些代码,从而造成安全问题。
示例代码
import java.io.*;
import java.util.*;
publicclassMaliciousClassimplementsSerializable{
private String payload;
// 构造函数中执行危险操作
publicMaliciousClass(){
try {
System.out.println("Executing malicious code...");
Runtime.getRuntime().exec("calc"); // 执行计算器
} catch (IOException e) {
e.printStackTrace();
}
}
publicMaliciousClass(String payload){
this.payload = payload;
}
@Override
public String toString(){
return"MaliciousClass{payload='" + payload + "'}";
}
publicstaticvoidmain(String[] args)throws Exception {
// 创建一个 MaliciousClass 对象
MaliciousClass obj = new MaliciousClass("test");
// 序列化该对象
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("malicious.dat"));
oos.writeObject(obj);
oos.close();
// 反序列化该对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("malicious.dat"));
MaliciousClass deserializedObj = (MaliciousClass) ois.readObject();
System.out.println(deserializedObj);
}
}
解释:
-
MaliciousClass
类:该类的构造函数在对象创建时执行了一个危险操作,即调用Runtime.getRuntime().exec("calc")
,启动计算器。这是在构造函数中隐式执行的恶意代码。 -
反序列化时触发构造函数:反序列化过程中,
MaliciousClass
对象的构造函数会被调用,进而执行其中的恶意代码。 -
序列化和反序列化过程:在
main
方法中,创建了一个MaliciousClass
对象并进行序列化。反序列化时,构造函数被自动调用,启动计算器。
漏洞分析:
-
隐式执行危险操作:构造函数中的恶意代码在对象创建时隐式执行,攻击者通过反序列化时触发构造函数,从而执行恶意操作。
-
反序列化控制:如果攻击者能够控制反序列化的输入数据,就能在反序列化期间触发危险的构造函数或静态代码块,进而执行恶意代码。
原文始发于微信公众号(泷羽Sec-sea):JAVA 反序列化学习笔记
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论