Apr 7 2025 |
An Operator’s Guide to Device-Joined Hosts and the PRT Cookie
Introduction
About five years ago, Lee Chagolla-Christensen shared a blog detailing the research and development process behind his RequestAADRefreshToken proof-of-concept (POC). In short, on Entra ID joined (including hybrid joined) hosts, it’s possible to obtain a primary refresh token (PRT) cookie from the logged in user’s logon session, enabling an attacker to satisfy single-sign-on (SSO) requirements to cloud resources. Dirk-jan Mollema has also blogged about this capability, where he noted that these PRT cookies (and access tokens requested with them) may contain the multi-factor authentication (MFA) claim — enabling the attacker to access MFA-protected resources.
For a capability that has been publicly known for half a decade, I’ve seen shockingly little online reference to it. I’m not sure if the frequency at which I encounter cloud/hybrid joined devices has recently increased or if I was sleeping on this capability for literal years (more likely), but this tradecraft has been a serious crutch on red team operations in the last six months. While some teams out there are undoubtedly reaping the benefits of this tradecraft during routine operations, I think there are probably quite a few operators out there who, like me, came across the prior works I’ve linked to but didn’t immediately connect the dots.
This blog won’t contain any truly new information or research, but it will try to distill some operationally-focused knowledge that I learned while fumbling through this tradecraft over the last year.
Situational Awareness
A new beacon has called back to your C2 server and you want to know if this blog is applicable to you! This tradecraft will work from device joined hosts — hosts that are either Entra ID joined (cloud-only join) or hybrid joined (joined to both Entra ID and on-premises AD), so we need to identify the join state.
Relevant join information, including the join type, can be obtained by calling NetGetAadJoinInformation from NetApi32.dll. I created a small Beacon object file (BOF) to call the API, which can be found in this PR to the TrustedSec situational awareness (SA) BOF repo. We’re looking for the “Device join” join type (as opposed to workplace join, where an Entra ID work/school account is added to the device but the device isn’t joined to Entra ID).
Some of the other information can be useful, including the tenant ID and user join information, which reveals some details about the account that was leveraged to perform the Entra ID device join (in this case, the same account I’m logged in with locally in the lab, but that is not guaranteed to always align).
Note: I’m using Havoc and its demon agent for C2 in my lab environment since it’s easy to set up, there’s no licensing to mess with, and it nicely approximates some relevant capabilities of commercial frameworks; however, the tradecraft discussed in this post is C2-agnostic. Most if not all of the tools that are referenced already have Cobalt Strike aggressor scripts or can be easily hooked into your C2 of choice.
We can also get most of this information from the registry under the key HKLMSYSTEMCurrentControlSetControlCloudDomainJoin. If present, its values hold information on the device join state and associated Entra ID tenant. An easy approach is to use TrustedSec’s SA reg_query_recursive BOF to surface the interesting values that are stored there.
The JoinInfo key won’t be present on workplace joined hosts, so just the presence of that key and its values should indicate we’re on a host that is device joined.
Alright; we’ve determined the host is Entra ID joined (cloud only in my lab), so the next question is, “What work or school accounts are added to the device?” Or, “What accounts can I obtain refresh token cookies for?”
Initially, this seems like it should be a 1:1 relationship with the account our agent is running in the context of. It probably will be in most cases; however, I’ve encountered production environments where we were able to obtain refresh tokens for multiple Entra ID accounts associated with the compromised user’s logon session. This can happen if the user has added multiple “work or school” accounts within System Settings. Envision a scenario involving a user who has been provisioned a separate admin account; if the user adds both their standard Entra ID account and admin Entra ID account to their user profile, now from the standard user account logon session our agent is running in, we can obtain a refresh token for both accounts. I’ve attempted to mimic this in my lab setup (I’m logged in on the box as MattC@specterdev.onmicrosoft.com and the agent is running as this account), but I’ve also connected a second account to my profile (i.e., admin-mattc@specterdev.onmicrosoft.com) within System Settings.
Note: In these scenarios where multiple work/school accounts are in play, you can obtain a PRT for the account the user is locally logged in with and can obtain a refresh token for the other account(s). I’ll cover this more in the next section.
Finding a C2-friendly way to enumerate which work/school accounts have been added to the user’s profile has proven to be the most difficult part of putting these tradecraft notes together. If you’re familiar with enumerating cloud-joined or hybrid-joined devices, you probably know that you can use dsregcmd.exe to enumerate the information we’ve discussed thus far, and it can also be used to list “web account manager (WAM) accounts.” If you are unfamiliar with the WAM, it is a technology on Windows that allows software such as the Microsoft Authentication Library (MSAL) to acquire tokens for cloud-based accounts. These cloud-based accounts include Entra ID accounts, AD FS accounts (which are sometimes referred to as “Enterprise” accounts), personal Microsoft accounts, and Microsoft work or school accounts. The dsregcmd.exe utility will collectively call all of these account types “WAM accounts” because the WAM can acquire tokens for all of them. The phrase “WAM account” is seldom used elsewhere and “cloud-based accounts” may be a more appropriate phrase, but we will stick with using “WAM account” in this blog to stay consistent with the output of the dsregcmd.exe utility.
If we’re feeling into process creation and/or spawning cmd.exe, we can leverage dsregcmd.exe with the /listaccounts flag to enumerate WAM accounts that have been added (or the /status flag to enumerate the device’s join state).
I can feel the collective boos raining in.
You’re right. Unfortunately, the one function dsregcmd.exe imports from dsreg.dll is undocumented, as are all the other interestingly named exports dsreg.dll has. Reversing those functions to figure out how they can be called from a BOF is way beyond my ability. This left me trying to find other documented ways to query the web account manager. I found three ways I might approach this:
- Using .NET projections of the Windows Runtime (WinRT), including the WebAuthenticationCoreManager .NET class [1, 2]
- Using C++/WinRT or C APIs to access these same WinRT classes [1, 2]
- Using component object model (COM) [1, 2]
My personal preference is to leverage BOFs over .NET assemblies wherever possible. I was also unsure of the viability of using C++/WinRT in a BOF, so I opted for option three: using COM. Several vibe coding sessions later, I patched enough working BOF code together to list the WAM accounts that were added to the current user profile.
The BOF code, along with some POC code to enumerate added WAM accounts in .NET and C++/WinRT, can be found in this repo.
Now that we’ve been responsible operators and confirmed the device join state and WAM accounts that are present, we can continue forward!
Note: My coworkers Evan McBroom and Kai Huang adeptly figured out how to call the undocumented DsrCLI API that dsregcmd.exe calls. You can find an implementation of their work here, which provides operators another option for performing these enumeration steps!
Requesting and Leveraging the PRT Cookie
Finally, the exciting part. Lee’s original RequestAADRefreshToken code was ported to a BOF by wotwot563 in their aad_prt_bof repo. This is trivial to use from our C2 agent and all we need to supply is a nonce for the request. We can obtain a nonce by running roadtx from Dirk-jan’s ROADtools project.
Take the nonce back to your agent and execute the aadprt BOF to obtain a PRT cookie (if you’re following along in the lab, the Havoc script that wraps the BOF can be found in this PR, but the repo already contains an OutflankC2 script and a Cobalt Strike aggressor script you can use. The PR also contains modifications to output the cookies as a JSON blob).

