免责声明
本公众号着重分享技术内容,所提供信息旨在促进技术交流。对于因信息使用而引起的任何损失,本公众号及作者概不负责。请谨慎行事,安全至上。
前言
深入理解java反序列化漏洞并非一帆风顺的过程,因其涉及到的一系列复杂技术要点,一直都是渗透、红队工作中必须掌握的技术之一!本文分为10个要点,带领读者去探索java反序列化!
1. 序列化与反序列化概念
-
序列化(Serialization):将对象转换为字节序列的过程,以便在网络上传输或持久化存储到文件中。
-
反序列化(Deserialization):从字节序列中重新构造对象的过程。
2. Serializable 接口
-
java.io.Serializable 接口是一个标记接口(marker interface),用于标识类的对象可以被序列化。
-
实现 Serializable 接口的类可以将其对象转换为字节流以便存储或传输,例如网络传输、文件存储等。
3. 对象的序列化与反序列化
-
ObjectOutputStream 类可以用于将对象序列化为字节流。
-
ObjectInputStream 类可以用于从字节流中反序列化对象。
4. transient 关键字
在 Java 中,transient 是一个关键字,用于修饰类的成员变量。当一个变量被 transient 修饰时,表示它不会被对象序列化的一部分。
在对象序列化的过程中,对象的状态被转换为字节流以便于存储或传输。然而,并非所有对象的所有部分都适合序列化。有时候,某些对象的特定字段,例如临时变量、不必要的缓存或者安全敏感信息,不应该被序列化。这时候就可以使用 transient 关键字来标记这些字段,防止它们被序列化。
transient 关键字的作用是告诉 Java 虚拟机在序列化该对象时跳过被标记为 transient 的字段,不将其包含在序列化的数据中。当对象被反序列化时,这些字段会被赋予默认值,而不是之前序列化时的值。
以下是一个简单的示例,展示了 transient 关键字的使用:
import java.io.*;
class User implements Serializable {
private String username;
private transient String password;
public User(String username, String password) {
this.username = username;
this.password = password;
}
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
}
public class SerializationExample {
public static void main(String[] args) {
User user = new User("Alice", "password123");
// 将 User 对象序列化到文件
try {
FileOutputStream fileOut = new FileOutputStream("user.ser");
ObjectOutputStream objectOut = new ObjectOutputStream(fileOut);
objectOut.writeObject(user);
objectOut.close();
fileOut.close();
System.out.println("User对象已经被序列化到user.ser文件中");
} catch (IOException e) {
e.printStackTrace();
}
// 从文件中反序列化User对象
try {
FileInputStream fileIn = new FileInputStream("user.ser");
ObjectInputStream objectIn = new ObjectInputStream(fileIn);
User deserializedUser = (User) objectIn.readObject();
objectIn.close();
fileIn.close();
System.out.println("从文件中反序列化得到的User对象:" + deserializedUser.getUsername());
System.out.println("密码:" + deserializedUser.getPassword()); // 输出为null,因为password字段被标记为 transient,不会被序列化
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
在上述示例中,User 类中的 password 字段被标记为 transient,因此在序列化和反序列化过程中不会被包含。在反序列化后,password 字段的值将会是默认值 null,而不是初始时的 "password123"。
5.文件 I/O
-
了解 Java 中的文件读写操作,特别是和字节流相关的文件读写操作。
-
文件 I/O(Input/Output)指的是在计算机中进行文件输入和输出操作的过程。在编程中,文件 I/O 是指程序与外部文件系统进行交互,读取或写入文件的过程。
Java 中的文件 I/O 涉及到使用类库中的类和方法来操作文件。一些关键的类包括:
-
InputStream 和 OutputStream: 这些是字节流操作类。InputStream 用于从文件中读取字节,OutputStream 用于向文件写入字节。
-
Reader 和 Writer: 这些是字符流操作类。Reader 用于从文件中读取字符,Writer 用于向文件写入字符。
Java 的文件 I/O 涉及以下操作:
-
读取文件内容: 使用适当的输入流(如 FileInputStream 或 BufferedReader)来读取文件中的数据。
-
写入到文件: 使用适当的输出流(如 FileOutputStream 或 BufferedWriter)来将数据写入文件。
-
文件的读写和操作: 可以创建、复制、移动、删除文件,以及创建文件夹等操作。
以下是 Java 中文件 I/O 的基本示例:
文件读取示例:
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class ReadFileExample {
public static void main(String[] args) {
try {
BufferedReader reader = new BufferedReader(new FileReader("example.txt"));
String line = reader.readLine();
while (line != null) {
System.out.println(line);
line = reader.readLine();
}
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
文件写入示例:
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class ReadFileExample {
public static void main(String[] args) {
try {
BufferedReader reader = new BufferedReader(new FileReader("example.txt"));
String line = reader.readLine();
while (line != null) {
System.out.println(line);
line = reader.readLine();
}
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
在这些示例中,BufferedReader 和 BufferedWriter 类被用来读取和写入文件,FileReader 和 FileWriter 用来将文件连接到程序中。文件 I/O 是处理文件数据的重要部分,而且在许多应用程序中都是必不可少的。
6. 异常处理
异常处理是编程中一种用于处理程序运行过程中出现的异常情况的机制。在程序执行过程中,可能会出现无法预料的情况,比如文件不存在、内存溢出、网络连接中断等,这些情况可能导致程序出现错误或崩溃。异常处理提供了一种机制,使得程序能够优雅地处理这些异常情况,而不至于完全终止执行。
在 Java 和许多其他编程语言中,异常处理涉及以下几个概念:
-
异常(Exception): 异常是指程序执行期间发生的不正常情况。在 Java 中,异常通常是指继承自 Throwable 类的对象,包括 Exception(编译时异常)和 RuntimeException(运行时异常)等。
-
抛出异常(Throwing an Exception): 当发生异常情况时,可以使用 throw 关键字手动抛出异常。
-
捕获异常(Catching an Exception): 使用 try-catch 块捕获可能抛出的异常,以便进行适当的处理。try 块中包含可能引发异常的代码,而 catch 块用于捕获并处理异常。
-
处理异常(Handling Exceptions): 处理异常意味着在异常发生时采取适当的措施,比如提供备用的逻辑、记录错误信息、恢复程序状态等。
-
finally 块:finally 块是一个可选的部分,在 try-catch 块之后,无论是否发生异常,其中的代码都会被执行。通常用于确保资源被正确释放,比如关闭文件、数据库连接等。
以下是一个简单的 Java 异常处理示例:
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class ExceptionHandlingExample {
public static void main(String[] args) {
FileInputStream file = null;
try {
file = new FileInputStream("example.txt");
// 读取文件内容
// ...
} catch (FileNotFoundException e) {
System.out.println("文件未找到:" + e.getMessage());
} catch (IOException e) {
System.out.println("IO 异常:" + e.getMessage());
} finally {
try {
if (file != null) {
file.close();
}
} catch (IOException e) {
System.out.println("关闭文件时发生异常:" + e.getMessage());
}
}
}
}
在上述示例中,代码尝试打开一个文件并读取内容,使用了 try-catch 块来捕获可能发生的 FileNotFoundException 和 IOException 异常。无论是否发生异常,finally 块中的代码都会尝试关闭文件流,确保资源得到释放。这有助于程序在异常情况下安全地结束执行,并且对资源进行清理。
7. 了解 Java 的类加载机制
-
了解类加载器、类加载过程对反序列化的影响,特别是在反序列化时确保类的可用性。
Java 的类加载机制是 Java 运行时环境中的重要部分,它负责加载并初始化类和接口。类加载器(ClassLoader)是实现这一机制的组件之一。Java 类加载机制遵循以下步骤:
1. 加载(Loading):
-
类加载的第一个阶段是加载。在这个阶段,类加载器负责查找并加载类的字节码文件。字节码文件可以来自本地文件系统、网络或其他来源。
-
Java 类加载器将字节码文件加载到内存中,创建一个代表该类的 Class 对象。
2. 链接(Linking):
-
链接阶段分为三个子阶段:验证(Verification)、准备(Preparation)和解析(Resolution)。
-
验证(Verification): 验证阶段确保加载的类符合 Java 虚拟机规范,不会危害虚拟机的安全性。
-
准备(Preparation): 在准备阶段,类加载器为类的静态变量分配内存空间,并设置默认初始值。
-
解析(Resolution): 解析阶段是可选的,它将符号引用转换为直接引用。这个阶段可能包括将类、接口、方法和字段的引用解析为直接引用。
3. 初始化(Initialization):
-
类加载的最后一个阶段是初始化。在此阶段,类的静态变量会被初始化为程序中定义的初始值,并执行类构造器 <clinit>() 方法的代码块。这个阶段标志着类的实际初始化。
4. 使用(Usage):
-
类加载完成后,类就可以被程序中其他部分所使用。
类加载器(ClassLoader):
Java 类加载器是负责加载类的重要组件。它们分为多个层次,形成了类加载器的层级结构。常见的类加载器包括:
-
启动类加载器(Bootstrap Class Loader): 负责加载核心 Java API,是由本地代码实现的一部分,不是 Java 类,是类加载器层次的顶级。
-
扩展类加载器(Extension Class Loader): 负责加载 Java 的扩展库,它是 sun.misc.Launcher$ExtClassLoader 类的实例。
-
应用程序类加载器(Application Class Loader): 也称为系统类加载器,负责加载应用程序的类路径上指定的类。它是 sun.misc.Launcher$AppClassLoader 类的实例。
双亲委派模型:
Java 类加载器采用了双亲委派模型。当一个类加载器需要加载某个类时,它首先将这个请求委托给父加载器。只有在父加载器无法完成加载请求时,子加载器才会尝试加载类。这种机制有助于保证类的唯一性和安全性,防止类被恶意代码替换或修改。
8.反序列化例子
import java.io.*;
class User implements Serializable {
private String username;
private int age;
public User(String username, int age) {
this.username = username;
this.age = age;
}
public String getUsername() {
return username;
}
public int getAge() {
return age;
}
}
public class DeserializationExample {
public static void main(String[] args) {
try {
FileInputStream fileIn = new FileInputStream("user.ser");
ObjectInputStream objectIn = new ObjectInputStream(fileIn);
// 从文件中反序列化User对象
User deserializedUser = (User) objectIn.readObject();
objectIn.close();
fileIn.close();
// 输出反序列化得到的User对象的信息
System.out.println("从文件中反序列化得到的User对象:" + deserializedUser.getUsername());
System.out.println("年龄:" + deserializedUser.getAge());
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
这个示例假设之前已经通过序列化将一个 User 对象保存到名为 "user.ser" 的文件中。现在,通过使用 ObjectInputStream 类,我们打开文件并将存储的对象反序列化为一个 User 对象。最后,我们可以使用得到的 User 对象来访问其属性并对其进行操作。
确保在运行此代码之前,已经创建了包含序列化 User 对象的文件 "user.ser"。这样,你就可以使用上述代码从文件中读取对象,并将其反序列化为 Java 对象。
9.ObjectOutputStream、ObjectInputStream类与InputStream、OutputStream
ObjectOutputStream 和 ObjectInputStream 类是 Java 中用于序列化和反序列化对象的类,它们允许将对象转换为字节流以便于存储或传输,并在需要时将其重新构造为对象。这两个类通常与 InputStream 和 OutputStream 接口结合使用,但它们有着不同的功能和作用。
ObjectOutputStream 与 ObjectInputStream:
-
序列化与反序列化:
-
ObjectOutputStream:它负责将对象序列化为字节流。通过其 writeObject() 方法,你可以将对象写入输出流。
-
ObjectInputStream:用于从字节流中读取并反序列化对象。其 readObject() 方法用于从输入流中读取对象。
-
对象操作:
-
ObjectOutputStream 和 ObjectInputStream 可以直接处理对象。你可以在这些流上直接操作对象,而不仅仅是字节或原始数据类型。
-
文件和网络通信:
-
通常,这两个类结合 FileOutputStream 或 FileInputStream(用于文件操作)或者 Socket 中的 getOutputStream() 和 getInputStream()(用于网络通信)来将对象写入文件或通过网络发送。
InputStream 和 OutputStream:
-
字节流操作:
-
InputStream 和 OutputStream 是抽象类或接口,用于处理字节流。它们是处理字节数据的基本类。
-
提供了读取和写入字节的方法(例如 read() 和 write()),能够处理字节数据的输入和输出。
-
数据处理:
-
这两个类专注于字节数据的传输,需要额外的处理才能在更高层次上处理对象。
-
文件和网络通信:
-
与 ObjectInputStream 和 ObjectOutputStream 不同,InputStream 和 OutputStream 专门用于字节流的读取和写入。它们常用于处理文件操作和网络通信。
10. 注意事项
-
需要确保序列化和反序列化的类具有相同的 serialVersionUID。如果不同,可能会导致反序列化失败或数据不一致的问题。
-
被序列化的类及其内部所有引用的类都必须是可序列化的,否则会抛出 NotSerializableException 异常。
点击关注下方名片进入公众号
原文始发于微信公众号(大仙安全说):保障代码安全:解析Java反序列化
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论