Flask Debug Mode RCE UPD
Flask is a very popular Python library for creating websites and APIs. I personally use it a lot in my projects and I see it deployed in production environments as well. When software engineers are developing applications they often enable debug mode for testing purposes. If this mode is enabled on production servers it can lead to remote code execution (RCE).
Flask Debug Mode RCE
Flask is one of the most popular Python libraries for building APIs. If your not careful you could introduce a vulnerability which would allow hackers to gain RCE on your system. Debug mode is dangerous and disabling the security pin is even more dangerous.
One of the easiest way to achieve code execution in PHP is by exploiting insecurely written file upload handling logic. If you are able to upload arbitrary PHP file by fooling the file upload logic, you can execute arbitrary PHP code. But when it comes to modern web frameworks written in Go, Node.js, Python, Ruby etc. it's a different story. Even if you managed to upload a .py or .js file to the server, requesting these resource via a URL often won't return anything as the route or URL is not exposed by the application. Even if you are able to access the resource by URL, it won't trigger any code execution as it's treated as a static file and just returns plain text source code. This post will explain how to get code execution in one such scenario in Python when you are able to upload compressed files to the server.Application security rule of thumb is never to trust user input. Don't just limit that concept to RAW HTTP request object that include query params, post body, files, headers etc. Carefully crafted compressed files that looks legit upon extraction can do bad things if it's handled by insecure code. This post is inspired from a Security Bug reported to MobSF and tries to cover the technical aspects of the vulnerability and exploitation. Let's take a look into the insecure code.
So if we can overwrite __init__.py file with arbitrary Python code inside a directory of the web application that act as a package, then we can achieve code execution if that package is imported by the application. For our code to execute, a server restart is required in most case. But in this example we are running a Flask server with debug set to True which means every time a Python file is changed, the server will do a restart.
In this example, the arbitrary code executed instantly as the Flask server was running on debug mode. This may not be the case elsewhere. You might need to wait until the server is restarted. Another problem is that we don't always know the package directory like config in this case. It's easy with an open source project where you have access to the source code. For closed source applications, you can take a good guess for package directories like conf, config, settings, utils, urls, view, tests, scripts, controllers, modules, models, admin, login etc. These are some of the common package directories found in some Python web frameworks like Django, Flask, Pyramid, Tornado, CherryPy, web2py etc.Alternatively, let's say the web application is running inside Ubuntu Linux. The installed and inbuilt Python packages will be available under: /home//.local/lib/python2.7/site-packages/pip. Assuming that the app is running under user directory, you can craft a filename like ../../.local/lib/python2.7/site-packages/pip/__init__.py. Upon extraction, this creates __init__.py file inside pip directory. If the app is using virtualenv and let's say the virtualenv directory is venv, you can use a filename like ../venv/lib/python2.7/site-packages/pip/__init__.py. The will brick pip, but next time someone run pip command in the server, your code will execute!
It's a well known fact that Flask's debug=True option can lead to remote code execution via werkzeug debugger capabilities and even several resources were hacked. I decided to look into it and it turned out that the technique doesn't work if the app is being run by a forking application server like uwsgi or gunicorn. So the questions are:
Keeping this in mind, I knew flask file write to RCE was a possibility, in certain circumstances. For example, if the script imported other custom modules in a different directory that you have to write access to, it will create an over-writable __init__.py file (See More). Or if you can somehow write to an authorized_keys file, you can SSH in.
However, this challenge did neither of the above. For a long time, I was stumped on how I could actually achieve RCE. In my mind, I thought, if I overwrite the /app/run.py file, it will crash the service, and I will be locked out, so I avoided testing it the whole time (I even nmapped the challenge to find other ports...). I spent a while staring at "Error occured during the zip upload process!" and reading about how flask works. I then remembered learning that a flask app runs in debug mode will automatically restart the service when a change is made to the application's script (See more).
Great. This carried on for a while, and I couldn't quite figure out why. I decided to use the flask template rendering to give me a more in-depth look into what the application has access to (what modules are accessible, for example).
As you can see this website runs on flask and you could extract the location of the python file that flask is runnning on from the error log.```File "/home/web_3/app.py", line 15, in read```So I tried to access the file with :7004/read/app.py and it showed me the source code.```pythonfrom flask import Flask from flask import render_template import random
```From there I was stuck for a while but I noticed after a bit of researching that there was a console under :7004/console. The problem was that it was secured with a pin.So I ran the sourcecode on my local machine and tried it there. I got a pin``` * Serving Flask app "app" (lazy loading) * Environment: production WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. * Debug mode: on * Running on :7004/ (Press CTRL+C to quit) * Restarting with stat * Debugger is active! * Debugger PIN: 234-662-675 ``` I wanted to know how the pin was calculated so I looked in the python directory of werkzeug for a generate pin function and I found this file ```/usr/local/lib/python2.7/site-packages/werkzeug/debug/__init__.py```and there was this function```pythondef get_pin_and_cookie_name(app): """Given an application object this returns a semi-stable 9 digit pin code and a random key. The hope is that this is stable between restarts to not make debugging particularly frustrating. If the pin was forcefully disabled this returns `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()]
private_bits = [ str (uuid.getnode ()) , get_machine_id () ] ```I debugged the function and stepped through every step to get to know what the different strings are.The username was just the plain username. In my case _h4ckua11_. The modname was just _flask.app_ and the thing after that was _Flask_. The last bit of the public_bits was the location of the location of the main flask directory _/usr/local/lib/python2.7/dist-packages/flask/app.py_.Now to the private bits.The _str (uuid.getnode ())_ was the MAC-Address in decimal ad:ce:48:11:22:33->191101483950643.Then it called the the _get_machine_id()_ function so I searched for it.```pythondef get_machine_id(): global _machine_id rv = _machine_id if rv is not None: return rv
print (rv)``````234-662-675```So I tried it on the server.Since I knew that the _app.py_ was in _/home/web_3/_ I had to get two directories back. With simple url encoding I got to the _/_ directory ( :7004/read%2F..%2F../).I read all the files that I needed for this:```probably_public_bits = [ 'web3_user' , # username :7004/read%2F..%2F../etc/passwd 'flask.app' , # modname Always the same 'Flask' , # Always the same '/usr/local/lib/python3.5/dist-packages/flask/app.py' # getattr (mod, '__file__', None) Error Message: :7004/read%2F..%2F../wrong/file ]
As you can see, the difference is night and day. Without having the interactive debugger enabled you get no information about the error in your browser. You would have to go your terminal and read the stack trace there.
TL;DR, Patreon got hacked. We reported a specific Remote Code Execution to them due to a public debugger before they were breached. We believe this was the attack method due to the simplicity and availability of the vulnerable endpoint. This is how you prevent this from happening to you.
Yesterday Patreon, which is a funding platform for artists and creators, went out with a Security Notice about a compromise happening on the 28th of September on one of their debug versions which was publicly available. Shortly after that, data from this instance, which contained live data, was publicly posted which you can read about here and here.
Their debug version of the application was running with the Werkzeug Debugger publicly available, this has also been shown in Shodan.io for at least a few weeks, this image is from the 11th of September:
This is python! Flexibility and simplicity should always be kept in mind. For this reason, we can use an extremely useful feature that comes as a filter for the flask templates: the format string feature.
Template engines are designed to combine templates with a data model to produce result documents which helps populating dynamic data into web pages. Template engines can be used to display information about users, products etc. Some of the most popular template engines can be listed as the followings: