Converting Tokens to Session Cookies for Outlook Web Application

More and more organizations are adopting cloud-based solutions and federating with various identity providers. As these deployments increase in complexity, ensuring that Conditional Access Policies (CAPs) always act as expected can become a challenge.

Today, we will share a technique we've been using to gain access to Outlook Web Application (OWA) in a browser by utilizing Bearer and Refresh tokens for the outlook.office365.com or outlook.office.com endpoints.

Picture the following scenario:

You're on an engagement and have successfully compromised a user's credentials. Attempts to authenticate to M365 via a web browser are met with a pesky MFA prompt. You run your MFA Sweeper utility of choice and find endpoints you can authenticate with Single-Factor Authentication. You've obtained a Bearer and Refresh token for outlook.office365.com and want to access the user's mailbox through your web browser to look for items that can help you advance toward your objectives.

We see this very scenario play out more often than you may think, so we developed the technique in this post to maximize our access level, given our authentication material.

Inspiration

The research within this post is inspired by the Open-MailboxInBrowser function of TokenTactics by Steve Borosh (@424f424f) and Bobby Cooke (@0xBoku). The Open-MailboxInBrowser function allowed web browser access to OWA by adding a Bearer header in requests to substrate.office.com or outlook.office.com. Sadly for us Open-MailboxInBrowser appears to have been patched by Microsoft, but the technique piqued our interest and begged the question - Could we turn tokens into cookies?

Please note that TokenTactics does still provide functionality to dump a user’s mailbox through the Graph API with Invoke-DumpOWAMailboxViaMSGraphApi, however our preference is to access OWA through a browser for a few reasons:

  • The OWA interface is easy to navigate and provides quick search functionality that can aid in finding information that would be helpful to action objectives
  • Minimize the exfiltration of email items to only those that are of interest to us
  • Having "live" access to a user's mailbox is beneficial for a variety of reasons, including intercepting emails, accessing the Global Address List, or performing easier Business Email Compromise phishing campaigns

The Research Process

The first requirement is to understand the authentication flow that occurs when a user authenticates. We'll do this by looking at the web requests in Burp Suite for a "legitimate" authentication in a test tenant where MFA is not enforced.

Entering a valid username and password and watching the web requests reveals a flurry of requests with a litany of cookies and headers. Through some trial and error, we determined that the cookie that's ultimately responsible for providing access to a mailbox in OWA is named OpenIdConnect.token.v1. This cookie is now our endpoint, and we need to work backwards to determine how to get a value for this cookie.

Through lots (and lots) of trial and error, we determine that when a user enters valid credentials in OWA, in its simplest form, there are 3 important web requests to obtain our OpenIdConnect.token.v1 cookie.

  1. A POST request to https://login.microsoftonline.com/kmsi with our plaintext credentials as parameters.
  2. A POST request to https://outlook.office365.com/owa with two parameters - code and id_token
  3. A GET request to https://outlook.office365.com/owa with two key cookies. If this requests succeeds, then we will receive our OpenIdConnect.token.v1 cookie.

Let's take a look at each step in a bit more detail.

Step 1

A successful POST request to https://login.microsoftonline.com/kmsi returns two hidden form fields named code and id_token. At a quick glance, it's easy to see these have an uncanny resemblance to the Bearer and Refresh token.

In the example above it appears as though the value associated with the code the hidden field is a Refresh token and the id_token value is a Bearer token.

Step 2

In our POST request to https://outlook.office365.com/owa we see two parameters being passed - code and id_token and the values of each match the values from the hidden fields in the first step. A successful request will return a response code of 302 and two new cookies will be created and have values set - OpenIdConnect.id_token.v1 and OpenIdConnect.code.v1.

Step 3

In the final step a GET request with the two cookies OpenIdConnect.id_token.v1 and OpenIdConnect.code.v1 is issued to https://outlook.office365.com. A successful request will return a response code of 302 and our sought after OpenIdConnect.token.v1 cookie.

Once the OpenIdConnect.token.v1 cookie is set we're free to browse https://outlook.office365.com.

Building our Attack

Now that the authentication workflow is clear we can build the web requests for our attack. We'll recreate the authentication workflow, but rather than obtaining the id_token and code hidden parameters from the POST request to login.microsoftonline.com/kmsi, we'll use tokens we've obtained through an alternate method.

The MFA challenge had been disabled on our test tenant to help better understand the authentication flow. We'll create a Conditional Access Policy for the tenant and apply it to all Client apps except "Mobile apps and desktop clients". This policy will require MFA for browser access, but allow us to gain tokens through a tool like TokenTactics.

Remember that our code variable will contain the value of our Refresh token, and id_token will contain the value of our Bearer token. From here the attack is a simple 4 step process.

Step 1.

We start with valid credentials and being prompted for an MFA code when attempting to authenticate Outlook.

