Resolución del LAB – DOM Clobbering
Reconocimiento
La aplicación es una tienda de merchandising con dos páginas: el escaparate y un soporte.html para abrir tickets. El servidor se presenta como un SimpleHTTP de Python sirviendo ficheros en crudo, así que no hay backend que atacar: toda la lógica vive en el JavaScript de cliente. La home es puro cartón piedra; el interés está en el portal de soporte, que refleja en el DOM el asunto del ticket que escribimos.
El reto vive tras la cookie de sesión de la plataforma, así que la guardamos en cookies.txt y apuntamos la URL del reto a $URL. Con esas dos variables los comandos quedan limpios y copian directos, sin clavar un host que a cada quien le sale distinto.
curl -sk -b cookies.txt "$URL/soporte.html"El portal recoge el asunto del ticket, lo mete en el hash de la URL y lo pinta dentro de un <div id="ticketData">. Todo el que renderiza texto del usuario en el DOM es un candidato, pero el <script> de la página esconde algo más interesante que un XSS de manual.
Identificación de la vulnerabilidad
Leyendo el JavaScript de la página aparecen dos piezas que hay que mirar juntas. La primera: medio segundo después de renderizar el asunto, la página carga como script la URL que saque de una propiedad de configuración anidada, window.APP_CONFIG.security_plugin.href. Esa propiedad no está declarada en ningún sitio, así que en condiciones normales el flujo cae a un valor por defecto.
setTimeout(() => {
let scriptUrl = "js/default-security-check.js";
if (window.APP_CONFIG && window.APP_CONFIG.security_plugin) {
scriptUrl = window.APP_CONFIG.security_plugin.href || scriptUrl;
}
let script = document.createElement('script');
script.src = scriptUrl;
document.head.appendChild(script);
}, 500);La segunda pieza es el detonante. La página ha reemplazado window.alert por una versión que, al invocarse, hace un POST a flag_avanzada.txt y pinta el resultado en pantalla. Nadie llama a ese alert por su cuenta; hace falta un script que lo dispare.
window.alert = function (message) {
originalAlert(message);
fetch('flag_avanzada.txt', { method: 'POST' })
.then(r => r.text())
.then(flag => { /* pinta la flag en un div */ });
};Las dos piezas encajan solas: si logramos que window.APP_CONFIG.security_plugin.href apunte a un script que llame a alert(), ese script se carga, invoca el alert sobrescrito y el fetch nos trae la flag con nuestra propia sesión. El problema es que esa propiedad no existe. Aquí entra DOM Clobbering: el navegador crea referencias globales a partir de los atributos id y name del HTML, así que un elemento bien colocado puede fabricar una variable que el código lee sin haberla definido.
Y HTML podemos inyectar, porque el asunto del ticket entra en el DOM. Solo pasa antes por un sanitizador a base de expresiones regulares. Está pensado contra XSS clásico: elimina <script>, los manejadores on*, las etiquetas <iframe>, <object>, <embed>, <svg> y el esquema javascript:.
clean = clean.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');
clean = clean.replace(/on\w+="[^"]*"/gi, '');
clean = clean.replace(/<iframe.*?<\/iframe>/gi, '');
clean = clean.replace(/<svg.*?>/gi, '');
clean = clean.replace(/javascript:/gi, '');El punto ciego es que DOM Clobbering no ejecuta nada. No necesita <script> ni un on*: solo etiquetas inertes con id y name. Y la etiqueta reina del clobbering, <a>, no está en la lista negra, ni lo están sus atributos id, name y href. Bloquearon javascript: pero no data:, que sirve igual de bien para entregar el script.
Explotación
El objetivo es un objeto de tres niveles, window.APP_CONFIG.security_plugin.href, y se construye nivel a nivel. Primero, window.APP_CONFIG: si hay dos elementos con el mismo id, el navegador no elige uno, los agrupa en una HTMLCollection. Segundo, .security_plugin: una HTMLCollection expone sus miembros por name, así que basta con que uno de esos anclas lleve name="security_plugin". Tercero, .href: la propiedad href de un <a> devuelve la URL ya resuelta a forma absoluta, y ahí colgamos un data: URI con alert(1).
Todo eso son dos anclas:
<a id="APP_CONFIG"></a>
<a id="APP_CONFIG" name="security_plugin" href="data:text/javascript,alert(1)"></a>Dos id duplicados forman la colección, el name del segundo ancla la convierte en el miembro security_plugin y su href resuelve al script. Pasado por el sanitizador real del reto, el payload sale intacto: ninguna regla lo toca. El asunto viaja en el hash de la URL, así que el ataque cabe en un solo enlace, con el payload URL-encodeado tras #asunto=.
$URL/soporte.html#asunto=%3Ca%20id%3D%22APP_CONFIG%22%3E%3C%2Fa%3E%3Ca%20id%3D%22APP_CONFIG%22%20name%3D%22security_plugin%22%20href%3D%22data%3Atext%2Fjavascript%2Calert(1)%22%3E%3C%2Fa%3EAl abrir el enlace, la página inyecta las dos anclas en el DOM, el navegador registra window.APP_CONFIG como la colección y security_plugin como su miembro, y a los 500 ms el sink lee el href, carga el data: URI como script y ejecuta alert(1). El alert sobrescrito hace el POST y suelta la flag en un cartel a media pantalla.
Flag
El reto pone una última trampa para cerrar el atajo. Pedir flag_avanzada.txt por GET devuelve un 403 con un mensaje que lo dice todo: TRAMPA DETECTADA. Solo el POST entrega la flag, y ese POST es exactamente lo que hace el alert sobrescrito. No vale con adivinar la ruta del fichero y pedirlo: hay que llegar a que el JavaScript de la víctima invoque alert(), cosa que solo pasa si el clobbering ha funcionado.
curl -sk -b cookies.txt "$URL/flag_avanzada.txt" # 403 - TRAMPA DETECTADACon el clobbering en su sitio, la flag llega por la vía prevista y aparece en el cartel de éxito; la dejamos enmascarada como FLAG{XXXXXXXX} para no reventarle el final a nadie.
El sanitizador hacía bien su trabajo contra el ataque que esperaba y por eso mismo cae: vigilaba la ejecución de JavaScript, pero DOM Clobbering no ejecuta nada, coloca elementos muertos que el propio código de la página interpreta después. La frontera que se rompe no es la del filtro de entrada, es la confianza en que una variable global no declarada valga lo que el programador supuso; un <a> con id, name y href basta para escribirla por él.
Un portal que solo quería leer nuestro ticket terminó cargándonos a nosotros el plugin de seguridad.
