Descripción del laboratorio
GhostFlix es un clon deliberadamente vulnerable de un servicio de
streaming: cuentas gratis y Premium, catálogo con contenido bloqueado, reseñas y cupones de descuento.
Corre en local (python app/app.py) y no depende de ningún servicio externo.
El objetivo, en los cuatro retos, es el mismo: conseguir funciones Premium en una cuenta gratuita, cada vez explotando un fallo de lógica de negocio distinto. Ningún reto requiere adivinar contraseñas ni saltarse autenticación — el fallo está siempre en cómo el backend confía (de más) en el cliente.
POST /api/account/updateGET /api/sessionGET /api/reviews/<id>POST /api/coupon/redeem
Antes de empezar: crea una cuenta en /register y configura el navegador (o usa el navegador
embebido de Burp) para que pase por el Proxy. Cada reto puede resolverse de forma independiente —
usa /reset en cualquier momento para devolver tu cuenta a plan Free.
1Mass Assignment
La página Mi cuenta solo debería dejarte cambiar tu nombre visible y tu avatar. Pero el endpoint que atiende ese formulario no filtra qué campos acepta: vuelca todo el JSON recibido directamente sobre tu registro de usuario.
Cuenta recién creada, plan free, sin meses de Premium.
En Proxy > Intercept, el botón empieza en Intercept is off.
Lo activamos para poder editar la petición antes de que llegue al servidor.
En la app, en Editar perfil, cambiamos el nombre visible y pulsamos
Guardar cambios. Burp retiene la petición POST /api/account/update:
Directamente en el editor de Burp, añadimos dos líneas al JSON: "plan":"premium" y
"premium_months":12. El servidor nunca ha ofrecido ese campo en el formulario —
lo estamos inventando nosotros.
Content-Length: Burp lo recalcula solo al reenviar
desde el editor de Intercept.
Reenviamos la petición, desactivamos Intercept y recargamos /account.
user.update(request.json) sin una lista
blanca de campos permitidos. Cualquier atacante que sepa el nombre de un campo interno (plan, rol,
is_admin…) puede escribirlo.
2Match & Replace — confianza en el cliente
El catálogo oculta el contenido Premium con un candado visual, decidido en el navegador a partir de la
respuesta de /api/session. El fallo: el endpoint que realmente sirve el contenido
(/api/stream/<id>) no vuelve a comprobar nada en el servidor. Si conseguimos que el
navegador crea que somos Premium, el candado desaparece solo.
Los títulos Premium muestran el candado y el botón «Reproducir» está deshabilitado.
En Proxy > Match and replace, añadimos una regla nueva sobre el
Response body: sustituye "plan":"free" por "plan":"premium".
Con la regla activa, cada respuesta de /api/session que pase por Burp llega al
navegador ya alterada. El JavaScript del catálogo, que solo confía en ese JSON, desbloquea la UI:
Pulsamos «Reproducir» en un título Premium. La petición llega a /api/stream/<id>,
que nunca comprobó el plan — responde con éxito igualmente.
/api/stream/<id> confía en que si el frontend te dejó pulsar el botón,
es porque ya comprobó tu plan. La seguridad real nunca puede depender del frontend.
3API que expone de más
La sección de reseñas de un título es pública — no hace falta ser Premium para leerlas. El problema es qué hay dentro de cada reseña: en vez de exponer solo usuario, puntuación y comentario, el backend serializa el objeto de cuenta completo del autor, incluyendo su plan y su cupón de bienvenida todavía sin canjear.
En el catálogo, con cuenta free, pulsamos «Ver reseñas» en un título con comentarios de otro
usuario (streamia_fan99).
En Proxy > HTTP history buscamos la petición GET /api/reviews/2 y
miramos su respuesta cruda — no lo que renderiza la página, sino el JSON completo que envía el servidor.
El cupón filtrado (WELCOME-STREAMIA_FAN99) pertenece a otra cuenta, pero el endpoint
de canje no comprueba de quién es — solo si ya se usó. Lo pegamos en el formulario de cupón de
nuestra cuenta free y pulsamos «Canjear».
4Race Condition
Cada cuenta tiene su propio cupón de bienvenida, pensado para canjearse una sola vez. El servidor comprueba si ya se usó, espera (simulando una llamada lenta a una pasarela de pagos) y luego lo marca como usado. Esa ventana entre el «check» y el «marcar como usado» es exactamente lo que vamos a explotar disparando muchas peticiones a la vez.
Reseteamos la cuenta (/reset) para que el cupón WELCOME-ALUMNO01 esté sin
canjear, enviamos la petición de canje a Repeater y confirmamos el código en el body.
Clic derecho sobre la pestaña > Add tab to group > New tab group. Con el grupo creado, clic derecho otra vez > Duplicate tab > 14 copias (15 en total).
En el desplegable junto a Send, elegimos Send group in parallel (last-byte sync): Burp Community Edition abre las 15 conexiones a la vez y suelta el último byte de cada petición casi simultáneamente, maximizando la ventana de carrera.
Revisamos varias de las 15 respuestas. Varias devuelven 200 OK con éxito:
coupon["redeemed_by"] is None y escribir coupon["redeemed_by"] = usuario hay
una ventana de tiempo (aquí, deliberadamente ampliada con un sleep(1) que simula una
pasarela de pago) en la que múltiples hilos pueden pasar el mismo chequeo antes de que ninguno haya
confirmado el canje.
Cómo se protege cada fallo
| Reto | Fallo | Mitigación |
|---|---|---|
| 1 | Mass Assignment | Lista blanca explícita de campos editables en el servidor; nunca update(request.json) a pelo. |
| 2 | Confianza en el cliente | Todo endpoint que sirva un recurso protegido debe comprobar el permiso en el servidor, no asumir que el frontend ya lo hizo. |
| 3 | Exposición de datos | Serializar vistas públicas explícitas (DTOs), nunca el objeto de dominio completo; validar propiedad antes de aceptar un cupón. |
| 4 | Race Condition | Operación atómica (transacción, lock o UPDATE condicional a nivel de base de datos) para el ciclo comprobar-y-marcar. |
Herramientas utilizadas
| Herramienta | Uso |
|---|---|
| Proxy > Intercept | Interceptar y modificar peticiones al vuelo (retos 1 y 4) |
| Proxy > Match and replace | Reescribir respuestas del servidor antes de que lleguen al navegador (reto 2) |
| Proxy > HTTP history | Inspeccionar tráfico ya capturado sin necesidad de repetirlo (reto 3) |
| Repeater > Tab groups | Agrupar y duplicar peticiones idénticas |
| Repeater > Send group in parallel | Disparar un grupo de peticiones casi simultáneamente (reto 4) |