In this section

TH4.5 Hunting MFA Fatigue Attacks

6-8 hours · Module 4
What you already know

The previous section covered hunting password spray. This section covers hunting mfa fatigue attacks.

The attacker has the password, they need the push approval

MFA fatigue (T1621. Multi-Factor Authentication Request Generation) is the bridge between a compromised password and full account access. The attacker obtained the password through spray (TH4.4), stuffing (TH4.3), phishing, or purchase on a dark web market. They submit it to the Entra ID login endpoint. MFA challenges the user via push notification. The user denies. The attacker submits again. Another push. And again. After 20 notifications in 10 minutes, the user, tired, confused, or assuming it is a system glitch, approves.

Anti-Pattern

Hunting hunting mfa fatigue attacks without a hypothesis

The hunter opens Advanced Hunting and starts writing queries without a clear hypothesis. They find interesting data but cannot determine whether it represents a threat, a misconfiguration, or normal activity. Every hunt starts with a hypothesis: a specific, testable statement about attacker behavior. Without a hypothesis, you are exploring, not hunting. Exploration has value, but it produces findings you cannot action without additional scoping.

This technique was used in the 2022 Uber breach (Lapsus$) and the 2022 Cisco breach (Yanluowang). It is effective because it exploits human behavior, not a technical vulnerability. Microsoft has since introduced number matching for Authenticator push notifications, which requires the user to enter a displayed number rather than just tapping "Approve." Number matching significantly reduces fatigue effectiveness, but it must be enabled, and not all organizations have enabled it.

Detecting rapid MFA prompts

// MFA Fatigue Hunt — repeated authentication attempts indicating
// rapid MFA push bombardment
let fatigueWindow = 30m;
let minPrompts = 5;
SigninLogs
| where TimeGenerated > ago(24h)
| where ResultType in ("50074", "50076", "500121",
    "53003", "50079")
// 50074: Strong auth required (MFA challenge issued)
// 50076: MFA required — password correct, awaiting second factor
// 500121: Authentication failed during MFA challenge
// 53003: Blocked by conditional access (MFA not completed)
// 50079: User needs to register for MFA
| summarize
    Prompts = count(),
    SourceIPs = make_set(IPAddress, 10),
    IPCount = dcount(IPAddress),
    Errors = make_set(ResultType, 10),
    Apps = make_set(AppDisplayName, 5),
    TimeSpan = datetime_diff(
        'minute', max(TimeGenerated), min(TimeGenerated)),
    FirstAttempt = min(TimeGenerated),
    LastAttempt = max(TimeGenerated)
    by UserPrincipalName, bin(TimeGenerated, fatigueWindow)
| where Prompts >= minPrompts
// 5+ MFA prompts in 30 minutes for one user = fatigue candidate
| extend PromptsPerMinute = round(
    todouble(Prompts) / todouble(TimeSpan + 1), 2)
// High prompts-per-minute from a single IP = automated fatigue tool
// Lower rate from multiple IPs = less certain (could be user
//   on multiple devices genuinely failing MFA)
| extend HourOfDay = hourofday(FirstAttempt)
| extend IsAfterHours = HourOfDay < 7 or HourOfDay > 19
// After-hours MFA fatigue is higher confidence — the user is
//   unlikely to be actively authenticating at 3 AM
| sort by Prompts desc

Correlating fatigue with successful approval

The fatigue attack succeeds when the user approves a push after the bombardment. The critical signal: many MFA failures followed by a success from a different IP.

// Did the user approve after the bombardment?
let fatigueUsers = SigninLogs
| where TimeGenerated > ago(24h)
| where ResultType in ("50074", "50076", "500121", "53003")
| summarize Failures = count() by UserPrincipalName,
    bin(TimeGenerated, 30m)
| where Failures >= 5
| distinct UserPrincipalName;
// Find successful sign-ins for fatigue-targeted users
SigninLogs
| where TimeGenerated > ago(24h)
| where UserPrincipalName in (fatigueUsers)
| where ResultType == "0"  // Successful authentication
| extend MFAMethod = tostring(
    parse_json(AuthenticationDetails)[1].authenticationMethod)
| extend Country = tostring(LocationDetails.countryOrRegion)
| project TimeGenerated, UserPrincipalName, IPAddress,
    AppDisplayName, MFAMethod, Country,
    DeviceOS = tostring(DeviceDetail.operatingSystem)
// Cross-reference: is this success IP in the user's baseline?
// If the success came from a non-baseline IP AFTER a fatigue
//   bombardment, the user approved the attacker's push
// This is a confirmed compromise — escalate immediately
| sort by TimeGenerated asc

Number matching effectiveness check

