跳到正文
Hacker News (official RSS)(blog.orange.tw)·· 5 小時前精選AI 評分90

僅利用邏輯漏洞構造 Microsoft Edge 完整攻擊鏈

Forging a Microsoft Edge Full Chain with Logic Bugs Only

AI 導讀

本文描述了研究者在 Pwn2Own Berlin 2026 中,利用 Microsoft Edge 內部邏輯缺陷,從零記憶體破壞到完全自主的 ACE,完成了一條 5 個漏洞 + 2 Windows 功能的全流程攻擊鏈。

推薦理由

本研究揭示邏輯漏洞可組成全流程攻擊,展示了 UXSS 與多步驟連結的可行性,為安全研究者提供新的攻擊面與防禦思路,同時說明瞭無記憶體破壞的攻擊設計可行性。

正文 · AI 翻譯

我覺得我還是應該時不時釋出一些作品,雖然現在似乎越來越少人真正坐下來閱讀文章(或者現在他們全都交給代理人 XD)。不管怎樣,這是我為 Pwn2Own Berlin 2026 構建的作品——完全手工製作、僅使用邏輯的 Microsoft Edge Full Chain,沒有記憶體破壞,也完全不使用 AI!🔥

確認!DEVCORE Research Team (@d3vc0r3) 的 Orange Tsai (@orange_8361) 連鎖 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 分享了整個漏洞挖掘與連結過程,還有一路上的趣事。你可以檢視 這裡的簡報!

此外,也許大家認為 LLM 已經能在 AI 時代中攻破一切,於是完全手工製作的東西反而顯得更珍貴。這也許就是為什麼這條鏈被提名今年的 Pwnie Awards 並獲得 Epic Achievement 獎項的原因。🎉

pwnie-award-2026

讓我們導航!

那麼,我們從什麼地方開始一個純邏輯的完整鏈呢?既然它是一個「瀏覽器」,那就先看看「瀏覽」(Chromium 所稱的「導航」)是如何運作的。我不會在這篇文章中深入細節,因為我覺得在這個時代,你可以直接請你的代理人填寫細節 :)

不過如果你想了解完整情況,我仍建議閱讀 Chrome University 的「Life of a Navigation」。

被遺忘的導航 Throttles

在我瀏覽 Chromium 的導航流程時,某個叫「Navigation Throttles」的東西吸引了我的注意。它們存在於 Browser Process,並攔截所有由 Browser 或 Renderer 發起的導航請求。回想起來說起來可能很簡單,但在 Google Chrome 過去五年修復的 3,000+ CVE 中,與它們相關的只有不到 20 個。因此我猜這也是一個不被太多人關注的角落。

不管怎樣,我開始深入研究導航節流器,並檢查 Google Chrome 與 Microsoft Edge 如何實作。當我瀏覽程式碼時,發現 Edge 為其企業 Entra ID 功能設定了一個名為 EdgeCAGuidanceNavigationThrottle 的節流器(CA 代表 Conditional Access)。此節流器的目的在於當 Edge 檢測到使用者以工作帳號登入 Microsoft 網站時,引導其切換到正確的使用者檔案。為了觸發此節流器,你必須從特定 Microsoft 擁有的來源啟動導航,例如 login.live.com:

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

當節流器被觸發後,Edge 會將目前使用者檔案的電子郵件與 ?email={email} 進行比較。若不相符,Edge 會彈出對話方塊,指示你切換帳號。

image

若相符,Edge 會從 switch_profile={url} 取得你原本想前往的網址,並使用 Chromium 內部的導航 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 架構並開始思考:節流是在瀏覽器程序中處理的,因此新導航也從瀏覽器啟動。然而,我們從中觸發節流的頁面 login.live.com 位於 Renderer,且在調查邏輯時,我找不到任何把瀏覽器和 Renderer 連結的程式碼。因此… CURRENT_TAB 指的是哪個分頁,確切來說?

為了找出這點,我快速測試並在原始觸發器加了一個延遲:

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

在延遲期間,我們手動將 Edge 切換到新分頁,看看會發生什麼,結果…

Boom! 我們在 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 攻擊面!

0 點選挑戰

