Flask 忘关 debug,一个 9 位数拿下服务器
📢 本文摘自「凋零骷髅Sec」公众号
Q: 我就点开一个内网网站,怎么就能把服务器拿下来?
A: 因为这是 Flask 写的,而且它把 debug 模式开在生产环境了。
Q: 啥是 debug?Flask 又是啥?
A: 慢慢来,这俩词讲明白,你就知道这锅该谁背了。
目标就是下面这个页面,一个很普通的”文件服务”:

名词解释
Flask 是框架,Werkzeug 是啥?
这么记:Werkzeug 是发动机和底盘,Flask 是套在上面的车壳。
Werkzeug 是 Python Web 底层的一个工具库,管 HTTP 请求解析、响应拼接、开发服务器、报错页面这些粗活。Flask 离不开它,装 Flask 的时候 Werkzeug 就自动跟来了。
一句话:Flask 是用 Werkzeug 搭起来的。所以待会儿要打的”调试器”,它住在 Werkzeug 里,不在 Flask 里。
debug 模式是啥?
开发者写代码时开了 debug=True,能享受两个”福利”:
- 报错时把完整的 Python 堆栈摆给你看,方便找 bug;
- 附带一个交互式调试控制台,能在报错的地方现场跑 Python 代码。
第 2 个就是问题。能跑 Python,等于能在服务器上执行任意命令。Werkzeug 给控制台上了把锁——PIN 码。输对了 PIN,控制台解锁,代码随便敲。
那 PIN 是不是随机生成、猜不出来?
想多了。这个 PIN 是”算”出来的,由 6 个固定配方算出个固定数字。谁凑齐这 6 样东西,谁就能算出来。就跟别人把保险箱密码设成”生日+车牌号”似的,你只要知道生日和车牌号,锁就是摆设。
SSTI 又是啥?
模板注入。说白了:网站把你输入的内容当成了网页模板的一部分来渲染。你在输入里塞点 {{ }} 这种特殊语法,服务器就当真去执行了。后面我们就在这站上试。
行,名词过完了,动手。
第一步:找个能下手的口子(两条路,会一条就够)
这站有两个口子能进。
路线 A:模板注入(SSTI)
搜索框里填 {{7*7}},服务器把它当模板渲染,吐了个 49:

能算乘法,说明你的输入进了模板引擎。这只是验伤,后面用这个读文件。
路线 B:文件读取(路径穿越)
这站有个 /file 功能,看服务目录下的文件。你把文件名写成 ../../../../etc/passwd,往上翻四层目录再进 etc,系统文件就出来了:

/etc/passwd 是 Linux 的用户表。能读它,说明任意文件读取成立。
新手建议先啃路线 B:只用”读文件”一个概念,没那些花哨语法。下面文章主线就走这条。
第二步:凑齐 PIN 的 6 个配方材料
PIN 由 6 个材料算出来,分两组。
打个 Minecraft 的比方:招出”凋零”要 3 个头颅加 4 个灵魂沙,缺一样都白搭。这里也一样——4 个 public_bits 加 2 个 private_bits,凑齐 6 样,才能把 PIN 这个”服务器钥匙”给合成出来。 少一个,密码就打不开。
public_bits,4 个,比较好搞:
| 材料 | 取值 | 上哪拿 |
|---|---|---|
| 运行用户名 | root | 读 /proc/self/environ 或 /etc/passwd |
| 模块名 | flask.app | 固定值 |
| 类名 | Flask | 固定值 |
| flask/app.py 路径 | /usr/local/lib/python3.10/site-packages/flask/app.py | debug 报错页白送 |
private_bits,2 个,藏得深点:
| 材料 | 取值 | 上哪拿 |
|---|---|---|
| 网卡 MAC(换整数) | f6:ea:03:be:89:1f → 271484945598751 | 读 /sys/class/net/eth0/address |
| machine_id | a90e1349-41f4-4315-8330-a620ca841350 | 读 /proc/sys/kernel/random/boot_id |
先看第 4 个材料,flask 路径。故意访问个不存在的文件,触发报错。因为开着 debug,服务器把完整堆栈原样甩了出来:

看见堆栈里那句 File "/usr/local/lib/python3.10/site-packages/flask/app.py" 没?——flask 的安装路径,人家自己送上门了。 debug 模式第一宗罪:啥都往外说。
接着把 private_bits 两个材料掏出来。两个入口都试一下,截图都对得上。
入口一:路径穿越(/file 路由)
1 | |

1 | |

入口二:模板注入(/search 路由,SSTI)


这里有个坑得提醒:Werkzeug 2.2.x 算 machine_id 时,会在 boot_id 后头再拼一段 cgroup 尾巴(读 /proc/self/cgroup 第一行最后一个 / 后面的字符)。咱这容器里 cgroup 是
0::/,尾巴是空的,所以 machine_id 恰好等于 boot_id。很多老教程漏了这步,拿到新环境就算不对。
第三步:把 6 个材料算成 PIN
Werkzeug 这算法不复杂,核心就两轮哈希。下面这个计算器可以直接跑:
1 | |
跑出来:**163-136-028**。跟服务器启动日志里打出来的 Debugger PIN: 163-136-028 一个字不差。算对了。
第四步:解锁控制台,拿下服务器
打开调试控制台 /console,锁着的,要 PIN。
说它是”下界传送门”一点不过分——门后面是能直接执行命令的 Python 环境,也就是服务器的命门。门锁着,密码就在我们手里:

