从两个Demo看Java与Python反序列化的差异

从两个 Demo 看 Java 与 Python 反序列化的差异

前言

最近在研究反序列化漏洞时,开始尝试将 Java 与 Python 放在一起进行比较。在 Java 的反序列化研究中,我们经常需要寻找合适的 Gadget Chain,分析依赖库中的类与方法,追踪对象恢复过程中触发的调用链。而在 Python 中,借助 pickle 的对象恢复机制,有时只需要一个可调用对象,就能直观地演示反序列化过程中触发代码执行的现象。

这让我产生了几个问题:两种语言的反序列化机制究竟有什么区别?为什么它们常见的利用思路有所不同?

为什么需要序列化 / 反序列化

我们以 Java 为例,写一个简单的 UserConfig 类:

public class UserConfig {
    private String username;
    private String theme;

    public UserConfig(String username, String theme) {
        this.username = username;
        this.theme = theme;
    }

    @Override
    public String toString() {
        return "UserConfig [username=" + username + ", theme=" + theme + "]";
    }
}

如果我们要创建一个 UserConfig 对象,可以使用:

UserConfig config = new UserConfig("Alice", "dark");

就可以创建一个对象。但如果应用需要在关闭后重新加载配置,或者在多次运行之间保存对象状态,就需要考虑如何把对象保存下来,以及之后如何恢复它。

这时,序列化和反序列化就派上了用场:序列化把对象转换成可以存储或传输的数据,反序列化则根据这些数据恢复对象。

接下来,我们将分别以 Java 和 Python 为例,了解它们的反序列化流程,以及如何造成代码执行的结果。

Java:从对象恢复到方法调用

以上一节的 demo 为例,我们修改 UserConfig 类:

// UserConfig.java
import java.io.Serializable;

public class UserConfig implements Serializable {
    private String username;
    private String theme;

    public UserConfig(String username, String theme) {
        this.username = username;
        this.theme = theme;
    }

    @Override
    public String toString() {
        return "UserConfig [username=" + username + ", theme=" + theme + "]";
    }
}

现在编写一个测试用例:

// Main.java
import java.io.*;

public class Main {
    public static void main(String[] args)
            throws IOException, ClassNotFoundException {

        UserConfig config = new UserConfig("Alice", "dark");

        try (ObjectOutputStream oos = new ObjectOutputStream(
                new FileOutputStream("config.ser"))) {
            oos.writeObject(config);
        }
        try (ObjectInputStream ois = new ObjectInputStream(
                new FileInputStream("config.ser"))) {
            UserConfig serConfig = (UserConfig) ois.readObject();
            System.out.println(serConfig);
        }
    }
}

运行上面的 Demo,可以得到类似下面的输出:

UserConfig [username=Alice, theme=dark]

从结果来看,程序成功地把 UserConfig 对象写入文件 config.ser,又通过 ObjectInputStream.readObject() 将其恢复了出来。恢复后的对象保留了原来的 username 和 theme 字段值。

那么,反序列化的过程是否仅仅是恢复这些字段?

为了回答这个问题,我们需要了解 Java 原生反序列化提供的一种特殊机制:反序列化回调方法。

对于实现了 Serializable 的类,我们可以定义一个 private void readObject(ObjectInputStream in) 方法。当 ObjectInputStream 反序列化该类的对象时,Java 会在对象恢复流程中调用这个方法,使类能够参与自定义的反序列化过程。

这意味着,除了恢复对象状态之外,我们还可以在这个过程中执行额外的逻辑。

我们修改 UserConfig 类,加入一个 readObject 方法:

import java.io.IOException;
import java.io.Serializable;

public class UserConfig implements Serializable {
    private String username;
    private String theme;

    public UserConfig(String username, String theme) {
        this.username = username;
        this.theme = theme;
    }

    @Override
    public String toString() {
        return "UserConfig [username=" + username + ", theme=" + theme + "]";
    }

    private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        Runtime.getRuntime().exec("calc");
    }
}

