事情的起因是公司业务线上采用了开源BI框架Redash进行二次开发做了一个监控系统,Redash是使用的Python的知名web框架Flask,且在github上有16.9k星。在走安全测试流程时,由于之前对paython的代码审计并不了解,所以我在安全测试的时候主要注重接口问题登录、找回密码等业务逻辑漏洞。
在看代码时,发现找回密码处存在问题
找回密码流程:
- 用户输入账号后点击找回密码(/forgot)
- 后台向账户邮箱发送重置密码邮件
- 用户访问邮箱中的邮件直接重置密码(/reset/<token>)
先看下第一步中的代码
再查看用户是否存在且不处于禁用状态,然后调用send_password_reset_email(user)发送重置密码的邮件,跟进
64行中reset_link_for_user(user),顾名思义是根据user对象生成重置密码的连接,继续跟进
很明显是重置密码的连接是根据token和base_url(user.org)拼凑来的,我们走一遍业务流程就可以发现base_url(user.org)实际就是项目地址,我们只需要关注token是如何生成的,跟进invite_token(user)
嗯?这么简单?就这?
发现是通过URLSafeTimedSerializer和userid来生成的token,userid很简单我们一般就用1就好了,userid为1一般就是管理员了,不行就23456789。
那么就看下URLSafeTimedSerializer的安全性了,最开始我看到方法名中有serializer是序列化的意思,心想对象序列化后的字符串不都是固定的吗,然后我连续拿了两次同一用户重置密码的token发现是不一样的,回过头发现方法名中有time的字样,心想是根据当前时间戳生成的随机序列化字符串,这样就没法猜解了(不考虑URLSafeTimedSerializer自身安全性问题),于是我想到那么程序又是如何校验token的正确性的呢,可以在上图中看到生成的token并没有存在数据库中
再去看看Redash倒是是如何校验token的
访问/reset/<token>后调用了render_token_login_page(),跟进后第一行就可以看到调用
user_id = validate_token(token)直接将token还原成了user_id
跟进validate_token方法
可以看到原来这里使用dump是序列化成token,loads反序列化成数据(userid)
那么我们知道这里token其实就是序列化一个userid的字符串,我们能本地序列化一个token后用在目标系统进行重置密码吗
答案显示是不行的,毕竟github上一万多颗星不可能这点安全意识都没有,我们可以看到12行处生成URLSafeTimedSerializer对象是根据一个SECRET_KEY来生成的
于是网上百度了下URLSafeTimedSerializer,大概发现了没有正确的SECRET_KEY是无法正确序列化token和反序列化成userid的
那么这里就没有问题了吗,记住这是一个开源框架啊,SECRET_KEY是写死的,看了下我们项目中的SECRET_KEY
再去看看github中的SECRET_KEY
发现是一样额,这。。。
复现一下,拿着这个secret_key本地生成token
if __name__ == "__main__":
SECRET_KEY = os.environ.get("REDASH_COOKIE_SECRET", "c292a0a3aa32397cdb050e233733900f")
serializer = URLSafeTimedSerializer(SECRET_KEY)
key = serializer.dumps("1")
print(key)
在fofa上搜了下redash
真不少,找了几个测试了一下,通过我们自己生产的token进行拼凑的url,访问后能正常进去重置密码的如下图,这就是有问题
发现效果不是很理想,虽然又不少成功的,但是大部分都不行,
报错“Invalid invite link. Please ask for a new one” 说明咋们的userid不对,这么没关系,我们可以直接枚举userid就行
报错“Your invite link has expired. Please ask for a new one”的说明人家系统把key修改了
我当时还挺好奇的,系统既然没提示修改key为啥有这么多人去修改secret_key呢,安全意识这么强
后来考虑下一个问题的时候发现原因了,原来这个Redash的这个secret_key应该也是Flask框架session使用到的secret_key
在我百度搜索URLSafeTimedSerializer这个方法时,网上出现最多的就是flask session的实现方式,所以我也去了解了下flask的session的存储方式
原来Flask的session默认时客户端session(也可以选择使用redis等数据库的存储方式),这样好像颠覆了我们之前理解的session时服务器端的cookie,简单解释下flask客户端session只所以能行的原因是放在cookie的session,客户端虽然能解码,但是无法篡改,因为有签名,客户端session的具体实现和可能导致的安全问题可以看P牛的文章
https://www.leavesongs.com/PENETRATION/client-session-security.html
我们自己写项目的时候如果是使用Flask的这种客户端session,都是必须手动设置secret_key的,除非开发者使用123456这样的弱key,一般是没有问题的,但是在开源软件中这个secret_key强度再高不也是硬编码在配置文件中的吗,这样还有意义吗
我在github上找了一些flask的流行开源系统
发现确实有一些通用系统是将secret_key写死的,但是也有安装系统时根据配置文件模版自动生成配置文件,此时的secret_key都是随机生成的,这种情况是安全的
思考:
- 对于flask框架的客户端session其实可以考虑下爆破secret_key,不排除确实存在有些开发安全意识不好或不知道flask中secret_key的作用而选择使用低强度的secret_key
- 使用了flask的开源软件,可以看下是否是硬编码了secret_key
- Flask中还原session,查看敏感信息,例如短信图片验证码、验证使用的token
- 审计开软软件的时候需要开始注意硬编码密钥的问题了,通过秘钥逆向去挖掘漏洞,关键在于了解密钥的用途
- CBC字节翻转攻击?
这次的key比较特殊,由于是flask的session使用到的key,熟悉flask的开发都知道这个key需要去改,所以影响范围不大