Post-Authentication Enumeration
With valid credentials or tokens in hand, systematically enumerate the tenant's users, groups, roles, applications, conditional access policies, and service configurations to map the attack surface from the inside.
Phase 4 Checklist
0 / 9 completed📂 Entra ID Enumeration
The first objective once inside a tenant is to enumerate every user, group, and group membership. This gives you a full organisational map, helps identify high-value targets (executives, IT admins, service accounts), and reveals group structures that may control access to sensitive resources.
Microsoft Graph API — Direct Calls
These are the raw REST endpoints. You can call them with any HTTP client using a Bearer token. The $select and $top parameters control which properties are returned and pagination size.
# Enumerate all users (requires User.Read.All or Directory.Read.All)
GET https://graph.microsoft.com/v1.0/users?$select=id,displayName,userPrincipalName,mail,jobTitle,department,accountEnabled&$top=999
# Enumerate all groups (requires Group.Read.All or Directory.Read.All)
GET https://graph.microsoft.com/v1.0/groups?$select=id,displayName,description,groupTypes,mailEnabled,securityEnabled&$top=999
# Get members of a specific group
GET https://graph.microsoft.com/v1.0/groups/{group-id}/members?$select=id,displayName,userPrincipalName
# Get all group memberships for a specific user
GET https://graph.microsoft.com/v1.0/users/{user-id}/memberOf?$select=id,displayName,@odata.type
# Enumerate guest users (external identities)
GET https://graph.microsoft.com/v1.0/users?$filter=userType eq 'Guest'&$select=id,displayName,mail,userPrincipalName
GraphRunner (PowerShell)
GraphRunner is a post-exploitation tool for Microsoft Graph that automates enumeration, data extraction, and persistence. It operates entirely through Graph API tokens.
# Import GraphRunner module
Import-Module .\GraphRunner.ps1
# Authenticate with a stolen access token or device code flow
# GraphRunner provides multiple auth methods
Get-GraphTokens
# Run the full enumeration suite - this enumerates users, groups,
# apps, roles, conditional access, and more in a single command
Invoke-GraphRunner
# Or run individual enumeration commands:
# Enumerate all users and export
Get-AzureADUsers -Tokens $tokens -OutFile users.json
# Enumerate all groups and memberships
Get-AzureADGroups -Tokens $tokens -OutFile groups.json
ROADtools (Python)
ROADtools by Dirk-jan Mollema collects Azure AD data into a local SQLite database and provides a web-based GUI for exploration. Ideal for offline analysis of large tenants.
# Install ROADtools
pip install roadtools
# Authenticate (supports multiple auth methods)
roadrecon auth -u user@target.com -p 'Password123'
# Or use an access token directly
roadrecon auth --access-token eyJ0eXAiOiJKV1Q...
# Gather all tenant data into a local database
# This pulls users, groups, roles, apps, service principals,
# conditional access policies, and more
roadrecon gather
# Launch the interactive web GUI to explore the data
roadrecon gui
# Opens browser at http://127.0.0.1:5000 with full tenant map
# The database is stored locally as roadrecon.db (SQLite)
# You can query it directly for custom analysis
sqlite3 roadrecon.db "SELECT userPrincipalName, jobTitle FROM Users WHERE accountEnabled = 1"
AzureHound (BloodHound Integration)
AzureHound collects Azure AD and Azure Resource Manager data for ingestion into BloodHound. This enables graph-based attack path analysis across the tenant.
# Download the latest AzureHound release from GitHub
# https://github.com/BloodHoundAD/AzureHound
# Authenticate and collect all data
./azurehound -u user@target.com -p 'Password123' list -o output.json --tenant target.onmicrosoft.com
# Or use a refresh token for authentication
./azurehound -r eyJ0eXAi... list -o output.json --tenant target.onmicrosoft.com
# Import the collected data into BloodHound CE
# Then query for attack paths from your compromised user to Global Admin
Microsoft Graph PowerShell SDK
The official Microsoft.Graph PowerShell module provides cmdlets that wrap the Graph API. Useful for targeted queries when you have an authenticated session.
# Connect with delegated permissions
Connect-MgGraph -Scopes "User.Read.All","Group.Read.All","Directory.Read.All"
# Enumerate all enabled users
Get-MgUser -All -Filter "accountEnabled eq true" -Property Id,DisplayName,UserPrincipalName,JobTitle,Department,Mail |
Export-Csv -Path users.csv -NoTypeInformation
# Enumerate all groups
Get-MgGroup -All -Property Id,DisplayName,Description,GroupTypes,SecurityEnabled,MailEnabled |
Export-Csv -Path groups.csv -NoTypeInformation
# Get members of each group
$groups = Get-MgGroup -All
foreach ($group in $groups) {
$members = Get-MgGroupMember -GroupId $group.Id -All
Write-Output "--- $($group.DisplayName) ($($members.Count) members) ---"
$members | ForEach-Object { Write-Output " $($_.AdditionalProperties.displayName)" }
}
# Find guest/external users
Get-MgUser -All -Filter "userType eq 'Guest'" -Property DisplayName,Mail,UserPrincipalName |
Format-Table DisplayName, Mail, UserPrincipalName
BlockMsolPowerShell to true. Note that Graph API access is harder to fully restrict without impacting legitimate operations.
Identifying which users hold administrative roles is critical. Global Administrators, Privileged Role Administrators, Application Administrators, and Exchange Administrators are high-value targets. Understanding who has standing assignments versus PIM-eligible roles helps prioritise your attack path.
Graph API — Directory Role Enumeration
# List all activated directory roles in the tenant
GET https://graph.microsoft.com/v1.0/directoryRoles?$select=id,displayName,description
# Get members of a specific directory role (e.g., Global Administrator)
# First find the role ID, then query its members
GET https://graph.microsoft.com/v1.0/directoryRoles/{role-id}/members?$select=id,displayName,userPrincipalName
# List all available role definitions (including unactivated)
GET https://graph.microsoft.com/v1.0/roleManagement/directory/roleDefinitions?$select=id,displayName,isBuiltIn
# Get all active role assignments
GET https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignments?$expand=principal($select=displayName,userPrincipalName)
# Get PIM-eligible role assignments (requires PrivilegedAccess.Read.AzureADGroup)
GET https://graph.microsoft.com/v1.0/roleManagement/directory/roleEligibilitySchedules?$expand=principal($select=displayName,userPrincipalName)
PowerShell — Enumerate All Admin Roles
# Connect to Graph with appropriate scopes
Connect-MgGraph -Scopes "RoleManagement.Read.Directory","Directory.Read.All"
# Get all directory roles that have been activated (have members)
$roles = Get-MgDirectoryRole -All
foreach ($role in $roles) {
$members = Get-MgDirectoryRoleMember -DirectoryRoleId $role.Id
if ($members.Count -gt 0) {
Write-Output "`n=== $($role.DisplayName) ($($members.Count) members) ==="
foreach ($member in $members) {
$user = Get-MgUser -UserId $member.Id -Property DisplayName,UserPrincipalName
Write-Output " $($user.DisplayName) - $($user.UserPrincipalName)"
}
}
}
# Specifically find Global Administrators
$globalAdminRole = Get-MgDirectoryRole -Filter "displayName eq 'Global Administrator'"
Get-MgDirectoryRoleMember -DirectoryRoleId $globalAdminRole.Id | ForEach-Object {
Get-MgUser -UserId $_.Id -Property DisplayName,UserPrincipalName,AccountEnabled
} | Format-Table DisplayName, UserPrincipalName, AccountEnabled
# Enumerate PIM-eligible assignments (who CAN become admin)
Get-MgRoleManagementDirectoryRoleEligibilitySchedule -All -ExpandProperty Principal |
Select-Object @{N='Role';E={
(Get-MgRoleManagementDirectoryRoleDefinition -UnifiedRoleDefinitionId $_.RoleDefinitionId).DisplayName
}}, @{N='Principal';E={$_.Principal.AdditionalProperties.displayName}}, Status |
Format-Table -AutoSize
High-Value Roles to Target
| Role Name | Risk Level | Why It Matters |
|---|---|---|
Global Administrator |
Critical | Full control over the entire tenant, all services, all data |
Privileged Role Administrator |
Critical | Can assign any role to any user, including Global Admin |
Application Administrator |
High | Can manage all app registrations and consent to any permissions |
Cloud Application Administrator |
High | Same as Application Admin except cannot manage app proxy |
Exchange Administrator |
High | Full access to Exchange Online, can read all mailboxes via eDiscovery |
SharePoint Administrator |
High | Full access to all SharePoint sites and OneDrive content |
Authentication Administrator |
High | Can reset passwords and MFA for non-admin users |
Privileged Authentication Administrator |
Critical | Can reset passwords and MFA for ALL users including admins |
Intune Administrator |
High | Can deploy scripts and configurations to all managed devices |
Conditional Access (CA) policies are the primary security control in Entra ID. Reading these policies reveals which users, applications, and conditions are protected — and more importantly, what is excluded. Every exclusion is a potential attack path. Service accounts, break-glass accounts, and legacy authentication exceptions are common gaps.
Graph API — Reading CA Policies
Reading CA policies requires the Policy.Read.All permission. Standard users typically do not have this scope, but compromised admin accounts or over-permissioned apps often do.
# List all Conditional Access policies (requires Policy.Read.All)
GET https://graph.microsoft.com/v1.0/identity/conditionalAccess/policies
# Get a specific policy by ID with full detail
GET https://graph.microsoft.com/v1.0/identity/conditionalAccess/policies/{policy-id}
# List named locations (trusted IPs, countries)
GET https://graph.microsoft.com/v1.0/identity/conditionalAccess/namedLocations
PowerShell — Full CA Policy Audit
# Connect with policy read permissions
Connect-MgGraph -Scopes "Policy.Read.All","Directory.Read.All"
# Get all CA policies
$policies = Get-MgIdentityConditionalAccessPolicy -All
foreach ($policy in $policies) {
Write-Output "`n=========================================="
Write-Output "Policy: $($policy.DisplayName)"
Write-Output "State: $($policy.State)"
Write-Output "=========================================="
# Analyse conditions
$conditions = $policy.Conditions
# Who is included?
Write-Output "`n[Included Users/Groups]"
if ($conditions.Users.IncludeUsers -contains "All") {
Write-Output " All Users"
} else {
$conditions.Users.IncludeUsers | ForEach-Object {
$user = Get-MgUser -UserId $_ -ErrorAction SilentlyContinue
Write-Output " User: $($user.DisplayName) ($($user.UserPrincipalName))"
}
$conditions.Users.IncludeGroups | ForEach-Object {
$group = Get-MgGroup -GroupId $_ -ErrorAction SilentlyContinue
Write-Output " Group: $($group.DisplayName)"
}
}
# Who is EXCLUDED? (This is the interesting part)
Write-Output "`n[EXCLUDED Users/Groups] <-- Potential gaps"
$conditions.Users.ExcludeUsers | ForEach-Object {
$user = Get-MgUser -UserId $_ -ErrorAction SilentlyContinue
Write-Output " EXCLUDED User: $($user.DisplayName) ($($user.UserPrincipalName))"
}
$conditions.Users.ExcludeGroups | ForEach-Object {
$group = Get-MgGroup -GroupId $_ -ErrorAction SilentlyContinue
Write-Output " EXCLUDED Group: $($group.DisplayName)"
}
$conditions.Users.ExcludeRoles | ForEach-Object {
$roleDef = Get-MgRoleManagementDirectoryRoleDefinition -UnifiedRoleDefinitionId $_
Write-Output " EXCLUDED Role: $($roleDef.DisplayName)"
}
# Which apps are targeted?
Write-Output "`n[Target Applications]"
if ($conditions.Applications.IncludeApplications -contains "All") {
Write-Output " All Applications"
} else {
$conditions.Applications.IncludeApplications | ForEach-Object {
Write-Output " App: $_"
}
}
# Excluded applications
$conditions.Applications.ExcludeApplications | ForEach-Object {
Write-Output " EXCLUDED App: $_"
}
# What controls are enforced?
Write-Output "`n[Grant Controls]"
$policy.GrantControls.BuiltInControls | ForEach-Object {
Write-Output " Control: $_"
}
# Platform conditions
if ($conditions.Platforms) {
Write-Output "`n[Platform Conditions]"
Write-Output " Include: $($conditions.Platforms.IncludePlatforms -join ', ')"
Write-Output " Exclude: $($conditions.Platforms.ExcludePlatforms -join ', ')"
}
Write-Output ""
}
ConditionalAccessDocumentation Tool
This community tool generates a comprehensive HTML or Excel report of all CA policies, making it easy to review exclusions visually.
# Install the CA documentation module
Install-Module -Name ConditionalAccessDocumentation
# Generate an HTML report of all CA policies
# This resolves GUIDs to friendly names and highlights exclusions
Export-ConditionalAccessPolicy -OutputFormat HTML -OutputPath .\CA_Report.html
# Generate Excel format for detailed analysis
Export-ConditionalAccessPolicy -OutputFormat Excel -OutputPath .\CA_Report.xlsx
Common CA Policy Gaps to Look For
- Break-glass accounts excluded from MFA: Emergency access accounts that bypass all CA policies are high-value targets if their credentials are weak
- Service accounts excluded from MFA: Accounts used for automation that cannot satisfy interactive MFA prompts
- Legacy authentication not blocked: Policies that do not explicitly block legacy auth protocols (IMAP, POP3, SMTP AUTH) allow credential-based attacks to bypass MFA
- Specific applications excluded: Apps excluded from CA policies may provide an unprotected authentication path
- Platform exclusions: Policies that only target specific platforms (e.g., Windows) leave other platforms (Linux, mobile) unprotected
- Named location trust: Policies that skip MFA from "trusted" IP ranges can be bypassed if you compromise infrastructure within those ranges
- Device compliance gaps: Policies requiring compliant devices but only for certain apps
📦 Application & Service Principal Enumeration
App registrations define applications in the tenant. Each registration can hold secrets (client secrets) or certificates used for authentication, and it declares the API permissions it requires. Enumerating these reveals over-privileged apps, apps with expiring or forgotten secrets, and apps owned by regular users who can escalate through them.
Graph API — Enumerate App Registrations
# List all app registrations (requires Application.Read.All)
GET https://graph.microsoft.com/v1.0/applications?$select=id,appId,displayName,requiredResourceAccess,passwordCredentials,keyCredentials,signInAudience&$top=999
# Get owners of a specific app (who can modify this app?)
GET https://graph.microsoft.com/v1.0/applications/{app-object-id}/owners?$select=id,displayName,userPrincipalName
# Check the requiredResourceAccess property for dangerous permissions
# Each entry maps to a resourceAppId (e.g., Microsoft Graph = 00000003-0000-0000-c000-000000000000)
# and lists the permission IDs the app requests
PowerShell — Full App Registration Audit
# Connect with application read permissions
Connect-MgGraph -Scopes "Application.Read.All","Directory.Read.All"
# Enumerate all app registrations with key details
$apps = Get-MgApplication -All -Property Id,AppId,DisplayName,RequiredResourceAccess,PasswordCredentials,KeyCredentials,SignInAudience
foreach ($app in $apps) {
Write-Output "`n=== $($app.DisplayName) ==="
Write-Output " AppId: $($app.AppId)"
Write-Output " Sign-in Audience: $($app.SignInAudience)"
# Check for client secrets
if ($app.PasswordCredentials.Count -gt 0) {
Write-Output " [!] CLIENT SECRETS FOUND:"
foreach ($secret in $app.PasswordCredentials) {
Write-Output " - KeyId: $($secret.KeyId)"
Write-Output " Display: $($secret.DisplayName)"
Write-Output " Expires: $($secret.EndDateTime)"
$daysLeft = (New-TimeSpan -Start (Get-Date) -End $secret.EndDateTime).Days
if ($daysLeft -lt 0) {
Write-Output " STATUS: EXPIRED"
} elseif ($daysLeft -lt 30) {
Write-Output " STATUS: EXPIRING SOON ($daysLeft days)"
}
}
}
# Check for certificates
if ($app.KeyCredentials.Count -gt 0) {
Write-Output " [!] CERTIFICATES FOUND:"
foreach ($cert in $app.KeyCredentials) {
Write-Output " - Type: $($cert.Type) | Expires: $($cert.EndDateTime)"
}
}
# Enumerate requested permissions
foreach ($resource in $app.RequiredResourceAccess) {
$resourceName = switch ($resource.ResourceAppId) {
"00000003-0000-0000-c000-000000000000" { "Microsoft Graph" }
"00000002-0000-0ff1-ce00-000000000000" { "Office 365 Exchange Online" }
"00000003-0000-0ff1-ce00-000000000000" { "SharePoint Online" }
default { $resource.ResourceAppId }
}
Write-Output " Permissions on $resourceName :"
foreach ($perm in $resource.ResourceAccess) {
$permType = if ($perm.Type -eq "Role") { "APPLICATION" } else { "Delegated" }
Write-Output " [$permType] $($perm.Id)"
}
}
# Get owners
$owners = Get-MgApplicationOwner -ApplicationId $app.Id
if ($owners.Count -gt 0) {
Write-Output " Owners:"
foreach ($owner in $owners) {
Write-Output " - $($owner.AdditionalProperties.displayName) ($($owner.AdditionalProperties.userPrincipalName))"
}
}
}
Dangerous App Permissions to Flag
| Permission | Type | Risk |
|---|---|---|
RoleManagement.ReadWrite.Directory |
Application | Critical — Can assign Global Admin to any user |
AppRoleAssignment.ReadWrite.All |
Application | Critical — Can grant any app permission to any service principal |
Directory.ReadWrite.All |
Application | Critical — Full read/write to all directory objects |
Mail.ReadWrite |
Application | High — Read/write all mailboxes without user context |
Mail.Send |
Application | High — Send mail as any user in the tenant |
Files.ReadWrite.All |
Application | High — Full access to all OneDrive and SharePoint files |
Sites.ReadWrite.All |
Application | High — Full access to all SharePoint sites |
User.ReadWrite.All |
Application | High — Modify all user profiles, including authentication methods |
RoleManagement.ReadWrite.Directory.
Service principals are the local tenant representation of an application. While app registrations define what an app can request, service principals define what has actually been granted in the tenant. Analysing service principals reveals which apps have active credentials, what permissions have been consented to, and which third-party apps have access to your tenant's data.
Graph API — Service Principal Enumeration
# List all service principals in the tenant
GET https://graph.microsoft.com/v1.0/servicePrincipals?$select=id,appId,displayName,servicePrincipalType,passwordCredentials,keyCredentials&$top=999
# Get service principals with credentials (client secrets or certificates)
# Filter for those that have password credentials set
GET https://graph.microsoft.com/v1.0/servicePrincipals?$filter=passwordCredentials/$count gt 0&$count=true&$select=id,displayName,appId,passwordCredentials
# Header required: ConsistencyLevel: eventual
# Get app role assignments granted TO a service principal
GET https://graph.microsoft.com/v1.0/servicePrincipals/{sp-id}/appRoleAssignments
# Get delegated permission grants (OAuth2) for a service principal
GET https://graph.microsoft.com/v1.0/servicePrincipals/{sp-id}/oauth2PermissionGrants
PowerShell — Service Principal Credential Audit
# Connect to Graph
Connect-MgGraph -Scopes "Application.Read.All","DelegatedPermissionGrant.Read.All"
# Find all service principals with credentials
$sps = Get-MgServicePrincipal -All -Property Id,AppId,DisplayName,ServicePrincipalType,PasswordCredentials,KeyCredentials
$spWithCreds = $sps | Where-Object { $_.PasswordCredentials.Count -gt 0 -or $_.KeyCredentials.Count -gt 0 }
Write-Output "`n[!] Service Principals with active credentials: $($spWithCreds.Count)`n"
foreach ($sp in $spWithCreds) {
Write-Output "=== $($sp.DisplayName) ==="
Write-Output " AppId: $($sp.AppId)"
Write-Output " Type: $($sp.ServicePrincipalType)"
foreach ($secret in $sp.PasswordCredentials) {
Write-Output " [SECRET] KeyId: $($secret.KeyId) | Expires: $($secret.EndDateTime)"
}
foreach ($cert in $sp.KeyCredentials) {
Write-Output " [CERT] Type: $($cert.Type) | Expires: $($cert.EndDateTime)"
}
# Check what app role assignments this SP has been granted
$appRoles = Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $sp.Id
if ($appRoles.Count -gt 0) {
Write-Output " Granted App Roles:"
foreach ($role in $appRoles) {
Write-Output " -> Resource: $($role.ResourceDisplayName) | RoleId: $($role.AppRoleId)"
}
}
Write-Output ""
}
# Find third-party (non-Microsoft) service principals
$thirdParty = $sps | Where-Object {
$_.ServicePrincipalType -eq "Application" -and
$_.AppId -notmatch "^00000"
}
Write-Output "`n[!] Third-party applications in tenant: $($thirdParty.Count)"
$thirdParty | ForEach-Object {
Write-Output " - $($_.DisplayName) (AppId: $($_.AppId))"
}
OAuth consent grants define which permissions users or admins have approved for applications. Delegated grants allow an app to act on behalf of a specific user, while admin consent grants apply tenant-wide. Reviewing these reveals apps with excessive access that may have been granted through social engineering, phishing, or careless admin consent.
Graph API — Consent Grant Enumeration
# List all delegated permission grants (OAuth2 consent grants)
# consentType "AllPrincipals" = admin consent (tenant-wide)
# consentType "Principal" = user consent (specific user)
GET https://graph.microsoft.com/v1.0/oauth2PermissionGrants?$select=id,clientId,consentType,principalId,resourceId,scope
# Get details about a specific grant
GET https://graph.microsoft.com/v1.0/oauth2PermissionGrants/{grant-id}
# List app role assignments (application-level permissions) for all service principals
GET https://graph.microsoft.com/v1.0/servicePrincipals/{sp-id}/appRoleAssignments
PowerShell — Comprehensive Consent Audit
# Connect to Graph
Connect-MgGraph -Scopes "DelegatedPermissionGrant.Read.All","Application.Read.All","Directory.Read.All"
# Get all OAuth2 permission grants
$grants = Get-MgOauth2PermissionGrant -All
Write-Output "=== ADMIN CONSENT GRANTS (Tenant-Wide) ==="
$adminGrants = $grants | Where-Object { $_.ConsentType -eq "AllPrincipals" }
foreach ($grant in $adminGrants) {
$clientSp = Get-MgServicePrincipal -ServicePrincipalId $grant.ClientId
$resourceSp = Get-MgServicePrincipal -ServicePrincipalId $grant.ResourceId
Write-Output "`n App: $($clientSp.DisplayName)"
Write-Output " Resource: $($resourceSp.DisplayName)"
Write-Output " Scopes: $($grant.Scope)"
# Flag dangerous scopes
$dangerousScopes = @("Mail.ReadWrite","Mail.Send","Files.ReadWrite.All",
"Directory.ReadWrite.All","User.ReadWrite.All",
"Sites.ReadWrite.All","MailboxSettings.ReadWrite")
$grantedScopes = $grant.Scope -split " "
$flagged = $grantedScopes | Where-Object { $_ -in $dangerousScopes }
if ($flagged) {
Write-Output " [!!!] DANGEROUS SCOPES: $($flagged -join ', ')"
}
}
Write-Output "`n`n=== USER CONSENT GRANTS (Per-User) ==="
$userGrants = $grants | Where-Object { $_.ConsentType -eq "Principal" }
foreach ($grant in $userGrants) {
$clientSp = Get-MgServicePrincipal -ServicePrincipalId $grant.ClientId
$resourceSp = Get-MgServicePrincipal -ServicePrincipalId $grant.ResourceId
$user = Get-MgUser -UserId $grant.PrincipalId -Property DisplayName,UserPrincipalName
Write-Output "`n App: $($clientSp.DisplayName)"
Write-Output " User: $($user.DisplayName) ($($user.UserPrincipalName))"
Write-Output " Resource: $($resourceSp.DisplayName)"
Write-Output " Scopes: $($grant.Scope)"
}
# Summary statistics
Write-Output "`n`n=== CONSENT SUMMARY ==="
Write-Output " Total grants: $($grants.Count)"
Write-Output " Admin consent: $($adminGrants.Count)"
Write-Output " User consent: $($userGrants.Count)"
Write-Output " Unique apps: $(($grants | Select-Object -Unique ClientId).Count)"
Mail.ReadWrite, Mail.Send, or Files.ReadWrite.All, this may indicate a prior consent phishing attack. An attacker may have tricked an admin into granting permissions to a malicious app, which now has persistent access to tenant data without any credentials stored in the tenant itself.
⚙ Service Configuration Enumeration
Exchange Online is often the most valuable target in an M365 tenant. Enumerating mail flow rules, transport rules, connectors, shared mailboxes, distribution lists, and forwarding rules reveals data exfiltration paths, backdoors left by prior attackers, and misconfigurations that can be exploited for lateral movement or persistence.
Connect to Exchange Online
# Install the Exchange Online Management module if not present
Install-Module -Name ExchangeOnlineManagement -Force
# Connect with interactive authentication
Connect-ExchangeOnline -UserPrincipalName admin@target.com
# Or connect using an access token (if you have one)
Connect-ExchangeOnline -AccessToken $accessToken -Organization target.onmicrosoft.com
Transport Rules (Mail Flow Rules)
Transport rules process messages as they flow through Exchange. Attackers use them to BCC copies of emails, redirect messages, or strip security headers. Existing malicious rules indicate prior compromise.
# List all transport rules (organisation-wide mail flow rules)
Get-TransportRule | Format-List Name, State, Priority, Description, Conditions, Actions
# Look specifically for rules that forward, redirect, or BCC externally
Get-TransportRule | Where-Object {
$_.BlindCopyTo -ne $null -or
$_.RedirectMessageTo -ne $null -or
$_.CopyTo -ne $null
} | Format-List Name, State, BlindCopyTo, RedirectMessageTo, CopyTo
# Check for rules that modify message headers (potential security bypass)
Get-TransportRule | Where-Object {
$_.SetHeaderName -ne $null -or
$_.RemoveHeader -ne $null
} | Format-List Name, State, SetHeaderName, SetHeaderValue, RemoveHeader
Inbox Rules (Per-User Forwarding)
Inbox rules run per-mailbox and can silently forward or delete messages. Attackers commonly create inbox rules to maintain access to sensitive communications or hide evidence of compromise.
# Get inbox rules for a specific user
Get-InboxRule -Mailbox user@target.com | Format-List Name, Enabled, Description, ForwardTo, ForwardAsAttachmentTo, RedirectTo, DeleteMessage, MoveToFolder
# Enumerate inbox rules across ALL mailboxes (requires Exchange Admin role)
$mailboxes = Get-Mailbox -ResultSize Unlimited
foreach ($mbx in $mailboxes) {
$rules = Get-InboxRule -Mailbox $mbx.UserPrincipalName -ErrorAction SilentlyContinue
$suspicious = $rules | Where-Object {
$_.ForwardTo -ne $null -or
$_.ForwardAsAttachmentTo -ne $null -or
$_.RedirectTo -ne $null -or
($_.DeleteMessage -eq $true -and $_.Enabled -eq $true)
}
if ($suspicious) {
Write-Output "`n[!] SUSPICIOUS RULES for $($mbx.UserPrincipalName):"
foreach ($rule in $suspicious) {
Write-Output " Rule: $($rule.Name) (Enabled: $($rule.Enabled))"
if ($rule.ForwardTo) { Write-Output " ForwardTo: $($rule.ForwardTo)" }
if ($rule.ForwardAsAttachmentTo) { Write-Output " ForwardAsAttachment: $($rule.ForwardAsAttachmentTo)" }
if ($rule.RedirectTo) { Write-Output " RedirectTo: $($rule.RedirectTo)" }
if ($rule.DeleteMessage) { Write-Output " [!] DELETES matching messages" }
}
}
}
Mailbox Forwarding (SMTP Level)
Forwarding can also be set at the mailbox level via SMTP, which does not appear in inbox rules. This is a stealthier persistence mechanism.
# Find mailboxes with SMTP forwarding configured
Get-Mailbox -ResultSize Unlimited | Where-Object {
$_.ForwardingSMTPAddress -ne $null -or $_.ForwardingAddress -ne $null
} | Format-Table DisplayName, UserPrincipalName, ForwardingSMTPAddress, ForwardingAddress, DeliverToMailboxAndForward
# Find shared mailboxes (may contain sensitive data, often less monitored)
Get-Mailbox -RecipientTypeDetails SharedMailbox -ResultSize Unlimited |
Format-Table DisplayName, UserPrincipalName, PrimarySmtpAddress
# Enumerate who has access to shared mailboxes
$sharedMbx = Get-Mailbox -RecipientTypeDetails SharedMailbox -ResultSize Unlimited
foreach ($mbx in $sharedMbx) {
$perms = Get-MailboxPermission -Identity $mbx.UserPrincipalName | Where-Object {
$_.User -ne "NT AUTHORITY\SELF" -and $_.IsInherited -eq $false
}
if ($perms) {
Write-Output "`n$($mbx.DisplayName) ($($mbx.UserPrincipalName)):"
$perms | ForEach-Object {
Write-Output " $($_.User) -> $($_.AccessRights -join ', ')"
}
}
}
# Enumerate distribution lists
Get-DistributionGroup -ResultSize Unlimited | Format-Table DisplayName, PrimarySmtpAddress, ManagedBy
Mail Connectors
# Enumerate inbound and outbound connectors
# These control how mail flows to/from external systems
Get-InboundConnector | Format-List Name, Enabled, SenderDomains, SenderIPAddresses, RequireTls, RestrictDomainsToCertificate
Get-OutboundConnector | Format-List Name, Enabled, RecipientDomains, SmartHosts, TlsSettings, RouteAllMessagesViaOnPremises
SharePoint Online and OneDrive for Business often contain the organisation's most sensitive documents: financial reports, HR records, credentials, architecture diagrams, and strategic plans. Discovering accessible sites, their permissions, anonymous sharing links, and sensitive document libraries is essential for demonstrating data exposure risk.
Graph API — SharePoint Site Discovery
# Search for all SharePoint sites (requires Sites.Read.All)
GET https://graph.microsoft.com/v1.0/sites?search=*&$select=id,displayName,webUrl,description&$top=999
# Get a specific site by URL path
GET https://graph.microsoft.com/v1.0/sites/target.sharepoint.com:/sites/HR
# List document libraries in a site
GET https://graph.microsoft.com/v1.0/sites/{site-id}/drives?$select=id,name,description,webUrl
# List items in a document library
GET https://graph.microsoft.com/v1.0/drives/{drive-id}/root/children?$select=name,size,webUrl,lastModifiedDateTime,file
# Search across all SharePoint content for sensitive terms
GET https://graph.microsoft.com/v1.0/search/query
POST body:
{
"requests": [{
"entityTypes": ["driveItem"],
"query": { "queryString": "password OR credentials OR secret OR API key" },
"from": 0,
"size": 25
}]
}
PnP PowerShell — Detailed Site Enumeration
# Install PnP PowerShell module
Install-Module -Name PnP.PowerShell -Force
# Connect to SharePoint Online admin center
Connect-PnPOnline -Url "https://target-admin.sharepoint.com" -Interactive
# Enumerate all site collections
Get-PnPTenantSite -Detailed | Format-Table Url, Title, Template, SharingCapability, Owner, StorageUsageCurrent
# Check for sites with external sharing enabled
Get-PnPTenantSite | Where-Object {
$_.SharingCapability -ne "Disabled"
} | Format-Table Url, Title, SharingCapability
# Connect to a specific site for deeper enumeration
Connect-PnPOnline -Url "https://target.sharepoint.com/sites/HR" -Interactive
# List all document libraries
Get-PnPList | Where-Object { $_.BaseTemplate -eq 101 } | Format-Table Title, ItemCount, LastItemModifiedDate
# Search for sensitive files
$sensitiveFiles = Find-PnPFile -Match "*.xlsx" -List "Documents"
$sensitiveFiles += Find-PnPFile -Match "*password*" -List "Documents"
$sensitiveFiles += Find-PnPFile -Match "*credential*" -List "Documents"
$sensitiveFiles | Format-Table Name, ServerRelativeUrl, TimeLastModified
# Check for anonymous sharing links on a site
Get-PnPFileSharingLink -FileUrl "/sites/HR/Shared Documents/Salaries.xlsx"
SharePoint Online Management Shell
# Connect using the SPO Management Shell
Connect-SPOService -Url "https://target-admin.sharepoint.com"
# Enumerate all site collections with sharing details
Get-SPOSite -Limit All -Detailed | Select-Object Url, Title, Owner, SharingCapability,
ConditionalAccessPolicy, StorageUsageCurrent, LastContentModifiedDate |
Export-Csv -Path sharepoint_sites.csv -NoTypeInformation
# Check tenant-level sharing settings
Get-SPOTenant | Select-Object SharingCapability, DefaultSharingLinkType,
PreventExternalUsersFromResharing, RequireAcceptingAccountMatchInvitedAccount,
ExternalUserExpirationRequired, ExternalUserExpireInDays
# Find OneDrive sites (each user has one)
Get-SPOSite -IncludePersonalSite $true -Limit All -Filter "Url -like '-my.sharepoint.com/personal'" |
Format-Table Url, Owner, StorageUsageCurrent
Microsoft Teams is deeply integrated with SharePoint, Exchange, and Entra ID. Enumerating Teams reveals organisational structure, communication channels, guest access configurations, and potential data exposure through shared files and chat history. Guest users in Teams can access channel content, files, and sometimes sensitive conversations.
Graph API — Teams Enumeration
# List all teams in the tenant (requires Group.Read.All)
# Teams are Microsoft 365 Groups with the 'Team' resource provisioning option
GET https://graph.microsoft.com/v1.0/groups?$filter=resourceProvisioningOptions/Any(x:x eq 'Team')&$select=id,displayName,description,visibility,mailNickname&$top=999
# Get details about a specific team
GET https://graph.microsoft.com/v1.0/teams/{team-id}?$select=displayName,description,isArchived,memberSettings,messagingSettings,guestSettings
# List channels in a team
GET https://graph.microsoft.com/v1.0/teams/{team-id}/channels?$select=id,displayName,description,membershipType
# List members of a team (includes guests)
GET https://graph.microsoft.com/v1.0/teams/{team-id}/members?$select=displayName,email,roles
# Get messages from a channel (requires ChannelMessage.Read.All)
GET https://graph.microsoft.com/v1.0/teams/{team-id}/channels/{channel-id}/messages?$top=50
# List all tabs in a channel (may reveal linked apps, SharePoint, Planner, etc.)
GET https://graph.microsoft.com/v1.0/teams/{team-id}/channels/{channel-id}/tabs?$select=displayName,webUrl,configuration
PowerShell — Teams Enumeration
# Using Microsoft Graph PowerShell SDK
Connect-MgGraph -Scopes "Group.Read.All","Team.ReadBasic.All","TeamMember.Read.All","Channel.ReadBasic.All"
# List all teams
$teams = Get-MgGroup -Filter "resourceProvisioningOptions/Any(x:x eq 'Team')" -All -Property Id,DisplayName,Description,Visibility
Write-Output "Total Teams found: $($teams.Count)`n"
foreach ($team in $teams) {
Write-Output "=== $($team.DisplayName) ==="
Write-Output " Visibility: $($team.Visibility)"
Write-Output " Description: $($team.Description)"
# Get team members and identify guests
$members = Get-MgTeamMember -TeamId $team.Id -All
$guests = $members | Where-Object {
$_.AdditionalProperties.'@odata.type' -eq '#microsoft.graph.aadUserConversationMember' -and
$_.AdditionalProperties.email -notlike "*@target.com"
}
Write-Output " Members: $($members.Count) (Guests: $($guests.Count))"
if ($guests.Count -gt 0) {
Write-Output " [!] GUEST USERS:"
foreach ($guest in $guests) {
Write-Output " - $($guest.AdditionalProperties.displayName) ($($guest.AdditionalProperties.email))"
}
}
# List channels
$channels = Get-MgTeamChannel -TeamId $team.Id -All
Write-Output " Channels: $($channels.Count)"
foreach ($ch in $channels) {
$type = if ($ch.MembershipType -eq "private") { "[PRIVATE]" }
elseif ($ch.MembershipType -eq "shared") { "[SHARED]" }
else { "[STANDARD]" }
Write-Output " $type $($ch.DisplayName)"
}
Write-Output ""
}
# Using the MicrosoftTeams PowerShell module for admin-level queries
Install-Module -Name MicrosoftTeams -Force
Connect-MicrosoftTeams
# Get Teams-level configuration policies
Get-CsTeamsClientConfiguration | Format-List AllowGuestUser, AllowDropBox, AllowBox,
AllowGoogleDrive, AllowShareFile, AllowEgnyte
# Check guest access settings
Get-CsTeamsGuestMeetingConfiguration | Format-List AllowIPVideo, ScreenSharingMode, AllowMeetNow
Get-CsTeamsGuestMessagingConfiguration | Format-List AllowUserEditMessage, AllowUserDeleteMessage,
AllowUserChat, AllowGiphy, AllowMemes, AllowStickers
# Check external access (federation) settings
Get-CsExternalAccessPolicy | Format-List Identity, EnableFederationAccess,
EnableOutsideAccess, EnablePublicCloudAccess
# Check messaging policies
Get-CsTeamsMessagingPolicy | Format-List Identity, AllowUrlPreviews, AllowOwnerDeleteMessage,
AllowUserEditMessage, AllowUserDeleteMessage, AllowUserChat, AllowRemoveUser
Teams Data Exposure Checklist
- Guest users in sensitive teams: Check if guest (external) users have been added to teams containing confidential information
- Public teams with sensitive data: Teams set to "Public" visibility are joinable by any tenant member without approval
- Shared channels: Shared channels can include users from external tenants, creating cross-organisation data exposure
- Chat history and file shares: Teams channels automatically store shared files in the associated SharePoint site
- Connected third-party apps: Tabs and connectors in channels may expose data to third-party services
- Webhook URLs in channels: Incoming webhook URLs, if exposed, allow anyone to post messages to a channel