我們剛得到的 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 換成白名單中的那個… 視窗就會出現。接著記得把它導回原本的網站,你就已經繞過了彈窗阻擋器!

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」的絕佳功能。這項功能讓任何 Edge WebView 應用程式在瀏覽 Microsoft 擁有的網站時,會自動以你目前的 MSA 憑證登入。且從 25H2 開始,Windows OOBE 需要你以 MSA 登入(除非使用本機帳號迴避)。因此,即使你從未在瀏覽器中建立過使用者設定檔,僅僅訪問 www.microsoft.com 就足以讓 Edge 建立設定檔並自動登入。

再次謝謝你,Microsoft!

4. 需要先知曉登入者的電子郵件

在登入問題破解後,最後的挑戰是事先取得受害者的電子郵件。當然,我們可以嘗試社交工程,或繼續反向工程 Microsoft Edge 以尋找另一個洩露此資訊的漏洞。但因為自動登入已經設定了多個 Microsoft Cookie,我們認為 Web 攻擊會是更簡單的竊取電子郵件方式。因此我們啟動 Burp Suite,開始尋找網頁漏洞。

我們花了幾個小時,發現多個 Microsoft 網站的 XSS 漏洞。在四處探查時,我注意到 assets.msn.com,它嵌入於 edge://new-tab-page-online,會回傳目前已登入使用者的電子郵件。更有趣的是,當我檢查回應標頭時,發現該網站似乎存在 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 檢查而造成的 Trusted Origin 迴避
  • 由硬編碼白名單造成的彈窗阻擋器迴避
  • 自動登入的 OneAuth 功能
  • 允許我們竊取已登入使用者電子郵件的 CORS 設定錯誤

現在我們終於能夠在不點選任何按鈕的情況下觸發 UXSS!更棒的是,因為這個 UXSS 是由瀏覽器程序啟動的(HandleRendererDebugURL 在 Mojo 中,如果你有興趣),它可以完全忽略任何 CSP。這意味著我們可以在 google.com、facebook.com、icloud.com 或任何想要的網站上執行 JavaScript!

突破瀏覽器沙盒

我們已經擁有一個通用 XSS。這是否意味著我們可以隨心所欲?若談及網頁安全,答案是肯定。但我們的目標是取得 ACE(任意程式碼執行),因此單靠 UXSS 還遠遠不夠。我們仍需...

我腦中最先浮現的最直接想法是:既然我們可以在任何網站,或更精確地說任何來源上執行 JavaScript,為何不直接利用具有特權的 WebUI 頁面,例如 edge://settings 或 edge://downloads,來取得 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:// 協議,而不是舊的 chrome-distiller:// 或被列入黑名單的 chrome-untrusted://。Microsoft 似乎也忘了把這個協議加入黑名單,於是我們的 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 Byte 能否把它除掉… 當然可以!所以現在我們擁有一個任意檔案寫入,可控制檔名與內容,並能覆寫任何位置的檔案!

再次再次感謝 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 等編碼代理,等到那時,我已經找出所有漏洞並完成最困難的部分 ¯\\_(ツ)_/¯

那麼最難的部分是什麼?對於記憶體腐敗來說,往往是利用本身。但我們在處理邏輯缺陷,難點在於發現它們,一旦全部找到,連結起來就最簡單了。因此我也保持整個漏洞利用完全不使用 AI。

除此之外,我也將 UXSS 原語視為新的攻擊面,並同意在 AI 時代,「攻擊面即新 0day」這句話。想在與 AI 巨頭分享前更深入探索這個黃金礦床,這也是我決定不在此鏈中使用 AI 的另一原因。

當然,我也不是那種拒絕嘗試新事物的老派人物。在這條鏈之外,我也在 tokenmaxxing 並沉迷 AI。我相信一旦體驗到 LLM 的便利,沒有人會想回到手動尋漏洞的方式。這也是未來,我想。

我也不會因被 AI 取代而感到恐懼。倒不如說正是因為 AI,我終於有足夠時間完成所有停留在待辦清單上的酷炫專案… 比如 剛報告的 Windows RCE、將鏈反向以 從 calc.exe 彈出 Microsoft Edge,或將此僅邏輯的完整鏈移植至 Google Chrome(已接近完成 ><)。我也會在下個月於 CODE BLUE in Japan 展示一些進展。敬請期待!😉

那麼,與其擔心漏洞研究門檻能降到多低,不如興奮於 AI 能把我們帶到多高?我們能玩什麼樂趣,能打造什麼更酷的駭客技術,能實現什麼不可能的事…?

永不停止利用!

來源: Hacker News (official RSS) (blog.orange.tw)
開啟原文