Exploiting clickjacking vulnerability to trigger DOM-based XSS
Cadena clickjacking + DOM XSS: el nombre de feedback se inyecta vía innerHTML, se pre-rellena desde la URL y ClickBandit orquesta el clic que dispara print().
Target Overview & Vulnerability Summary
El lab contiene un XSS disparado por un clic. La funcionalidad de envío de feedback refleja el campo name dentro de feedbackResult mediante innerHTML (sink inseguro). El ataque encadena: (1) pre-rellenar el formulario de feedback con un payload XSS vía parámetros URL, (2) enviarlo, y (3) usar clickjacking para que la víctima haga clic en el botón que dispara el submit — la combinación ejecuta print().
| Campo | Valor |
|---|---|
| Severidad | Alta (ejecución de JavaScript en el contexto de la víctima vía interacción engañosa) |
| Endpoint vulnerable | /feedback (campo name → sink innerHTML) |
| Root cause | Sink innerHTML con datos no sanitizados + página embebible (ausencia de anti-framing) |
Technical Breakdown & Exploitation Flow
1. El sink innerHTML
El formulario de feedback:
<form id="feedbackForm" action="/feedback/submit" method="POST" enctype="application/x-www-form-urlencoded" personal="true">
<input required type="hidden" name="csrf" value="YBZbpn20pVybpKdbSivPBuMawNzmw6IL">
<label>Name:</label>
<input required type="text" name="name">
<label>Email:</label>
<input required type="email" name="email">
<label>Subject:</label>
<input required type="text" name="subject">
<label>Message:</label>
<textarea required rows="12" cols="300" name="message"></textarea>
<button class="button" type="submit">Submit feedback</button>
<span id="feedbackResult"></span>
</form>
Y el script submitFeedback.js:
document.getElementById("feedbackForm").addEventListener("submit", function(e) {
submitFeedback(this.getAttribute("method"), this.getAttribute("action"), this.getAttribute("enctype"), this.getAttribute("personal"), new FormData(this));
e.preventDefault();
});
function submitFeedback(method, path, encoding, personal, data) {
var XHR = new XMLHttpRequest();
XHR.open(method, path);
if (personal) {
XHR.addEventListener("load", displayFeedbackMessage(data.get('name')));
} else {
XHR.addEventListener("load", displayFeedbackMessage());
}
if (encoding === "multipart/form-data") {
XHR.send(data)
} else {
var params = new URLSearchParams(data);
XHR.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
XHR.send(params.toString())
}
}
function displayFeedbackMessage(name) {
return function() {
var feedbackResult = document.getElementById("feedbackResult");
if (this.status === 200) {
feedbackResult.innerHTML = "Thank you for submitting feedback" + (name ? ", " + name : "") + "!";
feedbackForm.reset();
} else {
feedbackResult.innerHTML = "Failed to submit feedback: " + this.responseText
}
}
}
El valor del campo name se concatena en innerHTML sin validación, sanitización ni escapado:
feedbackResult.innerHTML = "Thank you for submitting feedback" + (name ? ", " + name : "") + "!";
2. Payload XSS
El payload se refleja y ejecuta tras el submit:
<img src=errorpls onerror=alert(document.domain)>
3. Prefill + clickjacking
El formulario se pre-rellena desde los parámetros de la URL (como en el lab de prefill de formulario):
/feedback?name=<img src=errorpls onerror=print()>&email=attacker@evil.com&subject=subject&message=message
Con ClickBandit se genera el HTML de clickjacking que reproduce el clic sobre el botón "Submit feedback": la víctima ve un señuelo, hace clic, el formulario pre-rellenado se envía, y la respuesta (con el name inyectado) se vuelca en innerHTML, ejecutando print() en el contexto de la página.
Cadena completa:
namecontiene<img src=errorpls onerror=print()>— pre-cargado por URL.- La víctima hace clic en el señuelo del iframe transparente.
- El submit envía el formulario; la respuesta del servidor rellena
feedbackResult.innerHTMLcon el payload. - El
<img>consrcinválido disparaonerror→print().
Root Cause Analysis
Se combinan tres fallos:
- Sink
innerHTMLsin saneamiento: el camponamedel feedback se refleja como HTML en el DOM del cliente. - Prefill desde URL: los campos del formulario se rellenan desde parámetros, permitiendo inyectar el payload sin interacción previa del atacante sobre la página.
- Página embebible: sin
X-Frame-Optionsni CSPframe-ancestors, el iframe transparente permite orquestar el clic que dispara el submit.
El CSRF token es irrelevante aquí: la request la genera el navegador de la víctima sobre el origen correcto.
Secure Remediation
- Sanitizar la salida del feedback: usar
textContentpara el mensaje o escapar el valor denameantes deinnerHTML:
feedbackResult.textContent = "Thank you for submitting feedback" + (name ? ", " + name : "") + "!";
- Bloquear el framing de la página:
X-Frame-Options: DENY
Content-Security-Policy: frame-ancestors 'none'
- No pre-rellenar campos de formulario desde parámetros URL en flujos que mutan estado, o validar el contenido contra una lista blanca.
- Añadir CSP
script-srcestricto para que handlers inline (onerror) no ejecuten incluso si un sink queda expuesto.