Exploiting insecure Flask debugger console
Category: en
Introduction
With the popularity of Python in web server development, more and more people use frameworks like Flask. Flask is a web application framework, and it is not a web application server. Therefore, it needs a WSGI server to actually run the application.
Among those WSGI servers, Werkzeug is one of them. What is interesting about Werkzeug is that, it comes with a debugger. It is awesome during development, and with Debug=1 you can see what is going wrong with your web application.
However, when a developer forgets to disable debug mode in production, an attacker can make use of this to exploit the application.
Insecure Flask application
I will be using the following Flask application for the demo.
from flask import Flask, request
app = Flask(__name__)
@app.route('/')
def hello():
return '<html><body><h2>My Notes</h2><li><a href="/notes?file=secret.txt"> secure notes </a></li></body></html>'
@app.route('/notes', methods=['GET'])
def notes():
args = request.args
filename = args["file"]
with open(f'./notes/{filename}', 'r') as f:
return f.read()
return 'File not found'
Then I put a note under ./notes/secret.txt
Sup3rS3cr3t
Only 1 dependency which is Flask :) and you can run as follows.
FLASK_APP=main.py FLASK_DEBUG=1 flask run
It has a home page, that as a link to list of notes (1 for the demo), and /notes is GET request with query parameter for file name. However, there’s no input validation, and it is vulnerable to Local File Intrusion (LFI) vulnerability.
If you go to the homepage, you will see the following page.
If you click secure notes or curl the URL directory, you will see notes content.
However, since there is no input validation, LFI can be done as below.
LFI alone is dangerous, but it cannot be code execution yet. So this is where /console comes it. By default, Werkzeug console is PIN protected.
So no more code execution? NO
Because it can be cracked if we know some key parameters. We can know these information by first reading debug output. This can be done by crashing the application.
I read a non-existent file in order to crash the app and dump debug logs.
Exploiting the server
Using LFI vulnerability, I can read the debug logic code of werkzeug. This file also contains the code for generating the console PIN.
In order to crack the Werkzeug console PIN, I need the following 6 parameters.
1. username
can get from enumeration or, in my case, from logs /home/khant/…
2. modname
flask.app
3. app class name
getattr(app, '__name__', getattr (app .__ class__, '__name__'))
→ flask
4. flask app.py location
getattr(mod, '__file__', None)
→ from debug logs I can see where the package installed, so should be under /home/khant/Documents/work/security/demo/flask-hello-world/env/lib/python3.10/site-packages/flask/app.py
5. mac address
read /proc/net/arp from LFI to find devices then /sys/class/net/DEVICE_ID/address, and you will receive a mac address. Example 62:c8:fb:aa:85:39, and you need to convert the hexadecimal to decimal.
6. machine ID
Read /etc/machine-id or /proc/sys/kernel/random/boot_i
Once you got everything, you can use the code from Hacktricks to generate a PIN.
In my case, the pin is 298-808-092.
Now, when I enter this, I can log in to the Python console and execute system commands as below.
It means I have code execution access on the system with the rights of the user that is used to run the Flask application.
PS
In this article, I showed how I can use the LFI vulnerability to gather information for cracking /console password.
This is not always the case; sometimes you already have a foothold on the system, but you realize another user (possibly a privilege user) is running Flask server, therefore, you can use this attack to either escalate privileges or perform lateral movement to another user.