You’ll likely get back a x-ms-RefreshTokenCredential and a x-ms-DeviceCredential for each account. If you obtain more than one token, you’ll see a x-ms-RefreshTokenCredential, then a x-ms-RefreshTokenCredential1, and so on. The refresh token credential(s) are what we’ll be leveraging for post-exploitation. The PR also spits out the cookies in a JSON blob as a quality of life improvement for the next section.
Using the Browser
Each time I’ve performed this in production, any MFA requirement that was present was already satisfied by the tokens that were returned (more on that in the next section). To demonstrate, if I try to logon to the Azure portal with my test account’s username and password, I’m prompted for my MFA method.
Note: We will still need to satisfy all conditional access policies (CAPs) to sign in. At a minimum, this usually involves coming from a trusted IP address. So you’ll likely need to use Proxifier on Windows (or a similar solution) to tunnel your browser traffic through your C2 agent before it goes back out to Microsoft. That is outside the scope of this blog and will be left up to you.
Let’s inject the refresh token cookies from the aadprt BOF into a browser on our operator VM, confirm that we satisfy the MFA policy for our lab environment, and that we can access resources behind SSO. Lee’s blog has the steps to do this manually using Chrome’s developer tools; however, after doing this multiple times per day for multi-week operations, it gets pretty tiresome. My coworker Forrest Kasler has some slick scripts to steal cookies via the Chrome remote debugger and inject them into a new instance of Chromium. We can throw the JSON blob we received from the aadprt BOF into a file and feed the file into a script from Forrest’s collection (i.e., stealer.js), which will add the cookies to our browser and open https://login.microsoftonline.com.

