Introduction

Last week I took part in a CTF, and one of the problems from the “Web” category seems pretty intriguing to me. Although I was able to get the Flag, but couldn’t submit the flag in time, so no points for me 😔.

The interesting thing about this problem was, that the technology which needed to be exploited was something I am pretty familiar with and had a lot of experience in, but still, even after working with that tech stack for years, I was unaware of this vulnerability. So I thought of publishing this write-up to share some of my findings and help you better understand how to write safe code. Even if you are not interested in security research and CTF’s I would still recommend you read this write-up, it may help your company save a fortune in post data breach marketing campainging 😁 and it will make you a better web developer for sure.

Prerequisite

  • Python
  • Basic HTML
  • Basic understanding of Web

Understanding the Source code

The challenge was to exploit a web page containing an HTML form, which would take text as user input and render a new web page with the user input as a parameter. The tech stack used for backend was Flask framework with Jinja2 for rendering template.

Below you can find the replica of that web page (pretty simple as you can see) that I have created, you can find the source code here. Now let’s look at the code and find out how this program functions. First we will look at all the modules we are using.

# main.py
1. from flask import (Flask, request, render_template_string)
2. from jinja2 import Template

So we are importing the Flask class to initiate the project, then request class to manage and work with requests that will be generated by the browser, Template class from jinja that will be given as an input to the render_template_string function that will render the actual web page from the string and return as the response.

After that we create a flask instance and load the appropriate configurations within the flask instance.

# main.py
3. app = Flask(__name__)
4. app.config.from_pyfile('config.py')

config.py contains all secrets as well, here are the contents of our config.py:

# config.py
import os
VERY_SECRET = os.environ.get('VERY_SECRET')

In the above codeVERY_SECRET can be any secret which you don’t want to expose (social auth tokens, database passwords etc).

Note: We don’t actually hard code the secret in the file, but store it as an environment variable and our code merely fetches that secret into Flask instance. You can read more about it here.

Next as you can see there are two web pages in our application first webpage that takes input from the user and other page for printing the “Hello” message. So for that we have two request handlers first one named get_username that renders a form and take user input for the username, and makes a POST request to say_hello request handler.

# main.py
@app.route("/",  methods=['GET'])
def get_username():
    return render_template_string(
        """
        <form action="/hello" method="POST">
            <label for="username">Enter username:</label><br>
            <input type="text" size="50" name="username" ><br>
            <input type="submit" value="Submit">
        </form> 
        """
    )

say_hello request handler is the endpoint for the POST request. It fetches the username value from the request.args (this value was given by the user). Next, we use a Jinja template object which takes our HTML string and {{ username }} as a parameter which will be later parsed by the Jinja and the actual value of username is placed there, and then we have a call to render_template_string which returns the appropriate template as an HTTP response. Pretty standard stuff.

# main.py
@app.route("/hello",  methods=['GET', 'POST'])
def say_hello():
    username = request.args.get('username')
    template = Template(
        """
        <h2> Hello <i style='color: royalblue;'> \
            {{ username }} </i>! Nice to meet you </h2>
        """
    )
    source = template.render(username=username)
    return render_template_string(source)

And finally the program ends with the main method (more like condition).

if __name__ == '__main__':
    app.run(debug=True)

Understanding the Exploit

Jinja Basics

As the title suggests we would be exploiting Jinja in this attack. Now the important thing to understand about jinja is that it’s not just a placeholder that you can use during template design. It’s like an programming language or more appropriately a text-based template language, with if-else, loops, and even some basic functions that you might come across in many languages. For example below you can see a demo of a Jinja template which would print all the odd and even numbers from 1 to 5, with labels.

{% for i in range(1, 5+1) %}
    {% if i%2 == 0 %}
        <p style="color:blue;">{{ i }} is Even</p>
    {% else %}
        <p style="color:red;">{{ i }} is Odd</p>
    {% endif %}
{% endfor %}

And the output html for the above code will be:

<p style="color:red;">1 is Odd</p>
<p style="color:blue;">2 is Even</p>        
<p style="color:red;">3 is Odd</p>
<p style="color:blue;">4 is Even</p>
<p style="color:red;">5 is Odd</p>        

