E-commerce ficticio que encadena dos fallos: una SPA en React que decide el rol del usuario leyendo `GET /api/auth/me` (CWE-602), y un middleware de cache propio que normaliza la query string (strip de `utm_*`, `ref`, `cb`, `_`, `t`), no varía sobre `Authorization`/`Cookie`, e ignora `Cache-Control: private, no-store` (CWE-524). Un bot interno de soporte (Puppeteer-core) navega URLs same-origin con cookies de admin, lo que permite sembrar el cache con la respuesta privilegiada y leerla anónimamente.
Entorno seguro y aislado
Crea una cuenta y suscríbete para acceder a todos los labs. Practica en un entorno real y seguro.
Cuando resuelvas este lab desbloqueas este logro compartible
Lumora Market es un mini e-commerce (Express + TypeScript + better-sqlite3 + React 18 + Vite + Puppeteer-core, puerto 2300) diseñado como playground para una cadena de dos vulnerabilidades reales:
Capa 1 — Confianza ciega del cliente en la respuesta del servidor (CWE-602). La SPA hace GET /api/auth/me al cargar y persiste el role devuelto. El navbar y el routing del panel /admin se renderizan solo a partir de ese campo. Una regla de Burp que reescriba "role":"user" → "role":"admin" en la respuesta hace aparecer la UI de admin con la lista de "Internal Reports", incluyendo la URL /internal/credentials-report.pdf. Al pedirla directamente, el middleware requireAdmin del backend devuelve 403 — esa puerta es honesta. El bypass solo sirve como recon: revela el nombre del recurso sensible.
Capa 2 — Web Cache Deception por normalización de la cache key (CWE-524). El middleware de cache propio (server/src/middleware/cache.ts) tiene tres defectos combinados: (a) reescribe la query string eliminando utm_*, ref, cb, _, t antes de calcular la key, (b) no incluye Authorization ni Cookie en la key (no hay vary por identidad), y (c) almacena toda respuesta 2xx durante 60 s ignorando Cache-Control: private, no-store que pone el origen. Resultado: dos URLs vistas como distintas por el origen (porque la cookie del bot autentica al admin) terminan en la misma bucket que la URL "limpia" anónima.
Gadget — el bot de soporte. El formulario público /support admite un campo screenshot_url. El proceso server/src/bot/supportBot.ts (Puppeteer-core) hace login como admin con credenciales aleatorias por boot, polling de tickets status=new, lanza Chromium, setea lm_token=<JWT admin> como cookie en localhost, y hace page.goto(screenshot_url, { waitUntil: 'domcontentloaded' }). Rechaza orígenes que no sean los del propio lab (localhost:2300, 127.0.0.1:2300, lumora:2300), por lo que no es un SSRF clásico — pero sí un primitivo perfecto para envenenar el cache con la respuesta privilegiada del propio sitio.
La cadena completa:
attacker@lumora.market / password123."role":"user" → "role":"admin"./admin con la URL /internal/credentials-report.pdf.curl -i 'http://localhost:2300/internal/credentials-report.pdf?cb=test' y ver que X-Cache-Key: /internal/credentials-report.pdf (sin la query)./support y enviar un ticket con screenshot_url: http://localhost:2300/internal/credentials-report.pdf?cb=poison.Cache-Control: private, no-store → el middleware lo guarda igual bajo la key normalizada.curl -i http://localhost:2300/internal/credentials-report.pdf -o leaked.pdf → X-Cache: HIT, 200 OK.pdftotext leaked.pdf - | grep -oE 'FLAG\{[a-f0-9]{32}\}' → flag.Por qué encaja en la familia "Web Cache Deception". El paper original de Omer Gil (2017) abusa de un CDN que cachea por extensión /account.php/foo.css mientras el origen ignora la parte cosmética y devuelve la página sensible. Aquí el twist es el mismo en otra dirección: dos URLs (...?cb=poison y ...) son distintas para el origen (autenticación + branching) pero iguales para el cache (la query se descarta antes de keyear). Una víctima privilegiada visita la "ruidosa" y la víctima anónima cosecha la "limpia".
Suscríbete para descargar