13.5 Wave 1: Sign-In Log Investigation

2–3 hours · Module 13

Sign-In Log Investigation

You have 5 users who clicked through to the AiTM proxy page. Now determine which ones actually entered credentials and which have active token replay.

Step 1: Check interactive sign-ins for the 5 clickers

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
let clicked_users = dynamic([
    "a.chen@northgateeng.com",
    "m.patel@northgateeng.com",
    "j.morrison@northgateeng.com",
    "s.williams@northgateeng.com",
    "r.thompson@northgateeng.com"
]);
SigninLogs
| where TimeGenerated between (datetime(2026-02-27T09:10:00Z) .. datetime(2026-02-27T10:00:00Z))
| where UserPrincipalName in~ (clicked_users)
| project TimeGenerated, UserPrincipalName, ResultType, IPAddress,
    AppDisplayName, ConditionalAccessStatus,
    tostring(LocationDetails.countryOrRegion),
    tostring(LocationDetails.city),
    tostring(MfaDetail.authMethod),
    tostring(DeviceDetail.browser),
    tostring(DeviceDetail.operatingSystem),
    tostring(DeviceDetail.isCompliant)
| sort by TimeGenerated asc

Result: All 5 users show successful interactive sign-ins from London IPs (their normal location) between 09:17 and 09:25. MFA was satisfied via Microsoft Authenticator push. Conditional access status: success. Device compliance: not evaluated (no compliance policy in scope).

The interactive sign-in looks completely normal

This is the defining challenge of AiTM investigation. The interactive sign-in passes through the proxy but appears to come from the user's real IP, real device, real location. MFA is satisfied. Conditional access passes. Nothing in the interactive sign-in screams "compromise." The signal is in the non-interactive logs — the token replay from a different IP.

Step 2: Check non-interactive sign-ins — the token replay check

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let clicked_users = dynamic([
    "a.chen@northgateeng.com",
    "m.patel@northgateeng.com",
    "j.morrison@northgateeng.com",
    "s.williams@northgateeng.com",
    "r.thompson@northgateeng.com"
]);
let known_ips =
    SigninLogs
    | where TimeGenerated > ago(30d)
    | where UserPrincipalName in~ (clicked_users)
    | where ResultType == 0
    | distinct UserPrincipalName, IPAddress;
AADNonInteractiveUserSignInLogs
| where TimeGenerated between (datetime(2026-02-27T09:10:00Z) .. datetime(2026-02-27T12:00:00Z))
| where UserPrincipalName in~ (clicked_users)
| where ResultType == 0
| join kind=anti known_ips on UserPrincipalName, IPAddress
| extend Country = tostring(LocationDetails.countryOrRegion)
| extend City = tostring(LocationDetails.city)
| project TimeGenerated, UserPrincipalName, IPAddress, Country, City,
    AppDisplayName, ResourceDisplayName
| sort by UserPrincipalName, TimeGenerated asc

Result:

UserAnomalous IP?Detail
a.chen@NoAll non-interactive from London IPs
m.patel@NoAll non-interactive from London IPs
j.morrison@YesNon-interactive from 203.0.113.45 (Lagos, Nigeria) starting 09:22
s.williams@NoAll non-interactive from London IPs
r.thompson@NoAll non-interactive from London IPs

One confirmed compromise: j.morrison. The other 4 users either closed the proxy page before entering credentials, or the proxy failed to capture a complete token.

Step 3: Deep-dive on the compromised account

1
2
3
4
5
6
7
8
9
AADNonInteractiveUserSignInLogs
| where TimeGenerated between (datetime(2026-02-27T09:00:00Z) .. datetime(2026-02-27T12:00:00Z))
| where UserPrincipalName =~ "j.morrison@northgateeng.com"
| where IPAddress == "203.0.113.45"
| project TimeGenerated, AppDisplayName, ResourceDisplayName,
    tostring(LocationDetails.countryOrRegion),
    tostring(LocationDetails.city),
    ResultType, UserAgent
| sort by TimeGenerated asc

Result: 14 non-interactive sign-in events from the attacker IP between 09:22 and 09:47 (25-minute window). Applications accessed: Exchange Online (email), Microsoft Graph API (broad access).

The attacker’s access pattern: token refresh every 2-3 minutes against Exchange Online, consistent with automated mailbox enumeration. The Graph API access suggests the attacker was also checking what other resources the token could reach (SharePoint, OneDrive, Teams).

Step 4: Check conditional access evaluation on the attacker’s sign-in

1
2
3
4
5
6
7
8
9
AADNonInteractiveUserSignInLogs
| where TimeGenerated between (datetime(2026-02-27T09:20:00Z) .. datetime(2026-02-27T09:30:00Z))
| where UserPrincipalName =~ "j.morrison@northgateeng.com"
| where IPAddress == "203.0.113.45"
| mv-expand CAPolicy = parse_json(ConditionalAccessPolicies)
| extend PolicyName = tostring(CAPolicy.displayName)
| extend PolicyResult = tostring(CAPolicy.result)
| extend GrantControls = tostring(CAPolicy.enforcedGrantControls)
| project TimeGenerated, PolicyName, PolicyResult, GrantControls

Result: The “Require MFA for all users” policy evaluated as success — the stolen token already contained the MFA claim. No policy requiring device compliance was in scope for this application.

This is the gap that allowed the attack

If a conditional access policy had required a compliant device for Exchange Online, this sign-in would have been blocked. The attacker's device is not Intune-enrolled. The token's MFA claim is irrelevant when device compliance is the enforcement point. This finding goes directly into the hardening recommendations (subsection 13.12).

Try it yourself

Write a query that checks whether any of the other 4 users (a.chen, m.patel, s.williams, r.thompson) show non-interactive sign-in activity from the attacker IP (203.0.113.45) in the week following the attack. The attacker may have captured tokens for other users but not used them immediately.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
let other_users = dynamic([
    "a.chen@northgateeng.com",
    "m.patel@northgateeng.com",
    "s.williams@northgateeng.com",
    "r.thompson@northgateeng.com"
]);
AADNonInteractiveUserSignInLogs
| where TimeGenerated between (datetime(2026-02-27T09:00:00Z) .. datetime(2026-03-06T00:00:00Z))
| where UserPrincipalName in~ (other_users)
| where IPAddress == "203.0.113.45"
| project TimeGenerated, UserPrincipalName, IPAddress, AppDisplayName, ResultType

If this returns no results, the attacker did not successfully capture tokens for these users. If it returns results days later, the attacker may have stored tokens and used them after a delay — a more sophisticated pattern.

Check your understanding

1. 5 users clicked the phishing link but only 1 shows token replay. What are the most likely explanations for the other 4?

The proxy was down for 4 of them
They reached the proxy page but either closed it without entering credentials, entered incorrect credentials, or the proxy failed to complete the MFA relay for their session
Their tokens expired before the attacker could use them

2. The attacker's sign-in passed the "Require MFA" conditional access policy. Why?

The stolen token already contained the MFA claim from the victim's legitimate authentication through the proxy. Conditional access saw valid MFA satisfaction and allowed the session.
The attacker completed their own MFA
MFA was not required for this application