GhostLabs

GhostFlix

ejercicios de burp suite :: lógica de negocio
ghostflix-lab — write-up
Laboratorio
GhostFlix
Dificultad
★★☆☆☆ fácil-media
Categoría
Business Logic / API Security
Objetivo
http://127.0.0.1:5000
Herramientas
Burp Proxy · Repeater · Match & Replace
Retos
4
⚠ Este documento contiene la solución completa de los 4 retos — resuélvelos primero por tu cuenta

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.

▶ Reto 1 → Mass Assignment en POST /api/account/update
▶ Reto 2 → Match & Replace sobre GET /api/session
▶ Reto 3 → API que expone de más en GET /api/reviews/<id>
▶ Reto 4 → Race Condition en 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

POST /api/account/update · Proxy > Intercept

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.

1
Estado inicial de la cuenta

Cuenta recién creada, plan free, sin meses de Premium.

Cuenta antes del ataque
/account — plan FREE, 0 meses premium
2
Activar Intercept en Burp

En Proxy > Intercept, el botón empieza en Intercept is off.

Intercept off
Proxy > Intercept — estado inicial (off)

Lo activamos para poder editar la petición antes de que llegue al servidor.

Intercept on
Intercept is on
3
Editar el nombre visible y forzar la petición

En la app, en Editar perfil, cambiamos el nombre visible y pulsamos Guardar cambios. Burp retiene la petición POST /api/account/update:

Petición original interceptada
Petición original: solo display_name y avatar en el body
4
Añadir campos que no deberíamos poder tocar

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.

Petición modificada con plan premium
Petición modificada: plan y premium_months añadidos a mano
No hace falta tocar el header Content-Length: Burp lo recalcula solo al reenviar desde el editor de Intercept.
5
Forward y comprobación

Reenviamos la petición, desactivamos Intercept y recargamos /account.

Resultado
Plan: PREMIUM · Meses acumulados: 12
Cuenta ahora premium
/account tras el ataque — badge PREMIUM sin haber pagado nada
Causa raíz: el backend hace algo equivalente a 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

GET /api/session · GET /api/stream/<id> · Proxy > Match and replace

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.

1
Catálogo con cuenta Free

Los títulos Premium muestran el candado y el botón «Reproducir» está deshabilitado.

Catálogo bloqueado
/catalog con plan free — contenido premium bloqueado
2
Crear la regla de Match & Replace

En Proxy > Match and replace, añadimos una regla nueva sobre el Response body: sustituye "plan":"free" por "plan":"premium".

Configuración de la regla
Match: "plan":"free" → Replace: "plan":"premium"
Regla añadida y activa
Regla activa en la lista de Match and replace
3
Recargar el catálogo

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:

Resultado
Catálogo desbloqueado sin haber cambiado el plan real
Catálogo desbloqueado
Mismo catálogo, misma cuenta free — ahora todo aparece reproducible
4
Reproducir contenido premium

Pulsamos «Reproducir» en un título Premium. La petición llega a /api/stream/<id>, que nunca comprobó el plan — responde con éxito igualmente.

Reproduciendo contenido premium
"Reproduciendo «Red Team Chronicles» en calidad 4K" con una cuenta free
Fíjate en que en ningún momento tocamos una petición de subida (request): solo interceptamos y reescribimos la respuesta del servidor. Esto demuestra que el problema no es el navegador — es que el servidor delega en el cliente una decisión que debería tomar él mismo.
Causa raíz: /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

GET /api/reviews/<title_id> · Proxy > HTTP history

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.

1
Consultar las reseñas desde la UI

En el catálogo, con cuenta free, pulsamos «Ver reseñas» en un título con comentarios de otro usuario (streamia_fan99).

Reseñas visibles en la UI
La UI solo muestra estrellas, comentario y nombre — nada sospechoso a simple vista
2
Inspeccionar la respuesta real en Burp

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.

JSON con datos filtrados
El campo "author" incluye plan y assigned_coupon de streamia_fan99 — nada de esto se pinta en pantalla
Este es el hábito más importante de todo el manual: mirar siempre la respuesta cruda en Burp, no solo lo que la interfaz decide mostrarte. Las APIs frecuentemente devuelven más de lo que la UI usa.
3
Canjear el cupón ajeno en nuestra cuenta

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».

Resultado
Plan: PREMIUM · +1 mes, usando el cupón de otra cuenta
Cupón ajeno canjeado con éxito
Cuenta escalada a premium con un cupón que nunca fue nuestro
Causa raíz doble: (1) el endpoint de reseñas serializa el objeto de usuario completo en vez de una vista pública explícita; (2) el canje de cupones no verifica que el código pertenezca a quien lo usa. Cualquiera de los dos fallos por separado ya sería un hallazgo reportable.

4Race Condition

POST /api/coupon/redeem · Repeater > Send group in parallel

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.

1
Capturar una petición de canje limpia

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.

Petición lista en Repeater
Repeater con el body {"code":"WELCOME-ALUMNO01"} listo para disparar
2
Agrupar y duplicar la pestaña

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).

Grupo Race Condition creado
Grupo "Race Condition" con la primera pestaña dentro
15 pestañas idénticas en el grupo
15 pestañas idénticas, todas con el mismo cupón de un solo uso
3
Enviar el grupo en paralelo

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.

Menú de envío en paralelo
Send group in parallel (last-byte sync) — disponible en Burp CE, sin necesitar Turbo Intruder
4
Comprobar cuántas pasaron

Revisamos varias de las 15 respuestas. Varias devuelven 200 OK con éxito:

Primera respuesta exitosa
Pestaña 3: 200 OK, premium_months salta a 4 — ya van varias coladas
Otra respuesta exitosa
Otra pestaña del mismo grupo: también 200 OK, premium_months ahora en 5
5
Resultado final
Resultado
15 de 15 peticiones aceptadas — 15 meses de Premium desde un cupón de un solo uso
Cuenta con 15 meses premium
/account — "Meses premium acumulados: 15" tras la ráfaga
Causa raíz: el patrón check-then-act no es atómico. Entre comprobar 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

RetoFalloMitigació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

HerramientaUso
Proxy > InterceptInterceptar y modificar peticiones al vuelo (retos 1 y 4)
Proxy > Match and replaceReescribir respuestas del servidor antes de que lleguen al navegador (reto 2)
Proxy > HTTP historyInspeccionar tráfico ya capturado sin necesidad de repetirlo (reto 3)
Repeater > Tab groupsAgrupar y duplicar peticiones idénticas
Repeater > Send group in parallelDisparar un grupo de peticiones casi simultáneamente (reto 4)