把算出来的 PIN 填进去:

点确认,锁定层没了,冒出 >>> 提示符。在控制台里敲一句:
1 | |
回车:

1 | |
uid=0(root)——我们已经能以 root 身份在服务器上跑任意命令了。 从”看一眼网页”到”拿下整台服务器”,中间只隔了:一个忘关的 debug、一条路径穿越、一个能算出来的 PIN。
加餐:代码审计——PIN 算法和脚本是怎么来的
前面打是”黑盒”打:对着现象凑参数。这节带你”白盒”看一眼——去 werkzeug 源码里,把这 6 个材料和算法一行行翻出来。审计这套思路放哪个漏洞都通用。
审计四步走:
- 先认组件:报错页底部写着 “Werkzeug Debugger”,debug 调试器是 werkzeug 的,源码在
werkzeug/debug/__init__.py。 - 找核心函数:调试器要生成 PIN,肯定有个”算 PIN 的函数”。进
__init__.py一搜,get_pin_and_cookie_name()明摆着——获取 PIN 和 cookie 名。 - 读函数体,6 个材料当场现形:
1 | |
- 追 machine_id:点进
get_machine_id(),看到它读/etc/machine-id或/proc/sys/kernel/random/boot_id,后面还追加 cgroup 尾巴:
1 | |
前面那个”cgroup 尾巴”的坑,就是在这里看出来的。审计的好处:不是猜,是源码告诉我材料是哪几个。
脚本是怎么写出来的:
看源码最后这段算法,compute_pin.py 就是照着它”抄”的:
1 | |
翻译成大白话:
- 6 个材料按
public + private顺序喂给 sha1,字符串先encode("utf-8") - 全部拼完再追加
cookiesalt,sha1 结果前 20 位 hex 就是 cookie 名__wzd... - 接着再追加
pinsalt,把 sha1 结果当十进制整数,补足 9 位取前 9 位——就是 PIN
所以 compute_pin.py 干的事,就是把源码这几行”复制”过来,把 6 个变量换成我们用漏洞拿到的 6 个值。审计 → 出脚本,本质就是”读源码 → 抄逻辑 → 填数据”三步。
小技巧:审计时看到”函数名带 get_xxx、返回值是固定位数、用了 cookiesalt / pinsalt 这种盐”的凭证类函数,基本就是能反推的确定性密钥,值得顺着读。
结尾:几个常见疑问
问:这算 Python 的漏洞吗?
不算。Python 本身没毛病。机制在 Werkzeug 的调试器里,根因是部署失误——把开发用的 debug 模式带到了生产。Werkzeug 文档明明白白写着:debug 禁止用于生产。
问:那它是 CVE 吗?
不是。PIN 能算出来是 Werkzeug”故意这么设计的”——它赌的是这 6 个材料很难拿到,靠保密不靠密码学。Werkzeug 自己注释里都承认这事。真正能上报的是那条信息泄露(SSTI / 路径穿越),PIN 只是放大器。
问:是不是必须会 SSTI 才能打?
不是。路线 B(路径穿越读文件)就够了。只要是能读文件或执行代码的漏洞都行:LFI、任意文件下载、SSRF 读 file://、报错信息泄露……SSTI 只是最常见组合。
问:我不会 SSTI,这篇的思路能学吗?必须先去学 SSTI 吗?
不用,一个都不用学。这篇文章的主线本来就是文件读取(路径穿越),全程没碰 SSTI。SSTI 只是”能凑 PIN 的入口”之一,不是核心。核心思路是:先找到任意一个能读文件/执行代码的口子,再把 6 个材料凑齐,算 PIN,解锁。 而且文件读取比 SSTI 通用得多——PHP、Java、Node 的站也有路径穿越,学会这一招到处能用;SSTI 基本只在 Flask/Python 的站上高频出现。会了是多一把武器,不会一点不影响你复现这篇。
问:咋防护?
- 生产必须 debug=False;
- 真要调试,别把 /console 暴露出来,用防火墙或访问控制挡住;
- 依赖升级到新版(旧版 Werkzeug 调试器出过真 CVE:CVE-2023-25577,XSS,新版修了);
- 对公网站扫一遍目录,确认没有裸奔的 /console。
问:为啥非得锁版本?
不同 Werkzeug 版本算 PIN 的细节不一样:machine_id 取法、PIN 分组、哈希结构都变过。老 payload 在 Werkzeug 3.x 上算错。复现先看目标版本,再套对应算法。
行了,洞挖到这里,钥匙也到手了。我是凋零骷髅sec,回去继续下界挖矿了——下次见了,各位。
本文所有操作在本地 Docker 沙箱完成,仅供安全学习。生产环境合规操作,没授权别乱试。