Now, if I open a new tab and browse to the Azure Portal in this Chromium session, it’ll briefly redirect to login.microsoftonline.com and then I’ll be logged into that resource as well without receiving a prompt for MFA.
We can leverage this same workflow to access Microsoft cloud services and apps (e.g., Azure, Teams, SharePoint, etc.) but we can also authenticate to third-party apps configured to leverage SSO. Common examples include applications such as Confluence and Jira, ServiceNow (which MDSec recently blogged about the usefulness of), CyberArk, and even custom internal apps configured to use SSO. When I first realized this, I was curious how to enumerate other third-party services the target tenant tied into SSO. Outside of downloading and reviewing browser history databases, one way is to check for app registrations in Azure. We can do this graphically in the Azure portal, or use ROADrecon data (more on this in a moment) to identify services in its “Applications” pane that contain “Reply URLs.” Here’s an example of the app registration data that was collected for a tenant’s confluence app.
If we navigate to the reply URL in our browser while we have a valid refresh token cookie, we’ll satisfy the SSO requirement and we’ll be logged in (provided that the user we have a token for is granted access to that application).
Using [Your Favorite Entra ID/Azure Post-Ex Toolkit]
We can also leverage the refresh token to request access tokens for the Graph API, or other resources, for usage with Azure post-exploitation toolkits like ROADrecon, TokenTactics, GraphRunner, TeamFiltration, MicroBurst, ADOKit, and more.
I prefer to use roadtx for token manipulation. If we take a token from the aadprt BOF output and use the roadtx auth module, we can request access tokens by passing our refresh token cookie in via the –prt-cookie argument. That will get a token for the older Azure AD Graph by default, but other resources can be specified with the -r/–resource flag.

Now we have a Azure AD Graph access token that we could use to perform a ROADrecon data collection with.
Dirk-jan makes a point in his blog that I think is worth repeating — refresh tokens and access tokens that are requested via a PRT cookie will inherit the same claims that the original PRT cookie had. So if the PRT cookie contained the MFA claim and a device ID (since we originally obtained it on a device joined host), our access tokens will too. This is why we can continue to satisfy CAPs that require MFA or usage of a joined device.
Shifting back to the post-exploitation examples, now that the roadtx auth module has populated my auth file (i.e., .roadtools_auth), it can be fed that into another tool such as TeamFiltration to use its exfiltration modules as well.
For TeamFiltration, the –teams module gathers data including contact lists, shared attachments, and conversations. That makes it a good alternative to adding a refresh token cookie to your browser and graphically browsing Teams.
Other tools may require tokens for different resources or different clients. As an example, MicroBurst modules that gather data on Azure resources rely on the Azure PowerShell module and thus will need an access token for the Azure Resource Manager (AzureRM) that specifies the Azure CLI client ID when authenticating. We can request that access token using roadtx by specifying the associated alias for that resource and the appropriate client name.

Take the resulting access token from the .roadtools_auth file and supply it to the Connect-AzAccount cmdlet from the Az PowerShell module and you should see that it succeeds.

MicroBurst can now be imported and run via Get-AzDomainInfo (in this example, we omit enumeration for things like Entra ID users and groups, which would also require a graph token).

These are just a few examples of the token requests that can be made using different tools. There are a number of other tools you utilize for different areas of the post-exploitation, but the operational workflow will generally remain the same.
Detection Guidance
The different tools outlined during this blog for situational awareness and token requests will cause different DLLs to be loaded in an agent’s process. When these DLLs are present together within the same process, that may indicate suspicious activity:
- aadjoininfo BOF and dsrcli BOF — Used to enumerate the device’s join state and causes C:WindowsSystem32dsreg.dll to be loaded into the beacon process
- listwamaccounts BOF — Used to enumerate work or school accounts added to the current user profile; causes C:WindowsSystem32aadWamExtension.dll to be loaded into the beacon process
- aadprt BOF — Used to obtain refresh token cookies. Being based on Lee’s original POC, the DLL load event he calls out for C:WindowsSystem32MicrosoftAccountTokenProvider.dll is still applicable here
On the separate device joined and workplace joined hosts I have access to, there are no running processes (outside of the beaconing demon.x64.exe process) that have loaded all three DLLs. Even examining processes on my host that have just two of these DLLs loaded shows that it’s limited to svchost.exe, OneDrive.exe, and ServiceHub.IdentityHost.exe.
This is bound to vary from environment to environment, but baselining processes that normally load two or more of these DLLs and monitoring for anomalies may be able to provide an indication of processes to review for reconnaissance and token gathering actions, like those described in this blog.
Conclusion
This blog examined how an operator can perform situational awareness steps prior to making a token request and how tokens can be effectively used once obtained. Hopefully, you’ve come away with some guidance and examples on how to leverage this tradecraft and some of the quality of life scripts/tools that may help you!
An Operator’s Guide to Device-Joined Hosts and the PRT Cookie was originally published in Posts By SpecterOps Team Members on Medium, where people are continuing the conversation by highlighting and responding to this story.