25/11/2024 0 Comments
Azure & Entra ID token manipulation
Access tokens + Refresh tokens edition.
Audience
This blog is for security enthusiasts, professionals, hobbyists and anyone with a curiosity for cloud security.
Introduction
While i’m not a red teamer or offensive specialist, this post focuses on manipulating and comparing parts of security tokens from an offensive perspective. By now a while back i was simulating token-based attack techniques to develop detection mechanisms when i stumbled upon some findings.
In this blog i’ll be covering two attack scenario’s and two types of security tokens within the Microsoft Identity platform. First is the Access token which grants access to specific resources on behalf of an authenticated user. Secondly when the user acquires an access token to access a resource, the user also receives a Refresh token which is used to obtain new access and refresh token pairs when the current access token expires.
Token theft attacks
Phishing remains the most common identity-based initial infection vector for any organization. Attackers can acquire credentials through various methods. Techniques like device code phishing and adversary in the middle attacks are prevalent for some years now. Zooming in on the token theft attack chain, I'd say the most common ways attackers acquire security tokens are:
- Adversary In The Middle (AITM) attacks;
- (Dynamic) Device code phishing attacks;
- Malware infections (e.g. stealers).
Tools & behavior
During my research phase my goal was to create behavioral detection rules left behind by a variety of cloud-based offensive tools. Detection rules are out of scope for this blog i will however use a small portion of the capabilities of the following tools:
Access vs. Refresh tokens
I will not go into detail on token mechanisms but let’s do a basic refresher.
Access token
An access token is an authentication mechanism that grants specific permissions to access resources. When you authenticate to Entra ID (formerly Azure AD) or another Identity Provider (IDP), you receive a token. This token acts as your key to access applications or services securely.
For a token to be valid, it must be signed by a trusted issuer. Token handling often takes place in the browser or through APIs, enabling seamless communication and authorization between systems. Understanding how tokens work and ensuring their proper management is essential for maintaining secure access in modern cloud environments.
Refresh token
A refresh token is a credential used to obtain new access tokens without requiring the user or application to reauthenticate. When you authenticate to Entra ID (formerly Azure AD) or another Identity Provider (IDP) you likely also receive a refresh token alongside the access token.
Unlike access tokens, which have a short lifetime (± 1 hour), refresh tokens are designed for long-term use (e.g. 90 days). They allow applications to request new access tokens as needed, keeping sessions active without additional login prompts.
OK, let’s dive in.
#Scenario:
So imagine a realistic scenario of an attacker that acquired an Access + Refresh token pair, BUT.. the access token is expired.. Therefore must rely on the refresh token for obtaining a new access token to access victim resources.
I want to run two attack scenario’s, with the second scenario uncovering a fun distinction and the comparing and manipulation bit comes into play.
- Device code phish:
> Token pair obtained of some application; - Malware stealer attack:
> Token pair of a browser session obtained.
The tools i mentioned earlier are easily capable of obtaining a new access token with a refresh token. With below command examples i’ve used msgraph as the application to authenticate to.
ROADtools:
roadtx gettokens --refresh-token "token_value" -r msgraph
TokenTactics:
Invoke-RefreshToGraphToken -domain victim-org.com -refreshToken
"token_value"
Scenario 1: Refresh token from device code phish
This scenario describes the way an attacker obtained a token pair via device code phishing.
Typical flow of device code phish:
- Attacker populates device code:
- Now the attacker needs to send the code + URL (https://microsoft.com/devicelogin) to the victim, via e-mail or other channels, crafting a phishing e-mail. The lured victim enters the device code and proceeds with the authentication steps.
- After the victim authenticates, the attacker receives the token pair as seen below:
- And lastly the attacker can execute the refresh command in order to obtain new tokens and stay persistent within the victims domain:
Invoke-RefreshToGraphToken -domain victim-org.com -refreshToken
"token_value"
Check, check, check. Other than an account takeover no abnormalities here. Now let’s have a look at the next scenario.
Scenario 2: Refresh token from browser session
This scenario describes the way an attacker obtained a token pair from a browser session. In this case, a session from the Azure portal.
First, let’s have a look at where to find the token payload from a browser session, so you can experiment some yourself.
From an authenticated session (M365 or Azure) go to:
- https://portal.azure.com;
- Press F12 / developer tools;
- Click the Network tab;
- In the search bar type “token” and click the Payload tab:
Each token can have a different endpoint in the scope value, more on this later. - Now switch to Response and here you can see both token values:
Access tokens are JSON Web Tokens, A JWT contains three segments separated by the . character. The first segment is the header, the second is the body, and the third is the signature. To decode your token and get information about any JWT token, go to https://jwt.io or https://jwt.ms.
OK, let’s decode the Access token acquired from the browser session:
In order to use a refresh token the request needs certain values in it’s header and body, like the Application ID or Client ID, check out a translated list of verified app IDs. In our case the application name is not a surprise since we’ve fetched it directly from the Azure Portal:
Armed with a valid access token, it let’s an attacker run Azurehound, ROADtools or other tools (AADInternals, Graphrunner, etc) for mapping out or discover organizations resources. ROADtools for now is the more stealthier tool due to the fact that it is using the (almost) deprecated Azure AD graph API endpoint instead of the MS graph API. Tools that use the MS Graph API will leave traces when monitoring the Microsoft graph API activity logs.
But in our scenario.. the access token is expired, so let’s look at some ways of refreshing the obtained token pair. While we’re at it, let’s do some extra testing. Let’s try both API endpoints and see if there are any surprising errors. I’ll be running refresh commands with the earlier mentioned tools.
Both API endpoints:
- MS graph API (graph.microsoft.com)
- AAD graph API (graph.windows.net)
TokenTactics
The Refresh command should now be:
Invoke-RefreshToAzureManagementToken -domain organization.com -ClientId c44b4083-3bb0-49c1-b47d-974e53cbdf3c -refreshToken "token_value"
- Refresh token from the MS graph API ✅
- Refresh token from the AAD graph API ✅
Refresh commands with both tokens succeeded in receiving new token pairs.
ROADtools
The Refresh command should be:
roadtx gettokens --refresh-token "token_value" -r
"app_id_value"
Trying to refresh with the MS Graph token results in the following error message:
This makes sense since ROADtools makes use of the AAD Graph API, so the MS graph refresh token appears invalid. You can check out the different Entra ID error codes here.
- Refresh token from the MS graph API ❌
So if you’re an attacker and only acquired tokens for the MS graph API endpoint.. you need to try another refresh method.. Luckily for us we’ve obtained tokens for BOTH endpoints. Let’s try it against the AAD Graph API:
- Refresh token from the AAD graph API ❌
So, the request was valid, aimed at the correct endpoint.. but this time we receive the following error:
Error during authentication: AADSTS9002327: Tokens issued for the ‘Single-Page Application’ client-type may only be redeemed via cross-origin requests.
So two things in the error response stand out here, let’s remember these for later:
- SPA — Single Page Application
- CORS — Cross Origin Requests
AzureHound
Now for the AzureHound part, i’m not going to request a new access token but rather use the refresh token to run the tool and try to fetch resources (devices, apps, users, etc). AzureHound can do this with a refresh token via the following command:
./azurehound -r "token_value" list --tenant "example.onmicrosoft.com" -o output.json
- Refresh token from the MS graph API ❌
- Refresh token from the AAD graph API ❌
Using both refresh tokens results in error code AADSTS7000, looking at the error code overview here, we can see two potential reasons:
- Token binding header is empty
- Token binding hash does not match
If this was a refresh token from another application like a backend resource as the Microsoft Graph API it will run and fetch information as desired and should look something like this:
Token manipulation
I’ve reproduced the SPA error by crafting a request with Postman to look into the issue a bit more, let’s check it out:
- POST request to: https://login.microsoftonline.com/common/oauth2/v2.0/token
- Headers: application/x-www-url-form-urlencoded
- Body needs to contain:
> grant_type
> client_id (App ID)
> refresh_token
> scope (AAD Graph api endpoint) - Send..
And there we can see the same error code: AADSTS9002327. Now this is where the fun starts and what got me digging into some Microsoft documentation because i haven’t read about this anywhere else. So reading the following documentation on Redirect URIs for single-page apps (SPAs), the following section contains the answer:
To ensure security and best practices, the Microsoft identity platform returns an error if you attempt to use a spa redirect URI without an Origin header. Similarly, the Microsoft identity platform also prevents the use of client credentials in all flows in the presence of an Origin header, to ensure that secrets aren't used from within the browser.
So it needs the origin value in the header of the request. Now the earlier AADSTS7000 error makes more sense as well since the expected token header is incomplete. Let’s add the Origin key with ANY value since that’s not important:
Pretty cool. Within the response we can obtain both Access and Refresh tokens. For this example i’ve used the graph.windows.net (AAD Graph API) endpoint, the same works for the Microsoft Graph API but you’d have to change the scope to the correct endpoint.
Looking back, we’ve done some exploring and seen different characteristics of tools, tokens and APIs. Gaining a solid understanding of basic token mechanisms is very valuable for both defenders and attackers.
I hope you found this insightful or was entertained by the above. If you have any feedback or additional thoughts, I’d be happy to hear either one.
Comments