
PoC laboratorio Docker: encadenando bypass de subida de archivos + XSS almacenado para crear cuentas de administrador. Recurso educativo para pentesters.
Una aplicación web deliberadamente vulnerable que demuestra cómo un bypass de carga de archivos se encadena con XSS almacenado para crear cuentas de administrador puerta trasera, incluso cuando las protecciones CSP, CORS y CSRF están activas.
Publicación completa del blog: Blog de KurtiseBear
Este es un laboratorio educativo para formación en seguridad defensiva. No lo implementes en ningún lugar accesible públicamente.
La aplicación tiene controles de seguridad reales:
'self' (pero con 'unsafe-inline' y 'unsafe-eval')Un atacante con una cuenta de usuario de bajo privilegio encadena dos vulnerabilidades para evadirlas todas:
Bypass de carga de archivos -- El formulario de carga restringe a .pdf mediante un atributo accept del lado del cliente, pero el servidor no realiza ninguna validación del tipo de archivo. Un atacante sube un archivo .js que contiene JavaScript. El endpoint de descarga lo sirve desde el mismo origen, por lo que CSP y CORS no lo bloquean.
XSS almacenado a través del asunto del mensaje -- La función de mensajería almacena la entrada del usuario sin desinfección. La bandeja de entrada del administrador renderiza el asunto del mensaje como HTML sin procesar. La carga útil XSS utiliza un manejador `` para obtener el script subido y ejecutarlo con eval(). CSP lo permite porque se permiten 'unsafe-inline' y 'unsafe-eval'.
Falta de CSRF en el endpoint de la API -- La API de gestión de usuarios (/api/manage-user.php) no valida los tokens CSRF, aunque los endpoints de formulario sí lo hacen. La carga útil XSS llama a esta API utilizando la sesión del mismo origen del administrador. Incluso si CSRF estuviera presente, el JavaScript del mismo origen podría leer el token del DOM.
El resultado: cuando un administrador abre su bandeja de entrada, el XSS se activa, el JavaScript crea una cuenta de administrador puerta trasera usando la sesión del administrador. Cada defensa está en su lugar y funcionando. La cadena funciona porque nunca sale del origen.
docker-compose up -d
Espera de 10 a 15 segundos para que MySQL se inicialice, luego visita http://localhost:8080
| Rol | Correo Electrónico | Contraseña |
|---|---|---|
| Admin | [email protected] | admin |
| Usuario | [email protected] | user |
Navega a http://localhost:8080 e inicia sesión con [email protected] / user.
Ve a Subir Archivos. El formulario dice "Solo PDF" pero solo lo aplica del lado del cliente. Puedes:
accept=".pdf" del campo de archivo, oSube el archivo payload.js proporcionado (o el tuyo propio). Toma nota del ID de archivo devuelto (ej., 1).
El archivo subido ahora se sirve desde /api/download.php?file_id=1 en el mismo origen. CSP no bloqueará las solicitudes a este endpoint porque es 'self'.
Ve a Enviar Mensaje. En el campo de asunto, ingresa:
r.blob()).then(b=>b.text()).then(eval)">
(Reemplaza 1 con el ID de archivo real del paso 2.)
Pon cualquier cosa en el cuerpo. Marca prioridad si lo quieres al principio de la bandeja de entrada. Envía.
El manejador onerror funciona porque CSP permite 'unsafe-inline'. El eval() funciona porque CSP permite 'unsafe-eval'. La solicitud fetch al endpoint de descarga funciona porque es del mismo origen.
Cierra sesión. Inicia sesión como [email protected] / admin. Ve a Bandeja de entrada.
El asunto del mensaje se renderiza como HTML sin procesar. La etiqueta `` falla al cargar, el manejador onerror se activa, obtiene la carga útil subida y eval() la ejecuta. La carga útil hace una solicitud POST a /api/manage-user.php usando la cookie de sesión del administrador (adjuntada automáticamente para solicitudes del mismo origen). No se necesita token CSRF porque el endpoint de la API no lo verifica.
Ve a Usuarios. Deberías ver un nuevo usuario: BackdoorAdmin con rol admin y correo electrónico [email protected].
Cierra sesión e inicia sesión con [email protected] / Compromised1! para confirmar.
CSP blocks external scripts
--> But the payload is hosted on the same origin via file upload
--> And unsafe-inline/unsafe-eval allow the onerror handler and eval()
CORS blocks cross-origin requests
--> But every request in the chain is same-origin
CSRF tokens protect form submissions
--> But the API endpoint doesn't validate them
--> And even if it did, same-origin JS can read tokens from the DOM
Session cookies have standard protections
--> But same-origin requests carry them automatically
Lo que realmente rompería esta cadena:
htmlspecialchars() en toda la salida controlada por el usuario. La bandeja de entrada renderiza $row['subject'] sin procesar. Esto elimina el XSS por completo.'unsafe-inline' y 'unsafe-eval'. Usa nonces o hashes para scripts en línea legítimos. Esto bloquea el manejador onerror y eval().docker-compose down -v
Esta aplicación es deliberadamente vulnerable. Está diseñada únicamente para fines educativos y de formación en seguridad defensiva. No la implementes en ninguna red accesible a usuarios no confiables. No utilices estas técnicas contra sistemas sin autorización explícita por escrito.