运行后可以观察到计算器被弹出,这样就实现了在反序列化阶段执行代码。

在实际应用中,危险操作往往并不直接写在目标类的 readObject() 方法里,而是隐藏在应用依赖的其他类中。反序列化时,某些对象之间的调用关系可能把这些操作串联起来,形成所谓的 Gadget Chain(利用链)。

Python:pickle反序列化触发代码执行

在上一节中,我们通过 Java 的 readObject() 方法,观察到反序列化过程中可以执行额外的逻辑。

那么,在 Python 中,是否也存在类似的机制?

答案是肯定的。Python 的 pickle 模块同样允许对象自定义其序列化和恢复方式。其中,__reduce__() 是理解这一机制的重要入口。

我们仍然从一个 UserConfig 类开始:

import pickle

class UserConfig:
    def __init__(self, username, theme):
        self.username = username
        self.theme = theme

config = UserConfig("Alice", "dark")

data = pickle.dumps(config)
restored = pickle.loads(data)

print(restored.username)
print(restored.theme)

这里的 pickle.dumps() 将对象序列化为字节数据,pickle.loads() 则从字节数据中恢复对象。

这个过程看起来与 Java 的原生序列化十分相似:我们保存对象的状态,再通过反序列化恢复它。

不过,pickle 不仅需要保存对象的字段,还需要能够描述如何重新创建对象,以及如何恢复对象状态。

现在,我们该考虑如何弹出计算器了。

Python 允许对象通过 __reduce__() 定义自己的恢复方式。简单来说,这个方法可以返回一个元组,告诉 pickle:

  1. 使用哪个可调用对象来重建实例;
  2. 调用时需要传入哪些参数;
  3. 在需要时,如何恢复对象的其他状态。

例如:

def __reduce__(self):
    return (some_function, (arg1, arg2))

其含义为:当 pickle 之后需要重建这个对象时,可以调用 some_function(arg1, arg2),并使用其返回结果作为重建结果。

现在我们加入一个 __reduce__ 函数:

class UserConfig:
    def __init__(self, username, theme):
        self.username = username
        self.theme = theme
    def __reduce__(self):
        return (__import__('os').system,("calc",))

config = UserConfig("Alice","dark")

data = pickle.dumps(config)
restored = pickle.loads(data)

运行后可以观察到计算器被弹出。

注意

在这个 Demo 中,pickle.loads() 返回的并不是原来的 UserConfig 实例,而是 os.system() 的返回值。这是因为 __reduce__() 指定的可调用对象决定了重建过程的返回结果。

在实际应用中,两者存在的漏洞

前面的 Demo 都是在我们自己编写的类中主动加入特定行为,因此可以很直观地观察到反序列化与代码执行之间的联系。

但真实应用中的反序列化漏洞往往更加隐蔽:应用开发者可能只是为了保存对象状态,或者读取缓存、模型文件和其他持久化数据,却没有意识到反序列化过程还可能执行其他操作。

下面分别以 Java 和 Python 生态中的公开漏洞为例,分析这种风险在实际应用中是如何出现的。

Java:Commons Collections 反序列化漏洞

Apache Commons Collections 为 Java 集合操作提供了额外的工具和功能。其中一些类能够将方法调用封装为对象,或者在处理集合元素时动态调用其他方法。

在特定条件下,攻击者可以组合这些已有类,构造出一条特殊的对象调用链。当应用反序列化攻击者提供的数据时,这条调用链便可能触发危险操作,最终导致任意代码执行。我们以最经典的 CC1 链为例,其触发流程大致如下:

java.io.ObjectInputStream.readObject()
 └ sun.reflect.annotation.AnnotationInvocationHandler.readObject()
     └ java.lang.reflect.Proxy.entrySet()
         └ sun.reflect.annotation.AnnotationInvocationHandler.invoke()
             └ org.apache.commons.collections.map.LazyMap.get()
                 └ ChainedTransformer.transform()
                     ├─ ConstantTransformer.transform()   → Runtime.class
                     ├─ InvokerTransformer.transform()    → Class.getMethod("getRuntime")
                     ├─ InvokerTransformer.transform()    → Method.invoke(null) → Runtime 实例
                     └ InvokerTransformer.transform()    → Runtime.exec("calc")

