Phase 4 Checklist

0 / 9 completed
Prerequisite: You must have a valid access token or authenticated session before proceeding. This phase assumes you have completed Phase 2 (Initial Access) or Phase 3 (Credential Attacks) and hold at least a standard user credential or a stolen access token with Graph API scopes.

📂 Entra ID Enumeration

1 User & Group 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.

GraphRunner ROADtools AzureHound Microsoft.Graph

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.

HTTP
# 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.

PowerShell
# 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.

Bash
# 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.

Bash
# 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.

PowerShell
# 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
💡
Tip: Even with a low-privilege standard user account, you can typically enumerate all users and groups in the tenant. By default, Entra ID allows all members to read directory information. This is controlled by the "User can read other users' properties" tenant setting, which is enabled by default in most organisations.
🛡
Defensive Note: To limit enumeration by standard users, configure restrictive settings under Entra ID > User Settings > "Restrict access to Microsoft Entra admin center" and consider setting BlockMsolPowerShell to true. Note that Graph API access is harder to fully restrict without impacting legitimate operations.
2 Role & Admin Enumeration

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.

Microsoft.Graph GraphRunner ROADtools

Graph API — Directory Role Enumeration

HTTP
# 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

PowerShell
# 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
PIM Considerations: Organisations using Privileged Identity Management (PIM) may have zero standing Global Admins. Users activate roles on demand for a limited duration. Check eligible assignments, not just active ones. A user with a PIM-eligible Global Admin role is just one approval step away from full tenant control.
3 Conditional Access Policy Enumeration

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.

Microsoft.Graph ROADtools ConditionalAccessDocumentation

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.

HTTP
# 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

PowerShell
# 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.

PowerShell
# 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
🚨
Critical Finding: If you discover a break-glass account excluded from all CA policies, attempt to determine if it uses a weak or default password. These accounts are designed to bypass every security control and provide unrestricted Global Admin access.

📦 Application & Service Principal Enumeration

4 App Registration 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.

Microsoft.Graph GraphRunner ROADtools

Graph API — Enumerate App Registrations

HTTP
# 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

PowerShell
# 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
💡
Key Insight: If a standard user is listed as an owner of an app registration that has high-privilege Application permissions, that user can add a new client secret, authenticate as the app, and exercise those permissions. This is a common privilege escalation path. Look for app registrations owned by non-admin users that have permissions like RoleManagement.ReadWrite.Directory.
5 Service Principal Analysis

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.

Microsoft.Graph ROADtools AzureHound

Graph API — Service Principal Enumeration

HTTP
# 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

PowerShell
# 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))"
}
Lateral Movement Opportunity: Service principals with active credentials and high-privilege app role assignments are equivalent to privileged service accounts. If you can obtain or create a secret for such a service principal, you can authenticate as it and exercise its permissions without any user context or MFA requirement. Application permissions bypass Conditional Access policies entirely.
6 OAuth Consent Grants Review

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.

Microsoft.Graph ROADtools GraphRunner

Graph API — Consent Grant Enumeration

HTTP
# 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

PowerShell
# 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)"
🚨
Illicit Consent Grants: If you find an unknown third-party application with admin-consented permissions like 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.
🛡
Defensive Note: Restrict user consent to verified publishers or disable it entirely under Entra ID > Enterprise Applications > Consent and Permissions. Enable the admin consent request workflow so users can request permissions through a governed process. Regularly review granted permissions using the Enterprise Applications > Permissions blade.

⚙ Service Configuration Enumeration

7 Exchange Online Configuration

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.

ExchangeOnlineManagement Microsoft.Graph

Connect to Exchange Online

PowerShell
# 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.

PowerShell
# 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.

PowerShell
# 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.

PowerShell
# 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

PowerShell
# 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
🚨
Indicators of Prior Compromise: If you find transport rules that BCC all email to an external address, inbox rules that delete security notifications, or SMTP forwarding to unknown domains, the tenant may have been previously compromised. Document these findings as critical and recommend immediate incident response.
8 SharePoint & OneDrive Enumeration

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.

PnP.PowerShell Microsoft.Graph SPO Management Shell

Graph API — SharePoint Site Discovery

HTTP
# 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

PowerShell
# 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

PowerShell
# 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
💡
Quick Win: Many organisations have a SharePoint site named "IT", "Infra", "Wiki", or "Helpdesk" that contains network diagrams, runbooks with credentials, and infrastructure documentation. As a standard user, you can often access these sites directly. Search for files containing "password", "credential", "API key", or "connection string" across all accessible sites.
🛡
Defensive Note: Audit sharing settings at the tenant and site level. Disable anonymous sharing links where not required. Use Microsoft Purview sensitivity labels to classify and protect sensitive documents. Regularly review external sharing reports in the SharePoint admin center. Consider implementing site-level Conditional Access policies for high-sensitivity sites.
9 Teams Configuration

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.

Microsoft.Graph MicrosoftTeams

Graph API — Teams Enumeration

HTTP
# 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

PowerShell
# 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
Common Finding: Many organisations create Teams for projects and add external contractors as guests. These guests often retain access long after the project ends. Check for stale guest accounts with access to Teams containing current sensitive information. Also check whether guest users can discover and join public teams without admin approval.
🛡
Defensive Note: Implement regular access reviews for guest users in Teams. Configure Teams policies to restrict guest capabilities (disable file sharing, limit channel creation). Use sensitivity labels to prevent guests from being added to highly sensitive teams. Set expiration policies for guest accounts and review the external collaboration settings in Entra ID.