ロジックバグのみでMicrosoft Edgeのフルチェーンを構築する
Forging a Microsoft Edge Full Chain with Logic Bugs Only (blog.orange.tw)
要約
本記事は、Orange Tsai氏がPwn2Own Berlin 2026で発表した、メモリ破壊やAIを使用しない、完全に手作業で構築されたMicrosoft Edgeのロジックバグのみによるフルチェーンエクスプロイトについて解説しています。このエクスプロイトは、複数のロジックバグを連鎖させることで、ブラウザのサンドボックスからの脱出と最終的なコード実行を可能にします。
全文翻訳
人間の手による最後の証明:ロジックバグのみでMicrosoft Edgeのフルチェーンを構築!
最近は記事をじっくり読む人が減っているように感じますが、それでもたまには自分の仕事を公開すべきだと思います(あるいは、今では皆エージェントに任せているのでしょうか XD)。いずれにせよ、これは私がPwn2Own Berlin 2026のために構築したものです。メモリ破壊やAIを一切使用しない、完全に手作業によるMicrosoft Edgeのフルチェーンです!🔥
確認済み!DEVCORE Research TeamのOrange Tsai(@orange_8361)は、4つのロジックバグを連鎖させてMicrosoft Edgeでサンドボックスエスケープを達成し、17万5千ドルと17.5 Master of Pwnポイントを獲得しました。完全勝利です!#Pwn2Own #P2OBerlin https://t.co/vJaIpGG4cD
— TrendAI Zero Day Initiative (@thezdi) 2026年5月14日
このチェーン全体は、5つのバグと2つのWindows機能で構成されています。バグハンティングとチェーン構築の全プロセスは、今年のHITCONとHexaconで発表し、道中の面白い話もいくつかしました。スライドはこちらで確認できます:こちら!
また、AI時代にはLLMがすべてをハックできるようになると皆が思っているかもしれませんが、完全に手作業で作られたものは、ある意味でより貴重に感じられるかもしれません。このチェーンが今年のPwnie Awardsにノミネートされ、Epic Achievement賞を受賞したのも、それが理由かもしれません。🎉
[画像: pwnie-award-2026]
ナビゲーションを進めよう!
では、ロジックバグのみのフルチェーンでどこから始めればよいでしょうか?「ブラウザ」なので、「ブラウジング」(あるいはChromiumが「ナビゲーション」と呼ぶもの)の仕組みから見ていきましょう。この投稿では詳細には触れません。なぜなら、この時代では、詳細を埋めるようにエージェントに頼むことができるからです :)
しかし、全体像を把握したい場合は、Chrome Universityの「Life of a Navigation」を読むことをお勧めします。
忘れられたナビゲーションスロットル
Chromiumのナビゲーションフローを調べていると、「Navigation Throttles」(ナビゲーションスロットル)というものに目が留まりました。これらはブラウザプロセスに存在し、ブラウザまたはレンダラーによって開始されたすべてのナビゲーションリクエストをインターセプトします。後から考えると簡単なことですが、過去5年間でGoogle Chromeが修正した3,000件以上のCVEのうち、これらに関連するものは20件未満です。ですから、これもあまり多くの人が注目していないコーナーだと思います。
anyway、ナビゲーションスロットルの調査を開始し、Google ChromeとMicrosoft Edgeがそれらをどのように実装しているかを確認しました。コードを調べていくと、EdgeにはエンタープライズEntra ID機能(CAはConditional Accessの略)のためのEdgeCAGuidanceNavigationThrottleというスロットルがあることがわかりました。このスロットルの目的は、EdgeがMicrosoftウェブサイトでワークアカウントでのサインインを検出した際に、ユーザーを適切なユーザープロファイルに誘導することです。このスロットルを発火させるには、login.live.comのような特定のMicrosoft所有のオリジンからナビゲーションを開始する必要があります。
1
location.href = `https://guidance-ca-error/?email={email}&switch_profile={url}`
スロットルが発火すると、Edgeは現在のユーザープロファイルと?email={email}を比較します。一致しない場合、Edgeはアカウントを切り替えるよう誘導するダイアログを表示します。
[画像: image]
しかし、一致した場合、Edgeは元のURLをswitch_profile={url}から取得し、Chromiumの内部ナビゲーションAPIを使用して新しいナビゲーションを開始します。
12345
NavigateParams params(browser, switch_profile_url, ui::PAGE_TRANSITION_LINK);params.disposition = WindowOpenDisposition::CURRENT_TAB;params.window_action = NavigateParams::SHOW_WINDOW;Navigate(¶ms);
基本的に、スロットルはターゲットURLが特定の内部フォーマットと一致した場合に元のナビゲーションをキャンセルし、条件が満たされれば攻撃者が制御するクエリ文字列からのURLで新しいナビゲーションを開始します。この新しいナビゲーションはブラウザプロセスによって開始されるため、理論的には、通常は禁止されているedge://settingsやdevtools://...のようなページ、あるいはjavascript:alert(origin)のような疑似スキームにジャンプするなど、以前はできなかったことがいくつか可能になります。
JavaScriptの疑似スキームは強力なプリミティブのように聞こえますが、スロットルを発火させるためには、まずlogin.live.comで実行コンテキストを取得する必要があります。そのため、javascript:にジャンプできても、実際には追加の機能は得られません。せいぜい、Self-XSSのようなものか、そこでのOpen RedirectをXSSに変換する方法です。いずれにしても、実行されるJavaScriptは同じlogin.live.comコンテキスト内です。
正直なところ、このバグを最初見たときはそれほど真剣に受け止めませんでした。私にとっては、単に強力なOpen Redirectで、いくつかのブラウザレベルのスキームにジャンプできるものの、他のバグとチェーン化する必要があるというだけでした。そのため、このバグは数週間私のノートに眠っていました…🤦♂️
ソースレスナビゲーション:新しいUXSSプリミティブ
ある日、Chromiumのアーキテクチャを見直していると、次のようなことを考え始めました。スロットルはブラウザプロセスで処理されるため、新しいナビゲーションもブラウザから開始されます。しかし、スロットルを発火させたページ、login.live.comはレンダラーに存在し、ロジックを調査している間、ブラウザとレンダラーを結びつけるコードを見つけることができませんでした。では… CURRENT_TABは具体的にどのタブを指すのでしょうか?
これを調べるために、簡単なテストを実行し、元のトリガーに遅延を追加しました。
123
setTimeout(function(){ location.href = `https://guidance-ca-error/?email={email}&switch_profile={url}`}, 8000)
遅延中に、Edgeを新しいタブに手動で切り替えて何が起こるか見てみました。すると…
ブーム!google.comでUXSSが発生しました。これが、チェーン全体の最初のバグです。UXSSの根本原因を完全に理解するために、Chromiumのソースコードを掘り下げました。source_contents(バインドするレンダラーソース)を指定せずにNavigate()を呼び出した場合、Chromiumは自動的に現在のアクティブタブをレンダラーソースとして使用することがわかりました。
↓ パス: chromium/chrome/browser/ui/browser_navigator.cc
12345678910111213141516171819202122232425262728293031323334353637383940414243444546
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を悪用する際に直面した4つの課題と、それらをさらに多くのバグでどのように解決したかを説明します 😉。
1. スロットルは信頼できるオリジンから発火させる必要がある
最初の課題は、前述したように、このUXSSは特定のMicrosoftサイトから発火させる必要があることです。そこでXSSを見つけてそれを実行することも可能ですが、それは骨の折れる作業のように思えたので、最後の手段として残しておくことにしました。代わりに、オリジンチェックに何か簡単な抜け穴がないか調べることにしました。
12345
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(...)で回避できます!
↓ 任意のウェブサイトでこれを実行してスロットルを発火させることができます
12
popup = window.open('https://login.live.com/', '_blank')
popup.location = `https://guidance-ca-error/?email={email}&switch_profile={url}`
これまでのところ、クロスウィンドウナビゲーションでオリジンチェックをバイパスできます。しかし、現代のブラウザでは、ウェブページが簡単に新しいウィンドウを開くことはできないため、ここに新たな問題が発生します。Popup Blockerはデフォルトでwindow.open(...)をブロックします。
[画像: popup-blocker]
これを解決するために、Microsoft Edgeのリバースエンジニアリングを続けました。そして、Microsoftが、おそらく内部使用のために、ポップアップ用の独自のハードコードされたホワイトリストを作成していることがわかりました…したがって、次のようなポップアップは通常通りブロックされます。
1
popup = window.open('https://malicious.tld', '_blank')
しかし、URLをホワイトリストに登録されたものに切り替えると…ウィンドウが表示されます。そして、それを元のサイトに戻すことを忘れないでください。これでポップアップブロッカーをバイパスできます!
12
popup = window.open('about:blank#quickAuthPopup', '_blank')
popup.location = 'https://malicious.tld'
ありがとう、Microsoft!
[画像: thanks-microsoft]
3. サインイン済みプロファイルが必要
これでEdgeCAGuidanceNavigationThrottleをどこからでも発火させることができるようになりました。次のステップは、Conditional Accessチェックを通過することです。コードはまず、Microsoft Edgeにサインイン済みのユーザープロファイルが既に存在することを要求し、次に?email={email}がプロファイルのメールと一致するかどうかをチェックします。これらの両方の条件が満たされて初めて、ソースレスのブラウザ起動ナビゲーションに到達し、UXSSプリミティブを最終的にトリガーできます。
12345678910
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を設定しているため、メールを盗むにはWebハッキングの方が簡単な方法だと考えました。そこでBurp Suiteを起動し、Web脆弱性を探し始めました。
数時間かけてMicrosoftサイトでいくつかのXSSバグを見つけました。調査中に、edge://new-tab-page-onlineに埋め込まれているassets.msn.comが、現在サインインしているユーザーのメールアドレスを返していることに気づきました。さらに興味深いことに、レスポンスヘッダーを確認したところ、そのサイトにはCORSの設定ミスがあるようでした…
[画像: cors-misconfig]
したがって、資格情報を含めた単純なfetch(...)で、任意のMicrosoft Edgeユーザーのメールアドレスを任意のウェブサイトから簡単に取得できます。
1234567891011
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 )
全てをまとめる!
これまでのところ、このプリミティブを機能させるために、私たちは以下を連鎖させました。
- クロスウィンドウオープナーチェックの欠如による信頼できるオリジンバイパス
- ハードコードされたホワイトリストによるポップアップブロッカーバイパス
- 自動サインインするOneAuth機能
- サインイン済みユーザーのメールを盗むことができるCORS設定ミス
これで、クリックなしでUXSSをトリガーできるようになりました!さらに良いことに、このUXSSはブラウザプロセスから開始されるため(興味があればMojoのHandleRendererDebugURL)、CSPを完全に無視できます。これは、google.com、facebook.com、icloud.com、または任意のウェブサイトでJavaScriptを実行できることを意味します!
ブラウザサンドボックスからの脱出
ユニバーサルXSSが得られたら、何でもできるようになるのでしょうか?ウェブセキュリティの話であれば、そうです。しかし、私たちの目標はACE(Arbitrary Code Execution)を得ることなので、UXSSだけでは全く不十分です。UXSSをコード実行に変換するために、ブラウザサンドボックスからの脱出方法を見つける必要があります。
最初に思いついた最も直接的なアイデアは、任意のウェブサイト、より正確には任意のオリジンでJavaScriptを実行できるので、なぜedge://settingsやedge://downloadsのような特権WebUIページを利用してACEを取得しないのか、ということでした。
このナイーブなアプローチは、もちろんChromium開発者によって既に緩和されています。レンダラーのMojoハンドラには追加のチェックがあります。オリジンが以下の特権WebUIのいずれかに属する場合、レンダラーはJavaScript URLを単純に無視します。
1234567891011
edge://chrome-extension://chrome://chrome-untrusted://chrome-distiller://chrome-error://chrome-native://chrome-search://devtools://// その他...
↓ パス: chromium/third_party/blink/renderer/core/frame/local_frame.cc
123456789101112
void LocalFrame::LoadJavaScriptURL(const KURL& url) {
if (SchemeRegistry::ShouldTreatURLSchemeAsNotAllowingJavascriptURLs(
GetSecurityContext()
->GetSecurityOrigin()
->GetOriginOrPrecursorOriginIfOpaque()
->Protocol())) {
return;
}
}
↓ パス: chromium/third_party/blink/renderer/platform/weborigin/scheme_registry.cc
12345678
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がその上に構築しています。そのため、古いchrome-distiller://やブラックリストに載っているchrome-untrusted://の代わりに、Edgeはカスタムのread://スキームを使用しています。そして、Microsoftはこのスキームをブラックリストに追加し忘れたようです。そのため、この特権WebUIで私たちのUXSSが機能します :)
この特権WebUIから、JavaScriptを介して多くのネイティブC++オブジェクトにアクセスできます。そして、chrome.edgeFeedbackPrivate.zipDiagnosticLogFiles()と呼ばれるオブジェクトの1つが特に興味深く思えました。それを試したところ、この関数はファイル名とJSON文字列の2つのパラメータを取ることがわかりました。
↓ read://https_orange.tw/?url=https://orange.twでこれを実行しました
123456789
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は初期化中に多くのコンポーネントをロードし、そのうちの1つが「AutoLaunchProtocolsComponent」であることがわかりました。このコンポーネントは、書き込み可能なフォルダからprotocols.jsonという設定ファイルを読み込み、そのファイルはどのオリジンがどのURLプロトコルハンドラを起動できるかを定義します。したがって、ファイルを上書きすることで、任意のオリジンから任意のプロトコルハンドラを起動できます。
↓ パス: AppData\Local\Microsoft\Edge\User Data\AutoLaunchProtocolsComponent\1.0.0.10\protocols.json
1234567891011121314151617181920212223242526
{
"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つが偶然、書き込み可能な場所でした…
123456
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のためだけです。デモを見てみましょう!🔥
最終的な考察
モダンブラウザのエクスプロイトに関するゼロの経験からフルチェーンを完成させるまで、全研究には2ヶ月かかりました。LLM/AIは一切関与していません。しかし、なぜか、とあなたは尋ねるかもしれませんか?おそらく、クールに聞こえるからかもしれませんし、あるいはタイミングも良かったのかもしれません。私はCodexやClaude Codeのようなコーディングエージェントを使い始めたのが今年の3月頃でしたが、その時点ですでにすべてのバグを見つけており、大変な部分は終わっていました¯_(ツ)_/¯
大変な部分とは何でしょうか?メモリ破壊の場合、それはしばしばエクスプロイトです。しかし、私たちはロジックバグを扱っており、大変なのはそれらを見つけることであり、すべて見つけてしまえば、チェーン化は最も簡単な部分です。だからこそ、このチェーン全体をAIフリーに保ったのです。
それとは別に、UXSSプリミティブを新しい攻撃サーフェスと見なしており、AI時代において「Attack Surface is the new 0day」であることに同意します。AIジャイアントと共有する前に、この金の鉱脈をもっと探求したかったのです。それが、このチェーンにAIを使用しないことを決定したもう一つの理由です。
もちろん、私は新しいことを試すことを拒否するような時代遅れの人間ではありません。このチェーン以外にも、私はトークンマキシングをしており、AIに夢中です。LLMがいかに便利かを一度味わえば、手作業でバグを探すのに戻りたいと思う人はいないでしょう。それも未来だと思います。
AIに取って代わられることをそれほど恐れてもいません。むしろ、AIのおかげで、ToDoリストにあったクールなものをすべて完成させる十分な時間があると言えます…例えば、私が報告したばかりのWindows RCEや、チェーンを反転させてcalc.exeからMicrosoft Edgeをポップアップさせること、あるいはこのロジックのみのフルチェーンをGoogle Chromeに移植すること(ほぼ完了><)などです。来月、CODE BLUE in Japanでも進捗の一部を発表します。お楽しみに!😉
脆弱性研究の敷居がどれだけ低くなるかを心配する代わりに、AIが私たちをどれだけ高く連れて行ってくれるかに興奮しませんか?どんな楽しみが得られるか、どんなさらにクールなハックを構築できるか、そしてどんな不可能なことを実現できるか…?
エクスプロイトをやめるな!