从触发流程来看,可以进行如下简化:

java.io.ObjectInputStream.readObject()
 └ 反序列化触发相关对象的方法调用(入口)
    └ 多个类组成的调用链
       └ 任意代码执行

感兴趣的读者可以自行搜索相关资料进行复现。在这里我们只是为了指出:对于 Java 反序列化漏洞而言,研究者需要分析目标环境中的依赖库,寻找能够在对象恢复过程中被触发的类和方法,并将这些行为串联起来(也就是组成我们所说的利用链),从而产生开发者没有预料到的安全影响。

Python:PyTorch 模型加载中的反序列化风险(CVE-2025-32434)

PyTorch 是一个广泛使用的 Python 深度学习框架,模型文件通常需要保存和加载模型参数,以便后续进行推理、训练或迁移。在这一过程中,开发者可能会使用 torch.save() 保存数据,并使用 torch.load() 将其重新加载。

由于 PyTorch 的部分模型保存格式涉及 Python 的序列化机制,模型加载过程也需要考虑反序列化带来的安全风险。为了降低加载不可信模型文件时的风险,PyTorch 提供了 weights_only=True 模式,限制反序列化过程中允许处理的对象类型与操作。

然而,安全限制的存在并不意味着实现本身绝对安全。2025 年公开的 CVE-2025-32434 就是一个典型案例:在 PyTorch 2.5.1 及之前的受影响版本中,即使使用 torch.load(..., weights_only=True) 加载模型,攻击者仍可能通过构造恶意模型文件实现远程代码执行。

在 Python 这边,漏洞的成因则是安全限制可以被绕过。

总结

通过前面的两个 Demo,我们已经分别观察到了 Java 和 Python 在反序列化过程中触发代码执行的现象。虽然两者最终都可能导致代码执行,但它们的机制与常见利用思路并不完全相同。

假设一个 Java Web 应用接收外部传入的序列化数据,并使用 ObjectInputStream.readObject() 进行反序列化。攻击者通常无法直接修改应用中已经编译好的类,也不能随意向服务器添加自己编写的 Java 类。因此,攻击者需要考虑的是:应用现有的类中,是否存在一些能够通过反序列化触发的方法调用,并最终执行危险操作?这就是 Gadget Chain 的基本思路。

相比之下,Python 的 pickle 提供了更为直接的对象重建机制。以 __reduce__() 为例,开发者可以通过该方法指定对象在反序列化时使用的可调用对象及其参数。当 pickle.loads() 恢复对象时,便可能调用这些可调用对象,从而触发额外的程序逻辑。

这意味着,在某些场景下,如果攻击者能够控制传入的 pickle 数据,就可能直接利用对象重建过程中对可调用对象的调用来触发危险操作,而不需要像典型的 Java 反序列化利用那样,专门寻找多个现有类之间的调用关系。例如,前面 Demo 中的 os.system() 就展示了这种情况:通过 __reduce__() 指定重建规则,便可以在反序列化阶段触发命令执行。

不过,两者的区别不能简单概括为"Java 需要利用链,而 Python 不需要"。Java 也可能通过一个自定义回调直接触发危险操作;Python 同样可能需要多个对象和方法相互配合,才能到达目标操作。它们只是提供了不同的对象恢复机制,因此在研究真实漏洞时,常见的分析路径有所不同。

从实际漏洞研究的角度来看,理解 readObject() 和 __reduce__() 只是第一步。更重要的是进一步分析不可信数据如何进入反序列化流程、对象恢复过程中触发了哪些方法,以及这些方法在目标运行环境中是否能够到达危险操作。只有结合输入可控性、调用路径和实际影响,才能判断一个具体场景是否构成可利用的安全漏洞。

posted @ 2026-10-17 14:14  R4v3n  阅读(6)  评论(0)    收藏  举报