The first step is to acquire the necessary tokens for your user. We generally use internal tooling on engagements, but for this demonstration we'll use TokenTactics and obtain tokens for the Outlook client.

Get-AzureToken -Client Outlook

We follow the instructions outlined by TokenTactics and see that we have tokens returned for the https://outlook.office365.com resource. A total of three tokens are returned, two of which start with the standard JTW eyJ. For our purposes we're interested in the access_token for our Bearer token, and the refresh_token.

You can quickly retrieve the values without line breaks from the existing PowerShell window by typing $response.access_token and $response.id_token

Step 2.

Issue a POST request to https://outlook.office365.com/owa and put your Refresh token in your code parameter and your Bearer token in your id_token parameter. You should expect a 302 response with several cookies. The only two we care about are OpenIdConnect.Id_token.v1 and OpenIdConnect.code.v1.

POST /owa/ HTTP/1.1
Host: outlook.office365.com
Origin: https://login.microsoftonline.com
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.5790.110 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: https://login.microsoftonline.com/
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close
Content-Length: 2888

code=[REFRESH_TOKEN_VALUE]&id_token=[BEARER_TOKEN_VALUE]

Step 3.

Issue a GET request to https://outlook.office365.com/owa with your two new cookies. A 302 response should be issued, and your OpenIdConnect.token.v1 cookie should be returned.

GET /owa/ HTTP/1.1
Host: outlook.office365.com
Cookie: OpenIdConnect.id_token.v1=[VALUE_FROM_COOKIE_RETURNED_IN_STEP_2]; OpenIdConnect.code.v1=[VALUE_FROM_COOKIE_RETURNED_IN_STEP_2]; 
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.5790.110 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: cross-site
Sec-Fetch-Mode: navigate
Sec-Fetch-Dest: document
Sec-Ch-Ua: 
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: ""
Referer: https://login.microsoftonline.com/
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close

Step 4.

Open a web browser with a cookie editor create a cookie with the following values.

Domain : outlook.office365.com
Name : OpenIdConnect.token.v1
Value : [Your Cookie Value]
Path : /
SameSite : None
Ensure Secure, HttpOnly and Session are selected

Step 5.

Open your web browser, navigate to https://outlook.office365.com/mail, and enjoy OWA access. If all has gone to plan, you've effectively bypassed MFA and gained access to a user's mailbox with Single-Factor Authentication.

Once you've been successful with manually creating the cookie through a tool like Burp Suite you can leverage scripting languages like Python to make the process easier in the future. This post is intended to shed light on the impact of obtaining valid tokens, so we won't be releasing the code we use to convert tokens to cookies. Suffice it to say that all the steps outlined above are all the information required to create your code.

Limitations

You may notice that after converting your tokens to a cookie that attempts to navigate to additional applications like SharePoint, OneDrive or Teams, it results in a Microsoft authentication page. This is because authentication is bound to the domain outlook.office365.com (or outlook.office.com), and resources like Teams, SharePoint, and OneDrive live outside that domain.

Microsoft has a wide variety of endpoints for M365, so you may find that leveraging the research steps outlined above and understanding authentication workflows for additional resources may provide browser-based access to an organization's M365 resources.

Defensive Guidance

This attack is only possible when we can retrieve tokens when an MFA challenge is expected. This technique does not leverage a security vulnerability but utilizes valid authentication material to create valid authentication material in a different form factor.

At its core, if this attack is possible, it indicates an overly permissive or incorrectly configured Conditional Access Policy (CAP) to an Azure tenant. In our experience, these issues seem more prevalent when an organization leverages an external Identity Provider rather than Azure Active Directory; however, that isn't always the case.

The best defence against this attack is to prevent authentication with Single-Factor Authentication on all endpoints. It would be prudent to try to authenticate against endpoints with various tools (MFASweeper by Beau Bullock(@dafthack) or MSSPray), and the aforementioned TokenTactics are two great places to start). Different tools may leverage different CAP bypass techniques (device authentication, User-Agent modification, etc.), so trying multiple tools to identify gaps is a great start.

Successful authentication through this attack will generate a status of "Success" and an Authentication requirement of "Single-factor authentication" in Azure Sign-in logs for the "Office 365 Exchange Online" resource. The Application ID may vary in your logs depending on what tool was used to request the bearer tokens.

Conclusion

Testing your organization's cloud infrastructure, and controls surrounding it serves a vital function in protecting data. It's important to remember that Microsoft has provided mechanisms to authenticate to Azure-based resources that extend beyond the web browser.

This blog post aimed to showcase the impact of a compromised set of tokens for Outlook Web Application, and highlight some opportunities to harden and detect such activity. Organizations should ensure that MFA challenges are functioning as expected through all authentication mechanisms and ensure any gaps are suitably assessed for security assurance.

References