Point of Attack

The exact line of code that will be expoited by our template injection would be:

# main.py
@app.route("/hello",  methods=['GET', 'POST'])
def say_hello():
    ...
    source = template.render(username=username)
    return render_template_string(source)

Source contains the HTML generated by the Jinja template in type str, and render_template_string renders the string into an HTML page that will be returned to the client side.

The vulnerabilty is that the username parameter is given by the user and no form input sanitization or check is done on the username value, so we can basically pass any valid template code and it would be executed. In the next section we would discuss how to identify the vulnerabilty and how to exploit it.

The Server Side Template Injection

Identification

First we need to verify that SSTI is possible in our case for that, we would simply input any valid Jinja template code and if the handler executes it, we would know that a vulnerability exists.

Let’s give {{ "exploit".upper() }} as input, the output that we will get is :

As we can see the render_template_string executed our template injection and converted the string into uppercase. So since now we know that SSTI is indeed possible, let’s see what else we can do.

Python Objects as Injection

Okay! So we have successfully executed template injection, great… So what ? It’s just a templating language what the worst thing that we can do with it.

The thing is Flask provides some special objects as context to the template, objects like request, self etc. These can be used by the template designer for various puproses. And these objects are the serialized version of python objects, let’s see what we can do with it.

Let’s try giving {{ self.__dict__ }} as input (the dict will get all the serializable parameters in a dictionary format), the output that we will get is:

Woh.. that’s a lot of stuff right there. You can see a lot of configuration stuff, some other objects, some references etc. But the thing that interests us the most is that VERY_SECRET key, it’s an environment variable that the config.py file of your application was reading. In real life this could be very dangerous, you could leak a lot of important data. But this is not the best we can do, let’s go for Remote Code Execution (RCE).

Some injections to try:

  • {{ request }}
  • {{ g }}
  • {{ config.items() }}
  • {{''.__class__.__mro__[2].__subclasses__() }}
  • {{ [].class.base.subclasses() }}

Remote Code Execution

Now for remote code execution, we will try to get access to the shell. If we can get the shell to execute some of our code then we could potentially do anything.

I will try to execute __globals__ this method will return all the functions present in any given namespace. So I will input {{ self.__init__.__globals__ }}.

But wait.. we are trying to get our code executed and we don’t actually need to get everything from the server, so we will add one more method to this that is __builtins__, it is a module containing the built-in functions and types. So we can directly access most of the modules provided by python from here.

After inputing {{ self.__init__.__globals__.__builtins__ }} we get:

Okay that’s a lot to unpack.. so you can observe that all the methods here are standard python stuff. But the most interesting one(which is also marked) is the exec method. exec can be used to execute the dynamically created program, which is either a string or a code object. So basically we can just past some valid python code as string and exec will happily execute it for us.

With all the information we have, let us create a payload. First our injection so far and with a call to exec method.

{{ self.__init__.__globals__.__builtins__.exec() }}

As a Proof of concept, I will try to a execute touch command using this injection. if an attacker can create a file and execute it, that’s game over. So valid python code for us would be:

import os
os.system('touch attack.sh')

Now add this code to our payload and we get:

{{ self.__init__.__globals__.__builtins__.exec("""import os; os.system('touch attackInator.sh')""") }}

This injection should create a file name attackInator.sh in our project root directory. So let’s move to the project directory and have a look

As we can see, our injection successfully created a shell file and executed it as well. With RCE we can achieve a lot more than just creating and executing a simple touch command. We can basically hi-jack the entire functioning of the application, leak database info, etc.

Conclusion

We created and successfully executed a server side template injection in Jinja and python. Such injections are also possible in other programming languages and frameworks as well. To safegurd from such attacks make sure to sanitize user input and follow best practices in whatever framework you are working in.

Techonolgies are not exploited, what’s actually exploited is the laziness and sloppiness of the developer.

Thanks for reading, See you in the next one 😃 .

Refrences

Peace was never an option 🦆🔪