11 min read

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.

Danger

The tenant boundary is not the same as the network boundary. Every query in the first section goes to Microsoft's infrastructure, not the client's, so the authorisation has to name the tenant — and a tenant can hold subsidiaries, a partner's guest accounts and mailboxes belonging to people outside the engagement. Enumerate the tenant ID and its verified domains, then stop and confirm ownership with the client before you run anything authenticated against it. Keep request volume low: a scripted loop over login.microsoftonline.com is indistinguishable from a password spray at Microsoft's edge, and it is your source address that gets throttled or blocked.

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

tenant-discovery.ps1
# 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

Information

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:

aad-connect-discovery.ps1
# 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

Service Principal Enumeration

service-principals.ps1
# 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

m365-endpoints.sh
# 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

Exchange Hybrid Detection

exchange-hybrid.sh
# 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

Teams & SharePoint Enumeration

teams-sharepoint.sh
# 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

Conditional Access Enumeration

conditional-access.sh
# 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

aws-gcp-metadata.sh
# 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

cloud-credentials.ps1
# 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

What the Defender Sees

Cloud-side telemetry is better than most internal telemetry, because the identity provider logs every sign-in and every directory read whether the client forwards it or not. What to ask for:

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
kql-cloud-enum.kql
// 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

Evidence Collection

Cloud findings are tenant facts, so the evidence is the tenant identifier and the response that proves it — plus the on-prem host that bridges the two.

Tenant identity: Tenant ID, primary domain, verified domains, and whether authentication is managed or federated. Without the tenant ID the client cannot tell which of their directories you tested.

Identity bridge: The AAD Connect server (or PTA agent, or ADFS farm) identified from AD, with the method that found it — MSOL_ account, service connection point, or network presence.

Conditional-access coverage: The policies you could read, and the test matrix you ran (client app, platform, location, device state). The finding is the gap in the matrix, not the policy count.

Hybrid endpoint evidence: MX, autodiscover and federation-metadata responses that establish Exchange Online versus on-prem, and the ADFS endpoint if federated.

Credential artefacts: Files, token caches or service-principal secrets found on hosts, with path and file hash — never the secret value itself in the report body.

False Positive Identification

  • A tenant that exists is not the client's tenant: A domain that resolves to an Entra tenant may belong to a subsidiary, a reseller or an acquired brand. Confirm ownership in writing before testing authenticated paths against it.
  • A metadata response is not a foothold: 169.254.169.254 answering proves the host runs in a cloud, not that it has an attached role. Request the credentials document before calling it credential access.
  • Legacy auth blocked at the edge only: IMAP or SMTP failing from your host can be a rate limit or a tenant block, not a policy. Confirm with an authorised client app before reporting MFA coverage.
  • A service principal with a role assignment: A directory role on a service principal is only useful if you hold its credential. Report the assignment and separately whether the secret was recoverable.

Testing Checklist

Cloud enumeration from an internal position produces tenant facts and bridge hosts. Both need confirming before either is a finding.

Confirm the tenant and the bridge

  • ☐ An unauthenticated query (userrealm, Get-AADIntTenantID) returns the tenant ID and verified domains, and they match the client's tenant in writing.
  • ☐ The AAD Connect host, PTA agent or ADFS farm is identified from AD (MSOL_ account, service connection point) and named.
  • ☐ MX, autodiscover and federation metadata establish Exchange Online versus on-prem, with the responses quoted.
  • ☐ 169.254.169.254 is queried from an in-scope host and either returns role credentials or the reason it does not (IMDSv2 token, proxy).

Confirm the finding is the client's

  • ☐ A discovered tenant is proved to belong to the client before any authenticated path is attempted.
  • ☐ Conditional-access findings name the matrix cell that failed (client app, platform, location, device state), not the policy count.
  • ☐ A service principal with a directory role is reported together with whether its credential was recoverable.
  • ☐ Every credential artefact is recorded by path and hash; the secret itself stays out of the report body.

Remediation & Defense

The controls that make the on-prem to cloud pivot hard are identity controls, not network ones.

Protect the sync identity

  • • Treat the AAD Connect server as Tier 0: separate admin accounts, no interactive logons, no shared admin
  • • Do not sync privileged on-prem groups to the cloud, and keep Global Administrator cloud-only
  • • Prefer password hash sync over PTA where the client has no hard requirement for on-prem validation

Close the authentication gaps

  • • Block legacy authentication protocols tenant-wide, then verify with a real client
  • • Apply conditional access to all apps, including the ones added after the policy was written
  • • Enforce MFA for every user, with number matching to blunt prompt bombing

Detect the enumeration

  • • Alert on AADInternals and ROADtools user agents, and on bulk Graph list operations per user
  • • Monitor 4624/4648 from the AAD Connect host to the DC outside the sync window
  • • Watch for new service principals, app consents and credential additions on existing apps