Cloud & Hybrid Enumeration
Internal penetration tests increasingly need to identify cloud integration points. Azure AD Connect servers, Microsoft 365 endpoints, service principals, conditional access policies, and cloud sync configurations are critical targets discovered from within the internal network. This guide covers enumerating the cloud attack surface from an internal perspective.
Information
Cloud enumeration from an internal network often requires no additional credentials β many Azure/M365
endpoints are publicly accessible and leak tenant configuration information.
π Quick Navigation
βοΈ Azure / Entra ID
π§ M365 / Exchange
π Cloud Resources
π― Why Cloud Enumeration Matters in Internal Tests
- Hybrid Reality: 90%+ of enterprises use Azure AD/Entra ID β cloud integration points are present in virtually every internal network.
- Lateral Movement to Cloud: Finding Azure AD Connect, PTA agents, or ADFS servers maps the on-premβcloud pivot path.
- Credential Reuse: Synced accounts share passwords between on-prem AD and cloud β one hash, two environments.
- Expanded Scope: Demonstrating cloud impact from an internal compromise significantly increases finding severity.
Azure Tenant Discovery
bash
# OpenID configuration (no auth required)
curl https://login.microsoftonline.com/DOMAIN.COM/.well-known/openid-configuration
curl https://login.microsoftonline.com/DOMAIN.COM/v2.0/.well-known/openid-configuration | jq .token_endpoint
# AADInternals β comprehensive tenant recon
Import-Module AADInternals
Invoke-AADIntReconAsOutsider -DomainName corp.com
# Check if user exists (user enumeration)
Invoke-AADIntUserEnumerationAsOutsider -UserName "admin@corp.com"
# Enumerate all domains registered to the tenant
Get-AADIntTenantDomains -Domain corp.com# OpenID configuration (no auth required)
curl https://login.microsoftonline.com/DOMAIN.COM/.well-known/openid-configuration
curl https://login.microsoftonline.com/DOMAIN.COM/v2.0/.well-known/openid-configuration | jq .token_endpoint
# AADInternals β comprehensive tenant recon
Import-Module AADInternals
Invoke-AADIntReconAsOutsider -DomainName corp.com
# Check if user exists (user enumeration)
Invoke-AADIntUserEnumerationAsOutsider -UserName "admin@corp.com"
# Enumerate all domains registered to the tenant
Get-AADIntTenantDomains -Domain corp.comInformation
Invoke-AADIntReconAsOutsider returns: Tenant ID/name, authentication type (Managed/Federated),
SSO configuration, Seamless SSO status, MFA configuration, and all registered domains β all without authentication.
Finding Azure AD Connect Servers
Azure AD Connect servers are high-value Tier 0 targets. Find them from the internal network using these methods:
powershell
# Method 1: LDAP query for sync accounts (MSOL_ / AAD_ prefixed)
Get-ADUser -Filter { Name -like "MSOL_*" -or Name -like "AAD_*" } -Properties Description
# Method 2: Service connection point in AD
Get-ADObject -Filter { objectClass -eq "serviceConnectionPoint" -and
Name -eq "62a0ff2e-97b9-4513-943f-0d221bd30080" } -Properties keywords
# Method 3: Network scanning for AAD Connect hosts
nxc smb network-range -u '' -p '' --gen-relay-list relay.txt
# Method 4: BloodHound β find MSOL_ account and source computer
MATCH (u:User) WHERE u.name STARTS WITH "MSOL_"
MATCH (c:Computer)-[:HasSession]->(u) RETURN c.name, u.name
# Method 5: PTA Agent discovery via service connection point
Get-ADObject -Filter { objectClass -eq "serviceConnectionPoint" -and
Name -eq "497b03c2-c5d8-4199-84c1-ee44e5a18a28" } -Properties keywords# Method 1: LDAP query for sync accounts (MSOL_ / AAD_ prefixed)
Get-ADUser -Filter { Name -like "MSOL_*" -or Name -like "AAD_*" } -Properties Description
# Method 2: Service connection point in AD
Get-ADObject -Filter { objectClass -eq "serviceConnectionPoint" -and
Name -eq "62a0ff2e-97b9-4513-943f-0d221bd30080" } -Properties keywords
# Method 3: Network scanning for AAD Connect hosts
nxc smb network-range -u '' -p '' --gen-relay-list relay.txt
# Method 4: BloodHound β find MSOL_ account and source computer
MATCH (u:User) WHERE u.name STARTS WITH "MSOL_"
MATCH (c:Computer)-[:HasSession]->(u) RETURN c.name, u.name
# Method 5: PTA Agent discovery via service connection point
Get-ADObject -Filter { objectClass -eq "serviceConnectionPoint" -and
Name -eq "497b03c2-c5d8-4199-84c1-ee44e5a18a28" } -Properties keywordsService Principal Enumeration
bash
# ROADtools β comprehensive Azure AD enumeration (requires valid creds)
roadrecon auth -u user@corp.com -p 'Password1'
roadrecon gather
roadrecon gui
# AzureAD PowerShell β enumerate service principals
Connect-AzureAD
Get-AzureADServicePrincipal -All $true | Select-Object DisplayName, AppId,
ServicePrincipalType | Where-Object { $_.ServicePrincipalType -eq "Application" }
# Find service principals with privileged roles
Get-AzureADDirectoryRole | ForEach-Object {
$role = $_
Get-AzureADDirectoryRoleMember -ObjectId $_.ObjectId | ForEach-Object {
[PSCustomObject]@{ Role = $role.DisplayName; Member = $_.DisplayName; Type = $_.ObjectType }
}
} | Where-Object { $_.Type -eq "ServicePrincipal" }# ROADtools β comprehensive Azure AD enumeration (requires valid creds)
roadrecon auth -u user@corp.com -p 'Password1'
roadrecon gather
roadrecon gui
# AzureAD PowerShell β enumerate service principals
Connect-AzureAD
Get-AzureADServicePrincipal -All $true | Select-Object DisplayName, AppId,
ServicePrincipalType | Where-Object { $_.ServicePrincipalType -eq "Application" }
# Find service principals with privileged roles
Get-AzureADDirectoryRole | ForEach-Object {
$role = $_
Get-AzureADDirectoryRoleMember -ObjectId $_.ObjectId | ForEach-Object {
[PSCustomObject]@{ Role = $role.DisplayName; Member = $_.DisplayName; Type = $_.ObjectType }
}
} | Where-Object { $_.Type -eq "ServicePrincipal" }Microsoft 365 Endpoint Enumeration
bash
# MX records reveal mail routing (Exchange Online vs hybrid)
nslookup -type=MX corp.com
# If MX points to *.mail.protection.outlook.com β Exchange Online
# If MX points to internal server β Exchange on-prem or hybrid
# Autodiscover β reveals Exchange configuration
curl https://autodiscover-s.outlook.com/autodiscover/autodiscover.json?Email=user@corp.com
# Federation metadata (ADFS detection)
curl https://login.microsoftonline.com/corp.com/FederationMetadata/2007-06/FederationMetadata.xml
# If federated, reveals ADFS server URL (e.g., sts.corp.com)
# From internal network β find ADFS servers
nslookup sts.corp.com
nslookup adfs.corp.com
nxc smb sts.corp.com -u '' -p ''
# Check for Exchange Online PowerShell endpoint
curl -I https://outlook.office365.com/PowerShell-LiveID
# Graph API β check available endpoints
curl https://graph.microsoft.com/v1.0/$metadata# MX records reveal mail routing (Exchange Online vs hybrid)
nslookup -type=MX corp.com
# If MX points to *.mail.protection.outlook.com β Exchange Online
# If MX points to internal server β Exchange on-prem or hybrid
# Autodiscover β reveals Exchange configuration
curl https://autodiscover-s.outlook.com/autodiscover/autodiscover.json?Email=user@corp.com
# Federation metadata (ADFS detection)
curl https://login.microsoftonline.com/corp.com/FederationMetadata/2007-06/FederationMetadata.xml
# If federated, reveals ADFS server URL (e.g., sts.corp.com)
# From internal network β find ADFS servers
nslookup sts.corp.com
nslookup adfs.corp.com
nxc smb sts.corp.com -u '' -p ''
# Check for Exchange Online PowerShell endpoint
curl -I https://outlook.office365.com/PowerShell-LiveID
# Graph API β check available endpoints
curl https://graph.microsoft.com/v1.0/$metadataExchange Hybrid Detection
bash
# Exchange hybrid environments have both on-prem and cloud mailboxes
# Identify hybrid configuration from internal network:
# Find Exchange servers via AD
Get-ADComputer -Filter { ServicePrincipalName -like "*exchangeMDB*" } -Properties ServicePrincipalName
# SPN-based enumeration for Exchange
setspn -Q */exchange*
setspn -Q */autodiscover*
# Exchange Management Shell (if accessible)
Get-ExchangeServer | Select-Object Name, ServerRole, Edition
Get-HybridConfiguration | Select-Object *
# Check for Exchange Web Services
nxc http exchange-server.corp.local -u user -p 'Pass123' -M ews
# Outlook Anywhere / MAPI
curl https://exchange-server.corp.local/owa/
curl https://exchange-server.corp.local/EWS/Exchange.asmx# Exchange hybrid environments have both on-prem and cloud mailboxes
# Identify hybrid configuration from internal network:
# Find Exchange servers via AD
Get-ADComputer -Filter { ServicePrincipalName -like "*exchangeMDB*" } -Properties ServicePrincipalName
# SPN-based enumeration for Exchange
setspn -Q */exchange*
setspn -Q */autodiscover*
# Exchange Management Shell (if accessible)
Get-ExchangeServer | Select-Object Name, ServerRole, Edition
Get-HybridConfiguration | Select-Object *
# Check for Exchange Web Services
nxc http exchange-server.corp.local -u user -p 'Pass123' -M ews
# Outlook Anywhere / MAPI
curl https://exchange-server.corp.local/owa/
curl https://exchange-server.corp.local/EWS/Exchange.asmxTeams & SharePoint Enumeration
bash
# SharePoint Online tenant URL
# https://corp.sharepoint.com
# https://corp-my.sharepoint.com (OneDrive)
# Find SharePoint on-prem servers
Get-ADComputer -Filter { ServicePrincipalName -like "*SharePoint*" }
nxc smb network -u user -p 'Pass123' --spider "*.sharepoint.*"
# Teams β with valid credentials, enumerate via Graph API
# GET https://graph.microsoft.com/v1.0/me/joinedTeams
# GET https://graph.microsoft.com/v1.0/groups?$filter=resourceProvisioningOptions/Any(x:x eq 'Team')
# TeamFiltration β enumerate and spray Teams
TeamFiltration.exe --outpath results --enum --domain corp.com
# Search SharePoint for sensitive files (with access)
# Use ROADtools or manual Graph API queries# SharePoint Online tenant URL
# https://corp.sharepoint.com
# https://corp-my.sharepoint.com (OneDrive)
# Find SharePoint on-prem servers
Get-ADComputer -Filter { ServicePrincipalName -like "*SharePoint*" }
nxc smb network -u user -p 'Pass123' --spider "*.sharepoint.*"
# Teams β with valid credentials, enumerate via Graph API
# GET https://graph.microsoft.com/v1.0/me/joinedTeams
# GET https://graph.microsoft.com/v1.0/groups?$filter=resourceProvisioningOptions/Any(x:x eq 'Team')
# TeamFiltration β enumerate and spray Teams
TeamFiltration.exe --outpath results --enum --domain corp.com
# Search SharePoint for sensitive files (with access)
# Use ROADtools or manual Graph API queriesConditional Access Enumeration
bash
# Understanding Conditional Access policies reveals MFA gaps and bypass paths
# ROADtools β dump CA policies
roadrecon auth -u user@corp.com -p 'Password1'
roadrecon gather --roadrecon-gather-conditional-access
roadrecon gui
# Navigate to Policies β Conditional Access
# AADInternals β enumerate authentication methods
Get-AADIntConditionalAccessPolicies -AccessToken $token
# Test different authentication parameters to map CA policy coverage:
# - Different client apps (desktop, browser, mobile, legacy)
# - Different platforms (Windows, macOS, iOS, Android)
# - Different locations (internal IP, VPN, external)
# - Different managed/unmanaged device states
# Legacy authentication often bypasses MFA requirements
# Test IMAP/POP/SMTP authentication
python3 -c "
import imaplib
m = imaplib.IMAP4_SSL('outlook.office365.com')
m.login('user@corp.com', 'Password1')
# If this works, legacy auth is allowed (bypasses MFA)
"# Understanding Conditional Access policies reveals MFA gaps and bypass paths
# ROADtools β dump CA policies
roadrecon auth -u user@corp.com -p 'Password1'
roadrecon gather --roadrecon-gather-conditional-access
roadrecon gui
# Navigate to Policies β Conditional Access
# AADInternals β enumerate authentication methods
Get-AADIntConditionalAccessPolicies -AccessToken $token
# Test different authentication parameters to map CA policy coverage:
# - Different client apps (desktop, browser, mobile, legacy)
# - Different platforms (Windows, macOS, iOS, Android)
# - Different locations (internal IP, VPN, external)
# - Different managed/unmanaged device states
# Legacy authentication often bypasses MFA requirements
# Test IMAP/POP/SMTP authentication
python3 -c "
import imaplib
m = imaplib.IMAP4_SSL('outlook.office365.com')
m.login('user@corp.com', 'Password1')
# If this works, legacy auth is allowed (bypasses MFA)
"AWS / GCP Detection from Internal
bash
# Check for AWS integration
# EC2 metadata endpoint (if running on EC2)
curl -s http://169.254.169.254/latest/meta-data/
# IAM role credentials
curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/
# Find AWS credentials in files
findstr /s /i "AKIA" C:\Users* # AWS access key IDs start with AKIA
findstr /s /i "aws_access_key|aws_secret" C:\Users*.aws*
# GCP metadata
curl -H "Metadata-Flavor: Google" http://169.254.169.254/computeMetadata/v1/instance/service-accounts/
# Find GCP service account keys
dir /s /b C:\Users*service-account*.json
# Environment variables
set | findstr /i "AWS_|AZURE_|GCP_|GOOGLE_"# Check for AWS integration
# EC2 metadata endpoint (if running on EC2)
curl -s http://169.254.169.254/latest/meta-data/
# IAM role credentials
curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/
# Find AWS credentials in files
findstr /s /i "AKIA" C:\Users* # AWS access key IDs start with AKIA
findstr /s /i "aws_access_key|aws_secret" C:\Users*.aws*
# GCP metadata
curl -H "Metadata-Flavor: Google" http://169.254.169.254/computeMetadata/v1/instance/service-accounts/
# Find GCP service account keys
dir /s /b C:\Users*service-account*.json
# Environment variables
set | findstr /i "AWS_|AZURE_|GCP_|GOOGLE_"Cloud Credential Discovery
powershell
# Search for cloud credentials across gathered machines
# Azure CLI tokens
type %USERPROFILE%\.azureaccessTokens.json
type %USERPROFILE%\.azuremsal_token_cache.json
# Azure PowerShell tokens
type %USERPROFILE%\.Azure\TokenCache.dat
type %USERPROFILE%\.Azure\AzureRmContext.json
# AWS credentials
type %USERPROFILE%\.awscredentials
type %USERPROFILE%\.awsconfig
# GCP credentials
type %APPDATA%\gcloudcredentials.db
type %APPDATA%\gcloudaccess_tokens.db
# Service principal certificates and secrets in AD
Get-ADObject -Filter { objectClass -eq "msDS-ManagedServiceAccount" } -Properties *
# Search config files for client secrets
findstr /s /i "client_secret|tenant_id|subscription_id" \\fileservershare\*.config
findstr /s /i "client_secret|tenant_id" \\fileservershare\*.json# Search for cloud credentials across gathered machines
# Azure CLI tokens
type %USERPROFILE%\.azureaccessTokens.json
type %USERPROFILE%\.azuremsal_token_cache.json
# Azure PowerShell tokens
type %USERPROFILE%\.Azure\TokenCache.dat
type %USERPROFILE%\.Azure\AzureRmContext.json
# AWS credentials
type %USERPROFILE%\.awscredentials
type %USERPROFILE%\.awsconfig
# GCP credentials
type %APPDATA%\gcloudcredentials.db
type %APPDATA%\gcloudaccess_tokens.db
# Service principal certificates and secrets in AD
Get-ADObject -Filter { objectClass -eq "msDS-ManagedServiceAccount" } -Properties *
# Search config files for client secrets
findstr /s /i "client_secret|tenant_id|subscription_id" \\fileservershare\*.config
findstr /s /i "client_secret|tenant_id" \\fileservershare\*.jsonDetection & Blue Team
| Source | Detection |
|---|---|
| Azure AD Sign-in Logs | Unusual AADInternals / ROADtools user-agent strings, bulk Graph API queries |
| Azure AD Audit Logs | Service principal enumeration, directory role queries, app consent events |
| Event 4662 | Read access to AAD Connect replication account (MSOL_*) or DPAPI-NG keys |
| Event 4624 / 4648 | Logons from AAD Connect server to DC using MSOL_ account outside sync schedule |
| Cloud Audit Logs | AWS CloudTrail / GCP Audit Logs showing credential file access or IAM enumeration |
text
// KQL β Detect AADInternals / ROADtools enumeration via user-agent
SigninLogs
| where UserAgent has_any ("AADInternals", "ROADtools", "python-requests")
| project TimeGenerated, UserPrincipalName, AppDisplayName, UserAgent, IPAddress
// Detect bulk Graph API enumeration
AuditLogs
| where OperationName has_any ("List servicePrincipals", "List applications", "List directoryRoles")
| summarize Count=count() by InitiatedBy.user.userPrincipalName, bin(TimeGenerated, 5m)
| where Count > 50// KQL β Detect AADInternals / ROADtools enumeration via user-agent
SigninLogs
| where UserAgent has_any ("AADInternals", "ROADtools", "python-requests")
| project TimeGenerated, UserPrincipalName, AppDisplayName, UserAgent, IPAddress
// Detect bulk Graph API enumeration
AuditLogs
| where OperationName has_any ("List servicePrincipals", "List applications", "List directoryRoles")
| summarize Count=count() by InitiatedBy.user.userPrincipalName, bin(TimeGenerated, 5m)
| where Count > 50