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.
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.
- Take the username of the user running the Flask application.
- Take the module name of the Flask application.
- Take the application name of the Flask application.
- Take the path to the Flask
app.py
file. - Take the output of
uuid.getnode()
, which is the MAC address of the network interface hosting the application, in decimal form. - Take the ID from
/etc/machine-id
. - 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. - Concatenate all of the above values into a single string.
- Take the MD5 hash or SHA1 hash of the concatenated string.
- 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
which shows the uid and gid which can then be matched up with the contents 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
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
Which we can then convert to decimal using python
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.
This script will produce an output such as the one below.
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.