// Is number matching enabled? Check MFA method distribution
SigninLogs
| where TimeGenerated > ago(7d)
| where ResultType == "0"
| extend MFAMethod = tostring(
    parse_json(AuthenticationDetails)[1].authenticationMethod)
| where isnotempty(MFAMethod)
| summarize Users = dcount(UserPrincipalName),
    SignIns = count()
    by MFAMethod
| sort by Users desc
// "Microsoft Authenticator (push notification)" without number
//   matching is vulnerable to fatigue
// "Microsoft Authenticator (number matching)" is resistant
// "FIDO2 security key" and "Passwordless phone sign-in" are
//   immune to fatigue (no push to approve)
// If push-without-number-matching is the dominant method,
//   your organization is vulnerable to fatigue attacks
MFA FATIGUE — ATTACK TIMELINE AND DETECTION POINTS PASSWORD CORRECT 50076: MFA required PUSH BOMBARDMENT 5-50 prompts in 30 min USER APPROVES Exhaustion or confusion FULL ACCESS — COMPROMISED ResultType 0 from attacker IP DETECT HERE: 5+ MFA failures in 30 min for one user Alert BEFORE the user approves — prevent the compromise DETECT HERE: Success from non-baseline IP after failures Compromise already occurred — contain immediately Detecting during bombardment prevents the compromise. Detecting after approval contains it. Both detection points matter — the hunt should check for both patterns.

Figure TH4.5. MFA fatigue timeline. Two detection opportunities: during the bombardment (preventive) and after the approval (reactive).

Run the fatigue detection query (24-hour window, 5-prompt minimum). Do any users show 5+ MFA prompts in a 30-minute window?

If yes, run the approval correlation query. Did any of those users subsequently authenticate successfully? If the successful auth came from a non-baseline IP (cross-reference TH4.1), the fatigue attack may have succeeded.

Run the number matching check. What percentage of your users authenticate with push-without-number-matching versus number-matching or FIDO2? If push-without-number-matching dominates, your organization is vulnerable to fatigue, recommend enabling number matching as a mitigation.

Compliance Context

Number matching makes fatigue significantly harder: the user must enter a two-digit number displayed on the sign-in screen, not just tap "Approve." But it does not eliminate the risk entirely. A user who receives 20 MFA prompts at 3 AM may call the helpdesk, and a socially engineered helpdesk agent could read the number to the user ("just enter 42 to stop the notifications"). Additionally, not all MFA methods support number matching. SMS OTP, phone call, and hardware OATH tokens are unaffected by the number matching setting. Hunting for fatigue patterns identifies: (1) whether fatigue attacks are being attempted against your organization, (2) which users are targeted (they likely have compromised passwords), and (3) whether any fatigue attempts succeeded despite number matching.

Extend this hunt

Correlate MFA fatigue targets with TH4.3 (credential stuffing) and TH4.4 (password spray) results. A user who appears in both the spray success list (password matched) and the fatigue target list is under active, multi-technique attack. The attacker first sprayed or stuffed to find valid passwords, then used fatigue to bypass MFA. This multi-technique chain, spray → fatigue → access, is the standard playbook for initial access brokers. Finding both patterns for the same user confirms a coordinated campaign rather than opportunistic probing.

Checkpoint

Hunt window: ___ to ___

Users with 5+ MFA prompts in 30-min window: ___

After-hours fatigue attempts: ___

Fatigue-targeted users with subsequent success: ___

Successes from non-baseline IPs (confirmed compromise): ___

MFA method distribution:

Push (no number matching): ___% of users

Number matching: ___% of users

FIDO2/Passwordless: ___% of users

Recommendation: Enable number matching ☐ Already enabled ☐

References Used in This Subsection

NE environmental considerations

NE's detection environment includes specific factors that influence this rule's operation:

Device diversity: 768 P2 corporate workstations with full Defender for Endpoint telemetry, 58 P1 manufacturing workstations with basic cloud-delivered protection, and 3 RHEL rendering servers with Syslog-only coverage. Rules targeting DeviceProcessEvents operate with full fidelity on P2 devices but may have reduced visibility on P1 devices. Manufacturing workstations in Sheffield and Sunderland represent a detection gap for endpoint-level detections.

Network topology: 11 offices connected via Palo Alto SD-WAN with full-mesh connectivity. The SD-WAN firewall logs feed CommonSecurityLog in Sentinel. Cross-site lateral movement generates firewall allow events that correlate with DeviceLogonEvents, enabling multi-source detection that single-table rules cannot achieve.

User population: 810 users with distinct behavioral profiles, office workers (predictable hours, consistent applications), field engineers (variable hours, travel patterns), IT administrators (elevated privilege, broad access patterns), and manufacturing operators (fixed shifts, limited application access). Each user population has different detection baselines.