内容简介:几星期之前,我和我的一个小伙伴正在讨论web框架,小伙伴声称他构造了一个“绝对无法绕过的”登录表单。征得小伙伴同意后,他把相关代码发给我审阅。正如我所料,他说的有一定道理,代码看上去的确非常安全,不存在
一、前言
几星期之前,我和我的一个小伙伴正在讨论web框架,小伙伴声称他构造了一个“绝对无法绕过的”登录表单。征得小伙伴同意后,他把相关代码发给我审阅。
正如我所料,他说的有一定道理,代码看上去的确非常安全,不存在 SQL注入 、 XSS 漏洞,他甚至限制了访问速率并会审计日志。看来如果不知道密码,我们似乎无法绕过登录表单。
这种实现非常完美,直到我注意到他使用的密钥(secret key)为“CHANGEME”。
二、利用人为错误
我的小伙伴没有意识到一点:使用脆弱的密钥会比在客户端存储当前用户的状态其实更加危险。考虑如下一个示例程序(该程序完全摘抄自小伙伴提供的代码):
# Requirements: Flask from flask import Flask, session app = Flask(__name__) app.config['SECRET_KEY'] = 'CHANGEME' @app.route('/') def index(): if 'logged_in' not in session: session['logged_in'] = False if session['logged_in']: return '<h1>You are logged in!</h1>' else: return '<h1>Access Denied</h1>', 403 if __name__ == '__main__': app.run()
乍看之下,代码似乎有点难以理解,我甚至都想直接删除登录表单(因为我们永远无法猜到正确的密码)。那么我们如何才能看到“You are logged in!”消息呢?
三、Flask的会话管理
默认情况下,Flask会使用名为“signed cookies”的一种机制,这是在客户端(而非服务端)存储当前会话(session)数据的一种简单方式,使其(从理论上)无法被篡改。
然而这种方法存在一些缺点,比如cookie值并非经过加密处理,而是经过签名处理。这意味着我们不需要密钥也能读取会话内容。
此外,我还与许多 Python 开发人员交流过,大部分人都认为客户端无法读取会话数据,因为用来签名cookie的代码为 SecureCookieSessionInterface
,这个函数名也给他们带来不可靠的安全感。我们来考虑如下会话:
图1. 一个“安全的”cookie可以切分成若干部分
会话数据
会话数据实际上就是会话的内容,虽然第一眼看上去这种数据很难理解,但其实这只是经过 Base64 编码的一个字符串。如果我们使用 itsdangerous
的base64解码器对其进行解码,可以得到如下输出:
图2. 服务端设置的会话数据
时间戳可以告诉服务端数据最后一次更新的时间。根据我们所使用的 itsdangerous
的具体版本,该时间戳可能对应当前的Unix时间戳、或者当前当前Unix时间戳减去epoch时间(即1970-01-01 00:00:00 UTC)所对应的值(由于之前存在一个bug,我们无法设置早于2011年的时间,因此存在两种不同的时间戳版本,参考此处 资料 )。
如果时间戳已超过31天,那么会话就会被打上过期标记,变成无效会话。
加密哈希
这就是让cookie变得“安全”的字段。服务器向我们发送最新的会话数据之前,会结合我们的会话数据、当前时间戳以及服务器的私钥来计算 sha1 哈希 。
每当服务器再次看到该会话,就会解析这几部分字段,然后使用同样的方法验证这些数据。如果哈希与给定的数据不匹配,那么表明数据已被篡改,因此服务端会将该会话当成无效会话。
这意味着如果我们的私钥非常容易猜测,或者已对外公开,那么攻击者可以不费吹灰之力修改会话的内容。我们可以来看一下网上已公开的私钥情况,在GitHUb上搜索 secret_key
,可以看到如下结果:
图3. 在GitHub上搜索 secret_key
可以返回将近240万条记录
四、绕过认证
那么我们怎么才能绕过认证呢?我们来看如下示例(请复制前文代码,以便按步骤执行):
前提条件
- 安装Python解释器(我使用的是Python 3.6版)
- 使用
pip install flask
安装Flask(可以在 虚拟环境 中安装)
在进一步操作前,我们需要启动Flask,命令如下:
$ python server.py
获取会话cookie
为了获取会话cookie,我们需要先从服务器探测出可能使用的cookie格式。这里我使用 curl
发起请求,并带上 -v
选项以获得 verbose
输出信息(可以打印出请求头部),大家也可以直接访问服务器web页面,再配合浏览器插件(如 EditThisCookie )来获取cookie的内容:
图4. 服务端返回会话cookie
需要注意的是,并非所有服务器都会立刻返回一个会话,有些服务器只会在出现错误时返回会话,而其他服务器只会在用户登录后返回会话。我们需要根据具体情况具体分析。为了演示方便,这里我们的示例程序无论何时都会设置会话信息。
创建字典
虽然我们可以组合各种可能的字母、数字以及符号来暴力破解,但更好的办法是创建一个字典集(wordlist),其中包含开发者已经公开过的秘钥。
为建立字典,我首先想到的是两个来源:GitHub(如前文所示)以及StackOverflow(我们可以通过 archive.org 来下载该网站上曾经发表过的每条评论、文章等数据)。
对于GitHub,我简单编写了一个一次性脚本,该脚本会搜索 secret_key
,然后尽可能多地下载commit以及文件。对于StackOverflow,我尝试遍历每个可能的文本,然后使用如下正则表达式来匹配秘钥值:
图5. 用来识别秘钥的正则表达式
我花了一周时间在我的VPS上爬取GitHub数据,结合我从StackOverflow上提取的每篇文章、评论后,我最终得到了37069条不同的秘钥值。
破解签名
现在我们手头上已经有一份字典,也知道cookie及部分Flask会话管理代码,接下来我们可以使用每个秘钥值来匹配cookie,判断签名是否有效。如果没有出现任何错误,则表明我们获得了有效的签名,这意味着我们已经找到了服务器的私钥!
图6. 会话破解示例应用
制作会话cookie
如果脚本运行成功,那么现在我们已经获得了服务器的私钥。现在我们可以使用同样的代码,但此时我们并不“加载”已有的会话,而是“转储”会话,使用任意数据来创建cookie。
图7. 构造新的会话
如果我们现在向同一个服务器发起请求,服务器应该会接受我们构造的cookie,因为这个cookie与服务器的秘钥匹配,这样服务器会认为我们已经成功登录。
图8. 使用我们构造的cookie来绕过服务端认证
这里指出的是,虽然我们可以修改会话,但并不意味着我们可以立刻绕过身份认证机制。并非所有的系统都使用同样的方法构建,我们可能需要进一步研究,确定具体情况,以及是否能在实际环境中使用这种技术。
五、Flask-Unsign
由于在分析Flask对会话的处理过程中我做了不少操作,因此我决定将代码集成到一款方便使用的命令行工具,使大家能够扫描自己的服务器是否存在类似问题。
大家可以在命令行中使用 pip 来安装该工具,命令如下:
$ pip install flask-unsign[wordlist]
如果大家不需要庞大的字典文件,只想使用代码,也可以忽略其中的 [wordlist]
命令。
$ pip install flask-unsign
大家也可以访问 GitHub 阅读源代码。
使用方法
Flask-Unsign主要有3种使用场景:对cookie的解码、签名以及解除签名(破解),代码支持HTTP协议,因此用户无需打开浏览器:
图9. 使用Flask-Unsign破解并构造会话
六、情况统计
现在可能有人会有问题:实际环境中这种攻击方法是否有效?为了测试这一点,我和Rik van Duijn( Twitter )利用 Shodan 简单统计了一下有多少设备可能在使用Flask框架,结果如下:
图10. 大约有88,000台服务器在使用Flask
然后我们将目标限定在立刻返回会话cookie的那些服务器(因为想查看完整的Shodan数据较为昂贵,并且爬取每台主机然后定位会话cookie也是动作较大的一种行为,可能会让我的路由器不堪重负,我的ISP可能也会介入,询问我的具体动机)。
图11. 结果缩小到1,500台服务器
下载搜索结果后,我开始分析我能利用之前的字典成功破解多少个会话。
服务器正在运行 Werkzeug
并不意味着它们使用的是Flask框架。此外有些应用的信息会被其他web服务器覆盖(如Nginx)、有些服务器不会立刻设置cookie、有些服务器前端存在防火墙,这些我们都没有考虑,因此下面给出的统计数据并不全面。
图12. 所有有效会话中有超过28%会被成功破解
最初我在MacBook上暴力破解会话,但很快我就意识到MacBook对多线程Python的处理并不高效,因此我切换到另一台(性能稍好的)游戏主机来运行脚本,整个任务大概花了20分钟。
剔除非签名的cookie后(大部分为服务端cookie,或者与Flask使用相同的命名约定及基本代码的其他框架),我得到了1242个有效的会话。将这些会话输入 Flask-Unsign 进行处理后,有352个会话被成功破解,破解率已经超过了28%。在这352个会话中,我只用到了78个不同的秘钥值。
图13. 10大常用的秘钥
七、缓解措施
有各种方法可以避免这种问题。首先也是最明显的办法就是保证我们所使用的秘钥的安全。此外,我们还可以考虑如下措施:
使用随机化秘钥
不使用容易被猜出的秘钥,而使用完全随机的值。理想情况下,每次启动应用时我们都应该将秘钥设置为随机值,但这样对用户体验可能不是很好,因此每次重启服务时用户会话都会过期。
最实用的解决方案就是生成一个UUID值。我们可以在类Unix系统上使用 uuid
或者 uuidgen
命令来完成该任务,或者在安装了Python环境的主机上执行如下命令:
$ python -c 'import uuid; print(uuid.uuid4());'
使用服务端会话
除了避免攻击者猜出秘钥之外,我们也可以使用服务端会话来避免攻击者查看会话的内容,此时攻击者只能得到一个唯一的token值。
对于Flask,我们可以安装 Flask-Session 包,然后在构建应用时进行初始化。
示例应用
Flask示例应用如下所示,其中使用了前文提到了缓解措施,可以加强会话的鲁棒性。
# Requirements: Flask, Flask-Session import os from flask import Flask, session from flask_session import Session app = Flask(__name__) app.config['SECRET_KEY'] = os.urandom(64) app.config['SESSION_TYPE'] = 'filesystem' Session(app) @app.route('/') def index(): if 'logged_in' not in session: session['logged_in'] = False if session['logged_in']: return '<h1>You are logged in!</h1>' else: return '<h1>Access Denied</h1>', 403 if __name__ == '__main__': app.run()
以上所述就是小编给大家介绍的《浅谈Flask cookie与密钥的安全性》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 密钥繁多难记难管理?认识高效密钥管理体系
- HTTPS之密钥知识与密钥工具Keytool和Keystore-Explorer
- 秘密的实质——密钥
- 密钥安全技术
- 密钥管理架构设计概述
- 代码审计 | SiteServerCMS密钥攻击
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Powerful
Patty McCord / Missionday / 2018-1-25
Named by The Washington Post as one of the 11 Leadership Books to Read in 2018 When it comes to recruiting, motivating, and creating great teams, Patty McCord says most companies have it all wrong. Mc......一起来看看 《Powerful》 这本书的介绍吧!