In the previous part of this series, we have demonstrated a simple SSTI attack on Jinja2 templating engine. In this section, we will be explaining out the underneath concept which allowed us to get the contents of /etc/passwd file.

But before diving further into reading the /etc/passwd file, let’s try to execute a plain text in the vulnerable application.

The following is the truncated code snippet used in the vulnerable application:

from flask import Flask, render_template_string, request
 
app = Flask(__name__)
 
@app.route("/")
def home():
    user_input = request.args.get('asura')
    return render_template_string(user_input)
 
if __name__ == "__main__":
    app.run(debug=True, host='0.0.0.0', port=5001)

As you can notice that the web application is written in flask and it is taking a GET parameter named input and the same is rendered on the website. Let’s try to input a simple Hello World string and check whether the same is printed on the web page or not.

Understanding Jinja2 Payload

In the previous part of the series, the payload used to print the /etc/passwd file is as follows:

{{ config.__class__.from_envvar.__globals__.import_string("os").popen("cat /etc/passwd").read() }}

In the above code segment, config variable is the first interaction point. But where this config variable comes from???

The answer lies in the Flask framework, while executing a Jinja environment config variable is defined inside the flask, therefore we are able to refer the same.

The next parameter, __class__ takes the class of the object which is our case <class 'flask.config.Config'>. Now the question lies in why from_envvar function from this class is referred?

Let’s try to look at the file containing the from_envvar function (site-packages/flask/config.py):

If you look at Line 9, there is a function named import_string imported from werkzeug.utils package.

Question

Why we need import_string function?

If you look at the official docs of werkzeug 1, import_string is a function which imports an object based on a string. Therefore, we can write a normal python code like importing os package and create a shell using popen to execute any arbitrary command.

Now, backtrack to where we left out our discussion, from_envvar function is used because we intented to use import_string function, but from_envvar does not import this function but it’s parent file therefore __globals__ provides access to globally defined variables and functions and thus we are able to execute import_string function and able to read the /etc/passwd file.

Source Code Evaluation

Referring to the previous code segment of our web application, a variable named user_input is defined to take the GET parameter named input from the user.

from flask import Flask, render_template_string, request
 
app = Flask(__name__)
 
@app.route("/")
def home():
    user_input = request.args.get('asura')
    return render_template_string(user_input)
 
if __name__ == "__main__":
    app.run(debug=True, host='0.0.0.0', port=5001)

In the above code segment, you can see that input parameter is directly rendered on the web page without any input validation.

Bug

Provided input parameter is not checked or sanitised before rendering on the web page

The mitigate the same, we can check for the presence of {{ and }} in the input string, if the same is present in the input parameter, we can abort the operation by throwing a 400 Bad Request error as follows:

from flask import Flask, render_template_string, request, abort
 
app = Flask(__name__)
 
@app.route("/")
def home():
    user_input = request.args.get('asura')
    if "{{" in user_input and "}}" in user_input:
        abort(400)
    return render_template_string(user_input)
 
if __name__ == "__main__":
    app.run(debug=True, host='0.0.0.0', port=5001)

Let’s verify with our simple {{ 7*6 }} payload and check it will result in 400 Bad Request error.

Bonus

As shown in the first section of this blog, a simple string and Jinja template is successfully rendered on the web page. But what about injection of Javascript Code? Let’s try to execute a normal javascript code by passing the following in input parameter of the web application.

<script>alert("Hello World!!!")</script>

The following output shows the successful injection of a simple javascript payload on the web application.

This section of the page demonstrates that same code segment is vulnerable to two injection attacks which are SSTI and Javascript injection. Similarly, in real-life applications a same code base can be suspectable to various vulnerabilities and our duty as a vulnerability researcher or an attacker is to find all the possible vulnerabilities.

Question

Why Javascript code execution helps in executing an attack?

Mitigation

For mitigation SSTI, we have checked for the presence of {{ and }} characters in our input parameter, but for javascript code we will use escape function from markupsafe python native library 2. The escape function changes the special characters to their relative HTML character entities i.e. < is converted to &lt;, therefore preventing Javascript injection attack.

The updated code snippet mitigating both SSTI and Javascript injection is mentioned below:

from flask import Flask, render_template_string, request
from markupsafe import escape
 
app = Flask(__name__)
 
@app.route("/")
def home():
    user_input = request.args.get('input')
    if "{{" in user_input and "}}" in user_input:
        abort(400)
    safe_input = escape(user_input)
    return render_template_string(safe_input)
 
if __name__ == "__main__":
    app.run(debug=True, host='0.0.0.0', port=5001)

The same payload will just print the Javascript code as a plain string now.

Bonus - Other Jinja2 Payload

Question

If config variable is not defined while executing the payload? Does it mean that the web application is not vulnerable to RCE exploit?

Not necessarily, we can try different payloads and may be some payload can result in successful vulnerability execution.

One more generic example of payload is as follows:

{{ [].__class__.__base__.__subclasses__()[529]('cat /etc/passwd',shell=True,stdout=-1).communicate()[0].strip() }}

Note: Index 529 used can be different depending on the web application.

In our first payload, we tried calling the popen function, such that we can use the cat command to read the /etc/passwd file. So, our main aim lies in getting our hands on executing the popen function.

Any default primitive value like a string, a list, a dict are all inherited from <class 'object'>. In our example, we have used list (but we can use empty string '' or {} which will result in same outcome) and similar to first payload, we try to get hands on <class 'object'> which is just the base class of str, list or dict. Therefore, __class__.__base__ provides access to <class 'object'>.

The next part of our payload __subclasses__() provides all the functions available in <class 'object'>. Let’s execute the following payload to get list of all available functions to our object class.

{{ [].__class__.__base__.__subclasses__() }}

In the above output, we can see that popen is available for our use. Now copy the output and find the index of the popen function. In our example, it is at 529 index. Therefore, [].__class__.__base__.__subclasses__()[529] will invoke the popen function internally.

And next is the parameters passing to our popen function is 'cat /etc/passwd',shell=True,stdout=-1, and at last communicate function reads all output and waits for the subprocess to exit.

In this part of the series, we have gone through the Server Side Template Injection (SSTI) attack along with understanding of the payload and it’s mitigation methods.

References

Footnotes

  1. https://tedboy.github.io/flask/generated/werkzeug.import_string.html

  2. https://markupsafe.palletsprojects.com/en/2.1.x/