Detection rules › Kusto

MFA Fatigue (OKTA)

Status
available
Severity
medium
Time window
1h
Group by
TimeGenerated, actor_alternateId_s, actor_displayName_s, authenticationContext_externalSessionId_s, outcome_result_s
Source
github.com/Azure/Azure-Sentinel

MFA fatigue attack is a cybersecurity threat where attackers exploit user exhaustion from multi-factor authentication prompts to trick them into providing their MFA details thus compromising their own security. The query identifies MFA fatigue attempts in the Okta data. Ref: https://www.okta.com/blog/identity-security/mfa-fatigue-growing-security-concern/.

MITRE ATT&CK coverage

Event coverage

Rules detecting the same action

Other rules on this platform that filter on the same API call or operation.

Rule body kusto

id: c2697b81-7fe9-4f57-ba1d-de46c6f91f9c
name: MFA Fatigue (OKTA)
description: |
    'MFA fatigue attack is a cybersecurity threat where attackers exploit user exhaustion from multi-factor authentication prompts to trick them into providing their MFA details thus compromising their own security. The query identifies MFA fatigue attempts in the Okta data. 
     Ref: https://www.okta.com/blog/identity-security/mfa-fatigue-growing-security-concern/.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: OktaSSO
    dataTypes:
      - Okta_CL
  - connectorId: OktaSSOv2
    dataTypes:
      - OktaSSO
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CredentialAccess
relevantTechniques:
  - T1621
query: |
  let PushThreshold = 10;
  OktaSSO
  | where ((eventType_s =="user.authentication.auth_via_mfa" and column_ifexists('debugContext_debugData_factor_s', '') == "OKTA_VERIFY_PUSH") or eventType_s == "system.push.send_factor_verify_push" or eventType_s == "user.mfa.okta_verify.deny_push") 
  | summarize IPAddress = make_set(client_ipAddress_s,100), City = make_set(client_geographicalContext_city_s,100),
            successes = countif(eventType_s == "user.authentication.auth_via_mfa"),
            denies = countif(eventType_s == "user.mfa.okta_verify.deny_push"),
            pushes = countif(eventType_s == "system.push.send_factor_verify_push") by TimeGenerated, authenticationContext_externalSessionId_s, actor_alternateId_s,actor_displayName_s, outcome_result_s 
  | summarize lasttime = max(TimeGenerated), firsttime = min(TimeGenerated),
            successes = sum(successes), failures = sum(denies), pushes = sum(pushes) by  authenticationContext_externalSessionId_s, actor_alternateId_s,actor_displayName_s, outcome_result_s 
  | extend seconds = lasttime - firsttime
  | where pushes >  (PushThreshold)
  | extend totalattempts = successes + failures
  | extend finding = case(
              failures == pushes and pushes > 1, "Authentication attempts not successful because multiple pushes denied",
              totalattempts == 0, "Multiple pushes sent and ignored",
              successes > 0 and pushes > 3, "Multiple pushes sent, eventual successful authentication!",
              "Normal authentication pattern")
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: actor_alternateId_s
      - identifier: DisplayName
        columnName: actor_displayName_s
version: 1.1.2
kind: Scheduled

Stages and Predicates

Parameters

let PushThreshold = 10;

Stage 1: source

OktaSSO

Stage 2: where

| where ((eventType_s =="user.authentication.auth_via_mfa" and column_ifexists('debugContext_debugData_factor_s', '') == "OKTA_VERIFY_PUSH") or eventType_s == "system.push.send_factor_verify_push" or eventType_s == "user.mfa.okta_verify.deny_push")

Stage 3: summarize

| summarize IPAddress = make_set(client_ipAddress_s,100), City = make_set(client_geographicalContext_city_s,100),
          successes = countif(eventType_s == "user.authentication.auth_via_mfa"),
          denies = countif(eventType_s == "user.mfa.okta_verify.deny_push"),
          pushes = countif(eventType_s == "system.push.send_factor_verify_push") by TimeGenerated, authenticationContext_externalSessionId_s, actor_alternateId_s,actor_displayName_s, outcome_result_s

Stage 4: summarize

| summarize lasttime = max(TimeGenerated), firsttime = min(TimeGenerated),
          successes = sum(successes), failures = sum(denies), pushes = sum(pushes) by  authenticationContext_externalSessionId_s, actor_alternateId_s,actor_displayName_s, outcome_result_s
Threshold
gt 10

Stage 5: extend

| extend seconds = lasttime - firsttime

Stage 6: where

| where pushes >  (PushThreshold)

Stage 7: extend

| extend totalattempts = successes + failures

Stage 8: extend

| extend finding = case(
            failures == pushes and pushes > 1, "Authentication attempts not successful because multiple pushes denied",
            totalattempts == 0, "Multiple pushes sent and ignored",
            successes > 0 and pushes > 3, "Multiple pushes sent, eventual successful authentication!",
            "Normal authentication pattern")
finding =
iffailures == "pushes" and pushes > 1"Authentication attempts not successful because multiple pushes denied"
eliftotalattempts == 0"Multiple pushes sent and ignored"
elifsuccesses > 0 and pushes > 3"Multiple pushes sent, eventual successful authentication!"
else"Normal authentication pattern"

Indicators

Each row is a field, operator, and value that the rule matches. The corpus column counts how many other rules in the catalog look for the same combination: high numbers point to widely-used, community-vetted indicators. Blank or 1 shows that the indicator is specific to this rule.

FieldKindValues
debugContext_debugData_factor_seq
  • OKTA_VERIFY_PUSH transforms: column_ifexists, cased
eventType_seq
  • system.push.send_factor_verify_push transforms: cased
  • user.authentication.auth_via_mfa transforms: cased
  • user.mfa.okta_verify.deny_push transforms: cased
pushesgt
  • 10 transforms: cased

Output fields

Fields the rule emits when it matches. Chronicle authors list these in the outcome block; they appear on the detection and $risk_score drives alerting. Sentinel / Defender XDR rules build them up through project / summarize / extend stages. Sentinel maps these into alert fields via entityMappings and customDetails; Defender XDR custom detections surface them as alert fields directly.

FieldSource
actor_alternateId_ssummarize
actor_displayName_ssummarize
authenticationContext_externalSessionId_ssummarize
failuressummarize
firsttimesummarize
lasttimesummarize
outcome_result_ssummarize
pushessummarize
successessummarize
secondsextend
totalattemptsextend
findingextend