Phishing investigation and AiTM detection¶
Two hunts: email header analysis for a reported phishing message, and Entra sign-in log analysis to detect AiTM (adversary-in-the-middle) token theft where MFA was bypassed through a reverse proxy.
AiTM phishing does not defeat MFA cryptographically. It proxies the authentication in real time: the victim authenticates to the proxy, the proxy forwards credentials and the TOTP code to the real service, and receives a valid session token that is then used independently. Detection is at the sign-in log layer, not the authentication layer: the session token issued to the legitimate user and the session used from the attacker’s IP are the same token, used from different locations.
Data sources: raw message files in .eml format, retrieved from the quarantine or the
user’s mailbox; Microsoft Entra ID sign-in logs via the Microsoft Graph PowerShell
module (Connect-MgGraph -Scopes "AuditLog.Read.All").
Email header analysis for a reported phishing message¶
Hypothesis: a user has forwarded a suspicious email. Extracting the delivery path, sender authentication results, and embedded URLs is the triage starting point.
#!/usr/bin/env python3
# header-triage.py: print delivery path and authentication results from an .eml file
import sys, email, email.policy
with open(sys.argv[1], 'rb') as f:
msg = email.message_from_binary_file(f, policy=email.policy.default)
for hdr in msg.get_all('Received', []):
print('Received:', hdr.strip().replace('\n', ' ')[:200])
for hdr in msg.get_all('Authentication-Results', []):
print('Auth-Results:', hdr.strip()[:400])
print('From: ', msg['From'])
print('Reply-To: ', msg['Reply-To'])
print('Return-Path: ', msg['Return-Path'])
print('X-Originating: ', msg.get('X-Originating-IP', 'not present'))
# extract all URLs from an .eml body (text/plain and text/html parts)
python3 - "$1" <<'EOF'
import sys, email, email.policy, re
with open(sys.argv[1], 'rb') as f:
msg = email.message_from_binary_file(f, policy=email.policy.default)
body = ''
for part in msg.walk():
if part.get_content_type() in ('text/plain', 'text/html'):
body += part.get_content()
for u in sorted(set(re.findall(r'https?://[^\s"<>]+', body))):
print(u)
EOF
SPF and DKIM results in Authentication-Results indicate whether the sending domain
authenticated successfully. A pass on both for a domain an organisation does not control
is expected for attacker-owned lookalike domains: the attacker controls
company-invoices.net, has valid SPF and DKIM, and the checks pass. DMARC alignment is
the relevant field: does the authenticated domain align with the header From address the
recipient saw?
A Received chain that terminates at an IP outside the sending domain’s expected sending
ranges, or that shows a gap consistent with a proxy insertion, warrants further
investigation.
AiTM token theft detection in Entra sign-in logs¶
Hypothesis: a user authenticated successfully through a reverse proxy. The session token was replayed from a different IP address. The sign-in log shows a successful authentication, but subsequent token use originates from an address that does not match the authentication event.
$startTime = (Get-Date).AddDays(-7).ToUniversalTime().ToString('o')
$upn = 'user@example.com'
# sign-in events for the user: look at IP, MFA requirement, and status
Get-MgAuditLogSignIn -Filter "userPrincipalName eq '$upn' and createdDateTime ge $startTime" |
Select-Object CreatedDateTime, IpAddress, AppDisplayName, ClientAppUsed,
@{N='MFA'; E={$_.AuthenticationRequirement}},
@{N='Result'; E={$_.Status.ErrorCode}},
@{N='Detail'; E={$_.Status.AdditionalDetails}},
ConditionalAccessStatus |
Sort-Object CreatedDateTime
# compare against a baseline window: surface IPs not seen in the prior 30 days
$baseline = (Get-Date).AddDays(-30).ToUniversalTime().ToString('o')
$window = (Get-Date).AddDays(-7).ToUniversalTime().ToString('o')
$upn = 'user@example.com'
$knownIPs = Get-MgAuditLogSignIn -Filter (
"userPrincipalName eq '$upn' and " +
"createdDateTime ge $baseline and createdDateTime lt $window"
) | Select-Object -ExpandProperty IpAddress | Sort-Object -Unique
Get-MgAuditLogSignIn -Filter (
"userPrincipalName eq '$upn' and " +
"createdDateTime ge $window and status/errorCode eq 0"
) |
Where-Object { $_.IpAddress -notin $knownIPs } |
Select-Object CreatedDateTime, IpAddress, AppDisplayName, AuthenticationRequirement |
Sort-Object CreatedDateTime
A successful sign-in from an IP with no prior history for that user, where
AuthenticationRequirement shows MFA was satisfied, is consistent with AiTM token theft
but not conclusive on its own. Confidence increases when the IP resolves to a hosting
provider rather than a residential or corporate range, subsequent API activity from that
IP is inconsistent with the user’s normal behaviour, or a forwarding rule or OAuth
consent event appears in the minutes following the sign-in.
The Evilginx and Modlishka proxy frameworks produce exactly this pattern: a successful TOTP-based authentication followed by session token replay from infrastructure the defender does not recognise.