概述
Python
中序列化一般有两种方式: pickle/cPickle
模块和json
模块, 前者是Python
特有的格式, 后者是json
通用的格式。
Pickle
有如下四种常用方法
函数 | 说明 |
---|---|
pickle.dump(obj, file) |
对象反序列化到文件对象并存入文件 |
pickle.dumps(obj) |
将对象序列化成字符串格式的字节流 |
pickle.load(file) |
读取文件, 将文件中的序列化内容反序列化为对象 |
pickle.loads(bytes_obj) |
将字符串格式的字节流反序列化为对象 |
1 | 注意:file文件需要以 2 进制方式打开,如 `wb`、`rb` |
除去文件,一种简单的工作方式涉及到:
- 序列化(
pickle.dumps
):将 Python 对象转换为字节流。 - 反序列化(
pickle.loads
):将字节流转换回 Python 对象。
给出一个示例漏洞代码
1 | import pickle |
在这个示例中,PickleRCE
类通过__reduce__
方法定义了在反序列化时应该执行的操作,即执行os.system('whoami')
,这是一个常用来查看当前用户的系统命令。
触发原理是:在pickle
的反序列化过程中,如果反序列化的对象包含特定的方法,如__reduce__
或__reduce_ex__
,这些方法在反序列化时会被自动调用(类似于 PHP 中的 wakeup()
),从而可能执行其中定义的任意代码。这正是pickle
反序列化漏洞的核心所在。
与 PHP 反序列化漏洞不同的是(PHP 反序列化漏洞要求源代码中必须存在有问题的类,且对象参数可控可生成恶意对象),而 Python 反序列化漏洞,只需要被反序列化的字符可控即可造成 RCE(可自己创建危险方法来命令执行)
OPcode(Operation Code)
通过 dumps 方式序列化之后,对应的字符串是 Pickle 的OPcode,我们可以再 pickle.py(位于 python 的 lib 中) 中看到详解(python312为例):
可以看到有多个 protocol 版本,整体是向下兼容的。可以在序列化时指定,如:dumps(aaa, 0)
我们利用一个例子来学习常见的 OPcode
1 | import pickle |
结果
1 | D:\\code\\py\\hack\\venv\\Scripts\\python.exe D:\\code\\py\\hack\\pickle01.py |
特性
反序列化执行 reduce 魔术方法,在 return 时,回自动导入源代码中没有引入的模块,例如:
1 | import pickle |
这极大地拓宽了我们的攻击面。其中 s 的构造可以自己写一个类去构造。规则就是 PVM 的操作码,py2 和 py3 不同,主要看环境。
Blacklist 绕过
我们查看官方推荐的沙箱机制
1 | import builtins |
用白名单的形式且限制了反序列化的对象必须是builtins
模块中的对象。
p 牛在code-breaking 2018 picklecode 中改写这个find_class
,改成了黑名单方式:
1 | import pickle |
这里黑名单的形式就意味着从 builtins
入手可能找到绕过黑名单的方式。
关于
builtins
,GPT4 解释:在 Python 中,builtins
模块是一个非常特殊的模块,因为它提供了所有 Python 内置函数的访问,这些内置函数是 Python 解释器的核心组成部分,无需任何导入就可以在任何地方使用。这些内置功能包括基础类型(如int
,float
,dict
)、内置函数(如print()
,len()
)、异常类型(如ValueError
,KeyError
)等。
同时,GPT 还说:
- 不要轻易覆盖:由于
builtins
模块中的元素是全局可用的,覆盖这些内置的名称可能会导致意想不到的行为。例如,重新定义list
或open
可能会在代码的其他部分引发错误。- 安全性:在执行动态操作时,如使用
getattr
访问builtins
,应当小心,避免执行不安全或未经验证的输入,这可能导致安全漏洞。
我们先看看这个使用 getattr
访问 builtins
是怎么个事
使用
getattr
函数来访问 Python 的builtins
模块可以提供一种动态调用内置函数或访问内置对象的方法。基本用法是getattr(object, name[, default]
,其中:
object
是你想从中获取属性的对象。name
是一个字符串,表示你想获取的属性名。default
是可选的,如果指定的属性不存在时返回的值。在
builtins
模块的上下文中,object
将是builtins
模块本身。
我们可以通过builtins.getattr('builtins', 'eval')
来获取eval函数,然后再执行,此时使用的模块是 builtins
而方法是 getattr
,绕过黑名单校验。
现在我们需要构造出这个 pickle 的 opcode 然后执行序列化操作从而执行我们构造的 eval
构造 opcode
目前的思路是我们要构造 pickle code,来执行:
1 | func = getattr(__import__('builtins'), 'eval') |
但是我们没办法直接通过 reduce 来执行这条:
因为 reduce 必须返回一个包含两部分的元组:一个可调用对象,用于重新创建序列化对象;一个元组,该元组包含传递给第一个元素(可调用对象)的参数。也就是说只能执行一个函数,而我们需要执行两个函数。所以要想办法手搓 opcode。
pickle的内容存储在如下两个位置中:
- stack 栈
- memo 一个列表,可以存储信息
对于之前那个简单的例子生成的opcode(b'cnt\\nsystem\\np0\\n(Vwhoami\\np1\\ntp2\\nRp3\\n.
)
做出解释:
1 | 0: c GLOBAL 'nt system' # 向栈顶压入`nt.system`这个可执行对象 |
接下来想办法构造出 opcode
- 可以直接使用marshell工具直接梭哈,但是它的原理是使用了
types.FunctionType
同样不是白名单模块,这里还是先列一下 POC,生成恶意 opcode
1 | import base64 |
- 也可使用开源项目 https://github.com/eddieivan01/pker,readme中有该情景的构造方式
- 尝试手搓
1 | cbuiltins #设置builtins为可执行对象 |