Web Security Vulnerabilities - Server Side Template Injection (SSTI)

8 minute read

SSTI

Table of Contents

What is Server Side Template Injection?

Server-Side Template Injection, also known as SSTI, is a web security vulnerability that allows an attacker to inject malicious code into a template. This code is written using the native template syntax, which is then executed on the server.

SSTI vulnerabilities arise when an application includes user input directly in the template without proper validation, instead of passing it as data to be safely rendered on the server.

Template engines are designed to generate web pages by combining fixed templates with volatile data. Templates also enable fast rendering of the server-side data that needs to be passed to the application. The template engine replaces the variables in a template file with actual values and displays these values to the client.

For example: jinja and twig

Example:

Suppose we have an application that allows a user to log in and displays their username using the Jinja2 template engine in Python.

The HTML code looks like:

<!DOCTYPE html>
<html>
<head>
    <title>Server-Side Template Injection Example</title>
</head>
<body>
    <h1>Welcome, !</h1>
</body>
</html>

The Flask route that renders this template looks like:

from flask import Flask, render_template, request

app = Flask(__name__)

@app.route('/')
def index():
    name = request.args.get('name', 'Guest')
    return render_template('index.html', name=name)

Here, the name variable is taken from the query string parameter name and rendered directly in the template without sanitization, allowing attackers to inject template expressions. If an attacker submits a malicious input like 7 as the name parameter in the URL, like:

GET /index.html/?name=7
HOST: example.com

The template engine will evaluate this expression as 49 and render the page with Welcome, 49!, which indicates an SSTI vulnerability.

What is the impact of SSTI?

The impact of an SSTI vulnerability depends on the template engine and how the website uses it. These vulnerabilities often have a critical impact, as they can result in Remote Code Execution (RCE). Even without code execution, the attacker may be able to read sensitive data on the server.

How to find SSTI?

There are three main steps to finding an SSTI vulnerability:

  1. Detect
  2. Identify
  3. Exploit

Detect

The first step in exploiting an SSTI vulnerability is detecting whether an application is vulnerable. The simplest way to find an SSTI vulnerability is by fuzzing the template with special characters commonly used in template expressions, such as $%. If the server raises an exception or returns an error, this might indicate the presence of a vulnerability.

Identify

The second step is identifying what is the template the application uses. Submitting an invalid syntax might reveal the template engine due to an error message returned by the server, Alternatively, you can inject arbitrary mathematical operations using syntax from different template engines and observe whether they are successfully evaluated.

The image below represents a decision tree to help in this process:

SSTI_Identify

It is important to note that sometimes one payload can successfully return different results in two or more templates. Therefore, it is essential not to jump to conclusions based on a single successful response.

Exploit

After detecting the vulnerability and identifying the template engine, it’s time to exploit it. The best approach is to read the documentation of the template engine to understand its basic syntax and built-in methods, which will help in exploiting the server.

How to prevent SSTI?

The most effective way to prevent SSTI attacks is not to include user input directly in the template engine without validation or sanitization. However, if you must include user input, you can use logic-less template engines such as Mustache or Handlebars , which completely separate code interpretation from visual representation.

Example of including user input directly (Python with Jinja2):

Vulnerable Code:

@app.route('/')
def index():
    name = request.args.get('name', 'Guest')
    # Directly injecting user input into the template
    return render_template_string(f"Welcome, {name}!")

Secure Code:

@app.route('/')
def index():
    name = request.args.get('name', 'Guest')
    # Passing user input as data to the template
    return render_template('index.html', name=name)

We can see in the secure code, name is passed as a variable, and the template engine automatically escapes it to prevent injection.

Example of using logic-less template engines (Node.js with Mustache):

According to Wikipedia

Mustache is a web template system. It is described as a logic-less system because it lacks any explicit control flow statements, like if and else conditionals or for loops; however, both looping and conditional evaluation can be achieved using section tags processing lists and anonymous functions (lambdas).

Vulnerable Code:

const template = `Hello, ${userInput}`;
res.send(Mustache.render(template));

Secure Code:

const Mustache = require('mustache');

const template = 'Hello, '; // Logic-less template
const data = { name: userInput };   // User input passed as data
res.send(Mustache.render(template, data));

Mustache automatically escapes special characters (e.g., <, >, ", &) to prevent HTML injection.

Time to practice

Now let’s practice and understand the vulnerability in simulating real-scenarios.

At first glance, we can see that the website is missing the name parameter. Let’s send a request and include this parameter.

Challenge_Overview

If we inject random text into the name parameter, we can see it reflected on the web page.

Let’s inject some special characters to observe how the website processes them.

Screenshot_2024-04-26_09_19_29

As we can see in the image above, the website reflects special characters without validation. So we can try testing for XSS.

Challenge_Xss

And yes, we successfully triggered XSS😁. But wait, is the website vulnerable only to XSS? Why not try testing for other vulnerabilities?

Let’s try testing for SSTI. There are many payloads to try, so let’s first try ``. If it’s vulnerable, it should return a result.

Challenge_Test

And we can see the payload rendered as 49, indicating to an SSTI vulnerability.

You can get payloads from HackTricks or PayloadAllTheThings

Now that we know there’s an SSTI vulnerability and the template engine is Jinja2 (a Python template engine), let’s proceed.

Everything in Python is an object, and each object has a class from which it is instantiated.

First, we need to call ''.__class__.__base__:

  • '': This is an empty string literal.
  • .__class__: This is a special attribute that returns the class of an object. So, ''.__class__ returns the class of the empty string, which is the str class in this case.
  • .__base__: This is another special attribute that returns the base class of a class. Since str is a built-in type in Python, its base class is the object class. Therefore, ''.__class__.__base__ returns the object class, which is the base class of the str class.

Challenge_Class

Next, we need to call the subclasses() method to return a list of subclasses of the str class.

Challenge_Subclasses

We have many subclasses. What we need is to choose a subclass that imports the sys module to execute commands. There are many subclasses; we will use the IncrementalEncoder class.

Challenge_Encoder_class

Let’s access the IncrementalEncoder class using ''.__class__.__base__.__subclasses__()[127].__init__.

Challenge_Encoder_init

Now that we’re in the class file, let’s access the modules in the class, and we’ll find sys.

Challenge_Encoder_globals

Once we’re in the sys module, we can access the os module and use it to execute system commands.

Challenge_Sys

Challenge_OS

The final payload will be:

{''.__class__.__base__.__subclasses__()[127].__init__.__globals__['sys'].modules['os'].popen('ls').read()}.

Python Popen is a class within the subprocess module that allows us to spawn new processes, connect to their input/output/error pipes, and obtain their return codes. It enables Python programs to run shell commands, system commands, and other external processes directly from within the script.

read() reads the output and returns it as a string.

The Python subprocess module is a tool that allows you to run other programs or commands from your Python code. It can be used to open new programs, send them data and get results back.

Challenge_directories

Finally, let’s read flag.txt.

Challenge_flag

The final tip is not always stop at just XSS.

Resources

Conclusion

In today’s tutorial, we talked about SSTI and how to find it with its impact and prevention. We also practice on a detailed lab (designed by me). Hope you enjoy reading this guide on Server-Side Template Injection (SSTI). I hope you found the information helpful and are better equipped to identify and exploit SSTI vulnerabilities.

Final Words

This is the last part of Web Security Vulnerabilities tutorial, hope you learn something from this tutorial.

If you found this series of web security vulnerabilities helpful, you can follow me for updates and give your feedback on twitter to improve myself in the future tutorials.

Keep Going. Thanks for reading.

Categories:

Updated: