Orange Tsai 用纯逻辑漏洞构造 Microsoft Edge 完整攻击链
Forging a Microsoft Edge Full Chain with Logic Bugs Only
DEVCORE 的 Orange Tsai 在 Pwn2Own Berlin 2026 上用 5 个逻辑漏洞和 2 个 Windows 特性构造出 Microsoft Edge 完整攻击链,实现沙箱逃逸,获 17.5 万美元奖金和 17.5 Master of Pwn 积分。
我觉得我还是应该偶尔把一些工作成果发出来,尽管现在真正坐下来读文章的人似乎越来越少(也许现在大家都交给自己的 agent 了 XD)。总之,这是我为 Pwn2Own Berlin 2026 做的东西——一条完全手工打造、纯逻辑的 Microsoft Edge 全链,没有内存破坏,也完全没有用 AI!🔥
确认!DEVCORE 研究团队(@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 上分享了整个漏洞挖掘与串联过程,以及沿途的一些趣事。你可以查看这里的幻灯片!
另外,也许大家都觉得在 AI 时代 LLM 已经能攻破一切了,所以完全手工做出来的东西反而显得更珍贵一些。这可能也是这条链被提名今年 Pwnie Awards 并拿下 Epic Achievement 奖的原因。🎉

让我们开始导航!
那么,一条纯逻辑全链该从哪里开始呢?既然它是“浏览器”,那就先看看“浏览”(或者 Chromium 所说的“navigation”)是如何工作的。这篇文章里我不会讲得太细,因为我觉得在这个时代,你直接让你的 agent 去补细节就行了 :)
但如果你想了解全貌,我还是推荐读一读 Chrome University 的“Life of a Navigation”。
被遗忘的 Navigation Throttles
在梳理 Chromium 的 Navigation 流程时,一个叫“Navigation Throttles”的东西引起了我的注意。它们位于 Browser Process 中,会拦截由 Browser 或 Renderer 发起的所有导航请求。也许事后说起来容易,但 Google Chrome 过去五年修复的 3000 多个 CVE 中,与它们相关的只有不到 20 个。所以我猜这也是一个没多少人关注的角落。
总之,我开始深入研究 Navigation Throttles,并查看了 Google Chrome 和 Microsoft Edge 是如何实现它们的。在翻代码时,我发现 Edge 有一个名为 EdgeCAGuidanceNavigationThrottle 的 throttle,用于其企业 Entra ID 功能(这里的 CA 代表条件访问)。这个 throttle 的目的是:当 Edge 检测到用户在 Microsoft 网站上使用工作账户登录时,引导用户进入正确的用户配置文件。而要触发这个 throttle,你必须从某个特定的 Microsoft 拥有的源发起导航,例如 login.live.com:
1 | location.href = `https://guidance-ca-error/?email={email}&switch_profile={url}` |
而一旦该 throttle 被触发,Edge 会将当前用户配置文件的邮箱与 ?email={email} 进行比较。如果不匹配,Edge 会弹出一个对话框,引导你切换账户。

但如果匹配,Edge 就会从 switch_profile={url} 中取出你原本要前往的 URL,并使用 Chromium 内部的 Navigation API 发起一个新的导航:
1 | NavigateParams params(browser, switch_profile_url, ui::PAGE_TRANSITION_LINK); |
所以基本上,当目标 URL 匹配某个特定内部格式且条件满足时,这个 throttle 会取消原始导航,然后用攻击者可控的 Query String 中的 URL 发起一个新的导航。由于这个新导航是由 Browser Process 发起的,理论上我们可以做一些以前做不到的事,比如跳转到通常被禁止的页面,如 edge://settings 或 devtools://...,甚至像 javascript:alert(origin) 这样的伪协议。
虽然 JavaScript 伪协议听起来像是一个强大的原语,但你仍然需要先在 login.live.com 上获取执行上下文,才能触发这个节流。所以,能够跳转到 javascript: 实际上并没有给你带来任何额外的能力。最多,它就像一次 Self-XSS,或者是一种把那里的开放重定向变成 XSS 的方法。无论哪种方式,你仍然只是在同一个 login.live.com 上下文中执行 JavaScript。
老实说,我第一眼并没有把这个漏洞太当回事。对我来说,它只是一个更强的开放重定向,可以跳转到更多一些浏览器级协议,而且仍然需要与其他漏洞串联。所以这个漏洞就在我的笔记里放了好几周…… 🤦♂️
无源导航:一种新的 UXSS 原语
转折点是有一天我在梳理 Chromium 架构时开始思考:节流是在浏览器进程中处理的,所以新的导航也是从浏览器发起的。然而,我们触发节流的页面 login.live.com 位于渲染器中,而在调查逻辑时,我找不到任何将浏览器和渲染器联系起来的代码。那么…… CURRENT_TAB 到底指的是哪个标签页?
为了弄清楚这一点,我做了一个快速测试,并在原始触发器中加入了一个延迟:
1 | setTimeout(function(){ |
而在延迟期间,我们手动将 Edge 切换到一个新标签页,看看会发生什么,结果……
砰!我们在 google.com 上获得了一个 UXSS,这也是整个漏洞链的第一个漏洞。为了完全理解 UXSS 的根本原因,我们深入研究了 Chromium 的源代码。我们发现,如果在调用 Navigate() 时没有指定 source_contents(要绑定的渲染器源),Chromium 会自动使用当前活动标签页作为渲染器源。
↓ 路径: chromium/chrome/browser/ui/browser_navigator.cc
1 | base::WeakPtr<content::NavigationHandle> Navigate(NavigateParams* params) { |
尽管代码路径要求 params.disposition 为 CURRENT_TAB、params.source_contents 为 NULL,但幸运的是,这些都是默认值,所以就连 Chromium 自己的示例代码也搞错了这一点。我想这应该算是一个新的 UXSS 攻击面!
零点击挑战
我们刚刚获得的 UXSS 原语听起来很棒,但在实践中触发它仍然伴随着不少挑战。这里我们列出了在将 UXSS 武器化时面临的四个挑战,以及我们如何用越来越多的漏洞攻克它们 😉。
1. 节流必须从受信任的源触发
第一个挑战,正如我们之前提到的:这个 UXSS 必须从某些微软站点触发。虽然我们仍然可以在那里找到一个 XSS 来实现这一点,但这听起来是一项痛苦的工作,所以我们会把它留作最后的手段。相反,我们决定先看看源检查中是否有任何容易得手的目标。
1 | content::WebContents* web_contents = navigation_handle()->GetWebContents(); |
我们发现 EdgeCAGuidanceNavigationThrottle 使用 GetLastCommittedURL() 来检索触发导航的源,然后对其进行检查。乍一看,这似乎是进行验证的正确方式。但再看一眼,你会发现微软似乎忘记了导航不仅可以来自当前的 WebContents,还可以来自另一个窗口……所以一个简单的 window.open(...) 就足以绕过它!
↓ 你可以在任何网站上运行此代码来触发节流
1 | popup = window.open('https://login.live.com/', '_blank') |
到目前为止,我们可以通过跨窗口导航绕过源检查。然而,现代浏览器如今不会让网页那么容易地打开新窗口,所以这里又出现了一个新问题:弹窗拦截器默认会阻止 window.open(...)。

为了攻克这一点,我们继续逆向 Microsoft Edge。我们发现微软可能出于内部使用目的,为弹窗制作了自己的硬编码白名单……所以像下面这样的弹窗通常会被如预期地阻止:
1 | popup = window.open('https://malicious.tld', '_blank') |
但如果你把 URL 换成白名单中的那个……窗口就直接出现了。然后记得把它导航回原来的网站,你就绕过了弹窗拦截器!
1 | popup = window.open('about:blank#quickAuthPopup', '_blank') |
谢谢你,微软!

3. 需要已登录的配置文件
既然我们可以在任意位置触发 EdgeCAGuidanceNavigationThrottle,下一步就是绕过条件访问检查。代码首先要求 Microsoft Edge 已经有一个已登录的用户配置文件,然后检查 ?email={email} 是否与该配置文件的电子邮件匹配。这两个条件都必须满足,我们才能到达无源、由浏览器发起的导航,最终触发我们的 UXSS 原语。
1 | Profile* current_profile = Profile::FromBrowserContext( |
然而,由于我们不能假定目标环境事先已有已登录的配置文件,我们仍然需要找到一种方法,即使没有也能让攻击奏效。在探索 Microsoft Edge 时,我们遇到了一个很棒的功能,叫做“OneAuth”。该功能让任何 Edge WebView 应用在访问微软拥有的网站时,自动使用你当前的 MSA 凭据登录。而且事实证明,从 25H2 开始,Windows OOBE 要求你使用 MSA 登录(除非你使用本地账户的变通方法)。所以即使你从未在浏览器中创建过用户配置文件,仅仅访问 www.microsoft.com 就足以让 Edge 创建一个并自动为你登录。
再次感谢,微软!
4. 需要事先知道登录电子邮件
登录问题解决后,最后一个挑战是提前获取受害者的电子邮件。当然,我们可以尝试一些社会工程学,或者继续逆向 Microsoft Edge 以找到另一个为我们泄露它的漏洞。但由于自动登录已经设置了好几个微软 cookie,我们认为 Web 黑客攻击是窃取电子邮件更简单的方法。于是我们启动了 Burp Suite,开始寻找 Web 漏洞。
我们花了几个小时,在微软网站上发现了几个 XSS 漏洞。在探查过程中,我注意到嵌入在 edge://new-tab-page-online 中的 assets.msn.com 会返回当前已登录用户的电子邮件。更有趣的是,当我检查响应头时,该网站似乎存在 CORS 配置错误……

所以,只需一个包含凭据的简单 fetch(...),你就可以轻松地从任何网站获取任何 Microsoft Edge 用户的电子邮件:
1 | resp = await fetch(`https://assets.msn.com/service/community/users/me?profile=social&it=edgeid`, { |
把它们组合起来!
到目前为止,为了让这个原语生效,我们串联了:
- 一个由缺失跨窗口 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 开发者早已缓解了这种朴素的方法。在 Renderer 的 Mojo 处理器中,有一个额外的检查:如果源属于以下特权 WebUI 之一,Renderer 就会直接忽略 JavaScript URL。
1 | edge:// |
↓ 路径: chromium/third_party/blink/renderer/core/frame/local_frame.cc
1 | void LocalFrame::LoadJavaScriptURL(const KURL& url) { |
↓ 路径: chromium/third_party/blink/renderer/platform/weborigin/scheme_registry.cc
1 | bool SchemeRegistry::ShouldTreatURLSchemeAsNotAllowingJavascriptURLs( |
基本上,Chromium 已经把你所能想到的每一个特权 WebUI 都加入了 not_allowing_javascript_urls_schemes 黑名单。所以到那时,我又卡了很长一段时间。😖
通过沉浸式阅读器实现任意文件写入
然而,在我们继续探索 Microsoft Edge 时,我们发现 Edge 有自己的阅读模式,称为 沉浸式阅读器。与 Chrome 内置的阅读模式不同,沉浸式阅读器似乎很早就从 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 | chrome.edgeFeedbackPrivate.zipDiagnosticLogFiles( |
在试用过程中,我们观察到 Microsoft Edge 会先创建一个临时文件夹,并将 AAAAAAAA 写入文件 ORANGE.json。然后它会将整个文件夹打包成 LOL.ZIP,上传到 Microsoft 服务器,之后清理所有内容。当然,你会想试试 ../……是的,它确实能让你逃出临时文件夹。但那个 .json 扩展名还是有点烦人。也许你也在想,NULL 字节能不能去掉它……当然可以!所以现在我们有了一个任意文件写入,可以同时控制文件名和内容,并且可以覆盖任何位置的任何文件!
再次感谢 Microsoft!
覆盖 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 | { |
至于我们如何让 Microsoft Edge 重新初始化?只需使用我们之前由浏览器发起的导航跳转到 edge://restart 即可!:)
从 telnet:// 到代码执行!
这里我们选择了 telnet:// 来实现代码执行。在 Windows 上,telnet 处理程序会尝试调用 telnet.exe,但该二进制文件默认并未随系统提供,因此 Windows 会在 PATH 环境变量列出的目录中查找它。而其中一个目录恰好对我们可写……
1 | C:\WINDOWS\system32 |
因此,只需放入一个修改过的 protocols.json 和一个 32 位微型 PE,我们就获得了 ACE。整个漏洞利用不到 100 行代码。在真实攻击中重启其实并非必要,只是为了 Pwn2Own。让我们看看演示吧!🔥
最终思考
从对现代浏览器漏洞利用零经验到完成完整利用链,整个研究花了我整整两个月,全程没有使用任何 LLM/AI。但你可能会问,为什么?也许是因为听起来很酷,或者也只是时机正好——我今年三月左右才开始使用 Codex 和 Claude Code 这类编码代理,而那时我已经找到了所有漏洞并完成了困难的部分 ¯\_(ツ)_/¯
那么困难的部分是什么?对于内存破坏漏洞,通常困难的是利用。但我们玩的是逻辑漏洞,困难的部分是找到它们,而一旦你全部找到,串联就是最简单的部分。所以这也是我让整个漏洞利用保持无 AI 的原因。
除此之外,我还将 UXSS 原语视为一个新的攻击面,并同意在 AI 时代,“攻击面就是新的 0day”。我想在与 AI 巨头分享之前更多地探索这座金矿。这也是我决定在这条利用链上不使用 AI 的另一个原因。
当然,我也不是那种拒绝尝试新事物的老古董。在这条链之外,我也在 tokenmaxxing 并对 AI 上瘾。我相信一旦你尝到了 LLM 有多方便,就没人想回到手工挖洞了。我认为那也是未来。
我也不太害怕被 AI 取代。我更愿意说,正是因为 AI,我终于有足够的时间来完成 TODO 列表上所有搁置已久的酷东西……比如我刚报告的 Windows RCE,把利用链翻转为从 calc.exe 弹出 Microsoft Edge,或者将这条纯逻辑的完整利用链移植到 Google Chrome(快完成了 ><)。下个月我还会在日本的 CODE BLUE 上展示一些进展。敬请期待!😉
所以,与其担心漏洞研究的门槛能降到多低,为什么不兴奋于 AI 能把我们带得多高?我们能玩得多开心,能构建多酷的 hack,能让什么不可能的事情发生……?
永不停止利用!
来源:Hacker News · blog.orange.tw