JAVA 反序列化学习笔记

admin 2025年3月5日21:10:37评论7 views字数 10458阅读34分51秒阅读模式

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 系统中的计算器程序。当执行这段代码时,系统会启动计算器。

代码的执行流程

  1. 反序列化触发

    • 当一个对象被反序列化时(例如通过 ObjectInputStream.readObject()),Java 会检查该对象的类是否定义了 readObject 方法。
    • 如果定义了,Java 会调用该类的 readObject 方法。
  2. 默认反序列化

    • 在 readObject 方法中,首先调用 obj.defaultReadObject (),完成对象的默认反序列化操作(即读取并赋值字段)。
  3. 执行系统命令

    • 接着,代码调用 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);
    }
}

解释:

  1. DangerousClass 类:该类有一个静态代码块,在类加载时执行。静态代码块中的操作会在类加载时触发,这里调用了 Runtime.getRuntime().exec("calc"),启动计算器。这是一个危险的操作,可能被利用来执行恶意代码。

  2. ControlledInputClass 类:该类实现了 Serializable 接口,并且在 readObject 方法中调用了 new DangerousClass(),从而触发了 DangerousClass 类的静态代码块。当反序列化 ControlledInputClass 对象时,readObject 方法会被调用,进而触发静态代码块中的危险操作。

  3. 序列化和反序列化过程:在 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);
    }
}

解释:

  1. MaliciousClass 类:该类的构造函数在对象创建时执行了一个危险操作,即调用 Runtime.getRuntime().exec("calc"),启动计算器。这是在构造函数中隐式执行的恶意代码。

  2. 反序列化时触发构造函数:反序列化过程中,MaliciousClass 对象的构造函数会被调用,进而执行其中的恶意代码。

  3. 序列化和反序列化过程:在 main 方法中,创建了一个 MaliciousClass 对象并进行序列化。反序列化时,构造函数被自动调用,启动计算器。

漏洞分析:

  • 隐式执行危险操作:构造函数中的恶意代码在对象创建时隐式执行,攻击者通过反序列化时触发构造函数,从而执行恶意操作。

  • 反序列化控制:如果攻击者能够控制反序列化的输入数据,就能在反序列化期间触发危险的构造函数或静态代码块,进而执行恶意代码。

原文始发于微信公众号(泷羽Sec-sea):JAVA 反序列化学习笔记

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年3月5日21:10:37
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   JAVA 反序列化学习笔记http://cn-sec.com/archives/3795139.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息