python反序列化
Last updated on September 27, 2024 am
基础
基于php反序列化的思路学习python反序列化。
Python 中最常用的序列化模块是 pickle
模块。
反序列化则使用pickle 模块的 load()
函数反序列化存储在文件中的 Python 对象。
Python序列化和反序列化例子
1 |
|
1 |
|
1 |
|
1 |
|
上传.pkl
文件进行dump
BuckeyeCTF 2023 Text Adventure API代码片断
1 |
|
这个地方要能够使pickle.load()解析正确做法: 要写一个类,不然会报错
1 |
|
PVM
我们在使用pickler的时候,我们要序列化的内容,必须经过PVM,Pickle Virtual Machine (PVM)是Python语言中的一个虚拟机,用于序列化和反序列化Python对象。它是Python标准库中的一部分,由Python的pickle模块提供支持。下面是Pickle Virtual Machine的运行原理:
- 生成操作码序列:pickle模块在序列化Python对象时,会生成一系列操作码(opcode)来表示对象的类型和值。这些操作码将被保存到文件或网络流中,以便在反序列化时使用。
- 反序列化操作码:在反序列化时,pickle模块读取操作码序列,并将其解释为Python对象。它通过Pickle Virtual Machine来执行操作码序列。Virtual Machine会按顺序读取操作码,并根据操作码的类型执行相应的操作。
- 执行操作码:Pickle Virtual Machine支持多种操作码,包括压入常量、调用函数、设置属性等。执行操作码的过程中,Virtual Machine会维护一个栈来存储数据。当执行操作码时,它会将数据从栈中取出,并根据操作码的类型进行相应的操作。执行完成后,结果将被压入栈中。
- 构造Python对象:当操作码序列被完全执行后,Pickle Virtual Machine会将栈顶的数据作为结果返回。这个结果就是反序列化后的Python对象。
组成
指令处理器、栈区和内存区。
- 指令处理器:从流中读取 opcode 和参数,并对其进行解释处理。重复这个动作,直到遇到
.
这个结束符.
后停止。最终留在栈顶的值将被作为反序列化对象返回。 - 栈区:用 list 实现的,被用来临时存储数据、参数以及对象。
- 内存区:用 dict 实现的,为 PVM 的整个生命周期提供存储。
注意:
(1)操作码是单字节的
(2)带参数的指令用换行符定界
协议
PVN总共有6种协议:v0-v5
从序列化的字节可以判断出是哪种协议,v2-v5的字节都是\x80\x0版本号
的形式
而v0-v1则都是以ccopy_reg\n_reconstructor\n
开头
opcode(操作码)
指令 | 描述 | 具体写法 | 栈上的变化 |
---|---|---|---|
c | 获取一个全局对象或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标记以及被组合的数据出栈,列表被更新 |
pickletools
pickle规定了pickletools便于人工解读opcode
1 |
|
魔术方法
__reduce__()
对应opcode:R
通过重写__reduce__()
使得序列化时按照重写的方式进行。
__reduce__()
定义的函数需要返回一个字符串或者元组,但返回元组(callable, ([para1,para2...])[,...])
时,进行反序列化会调用callable(para,...)
程序有import os
时
1 |
|
程序没有import os
时
1 |
|
__repr__()
通过重写该函数,打印实例化对象时会输出__repr__
返回的内容,前提是没有写__str__
,否则会调用成__str__
1 |
|
__eq__()
当进行两个类比较时会自动调用
1 |
|
__setstate__()
反序列化时调用__setstate__
__getstate__()
被序列化时调用__getstate__
必须返回一个字典
pickle反序列化漏洞利用思路
全局变量覆盖
1 |
|
全局变量引入
1 |
|
1 |
|
1 |
|
命令执行
通过在类中重写__reduce__
方法,从而在反序列化时执行任意命令,但是通过这种方法一次只能执行一个命令,如果想一次执行多个命令,就只能通过手写opcode的方式。可以依靠四个指令来完成:R
,i
,o
,b
R
选择栈上的第一个对象作为函数、第二个对象作为参数,然后调用该函数
1 |
|
i
相当于c和o的组合,先获取一个全局函数,然后寻找栈中的上一个MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(或实例化一个对象)
1 |
|
o
寻找栈中的上一个MARK,以之间的第一个数据(必须为函数)为callable,第二个到第n个数据为参数,执行该函数(或实例化一个对象)
1 |
|
b +__setstate__()
1 |
|
过滤绕过
过滤R
利用其他指令绕过
过滤变量名
双exec绕过
1 |
|
绕过Unpickler.find_class()
只用c
对builtin进行操作来构造payload
1 |
|
1 |
|
绕过R,使用o
1 |
|
编码绕过
S'flag'
可以替换为S'\x66\x6c\x61\x67'
或者V'\u0066\u006C\u0061\u0067'
参考资料
https://tttang.com/archive/1885/
本文作者: fru1ts
本文链接: https://fru1ts.github.io/2023/08/28/python%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/
版权声明: 本站均采用BY-SA协议,除特别声明外,转载请注明出处!