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 approving multi-factor authentication prompts, potentially compromising their own security. The query identifies MFA fatigue attempts in Okta by correlating repeated Okta Verify push requests, denied pushes, and eventual successful MFA authentication within the same session. Ref: https://www.okta.com/blog/identity-security/mfa-fatigue-growing-security-concern/.

MITRE ATT&CK coverage

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Rule body

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 approving multi-factor authentication prompts, potentially compromising their own security. The query identifies MFA fatigue attempts in Okta by correlating repeated Okta Verify push requests, denied pushes, and eventual successful MFA authentication within the same session. 
  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: |
  // Adjust threshold for MFA pushes to reduce noise
  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")
  | project lasttime, firsttime, seconds, actor_alternateId_s, actor_displayName_s, authenticationContext_externalSessionId_s, pushes, successes, failures, totalattempts, finding, outcome_result_s
  | extend AccountName = tostring(split(actor_alternateId_s, "@")[0]), AccountUPNSuffix = tostring(split(actor_alternateId_s, "@")[1])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
alertDetailsOverride:
  alertDisplayNameFormat: 'MFA fatigue suspected for user {{actor_displayName_s}} ({{actor_alternateId_s}})'
  alertDescriptionFormat: 'Okta session {{authenticationContext_externalSessionId_s}} recorded {{pushes}} MFA pushes, over {{seconds}} seconds.'
customDetails:
  SessionId: authenticationContext_externalSessionId_s
  Pushes: pushes
  Successes: successes
  Denies: failures
  TotalAttempts: totalattempts
  Duration: seconds
version: 1.1.3
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

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"

Stage 9: project

| project lasttime, firsttime, seconds, actor_alternateId_s, actor_displayName_s, authenticationContext_externalSessionId_s, pushes, successes, failures, totalattempts, finding, outcome_result_s

Stage 10: extend

| extend AccountName = tostring(split(actor_alternateId_s, "@")[0]), AccountUPNSuffix = tostring(split(actor_alternateId_s, "@")[1])

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
debugContext_debugData_factor_seq
  • OKTA_VERIFY_PUSH transforms: column_ifexists
field:"debugContext_debugData_factor_s" kind:eq value:"OKTA_VERIFY_PUSH"
eventType_seq
  • system.push.send_factor_verify_push
  • user.authentication.auth_via_mfa
  • user.mfa.okta_verify.deny_push
field:"okta::eventType" kind:eq
pushesgt
  • 10
field:"pushes" kind:gt value:"10"

Output fields

These fields are emitted when the rule matches.

FieldSource
actor_alternateId_sproject
actor_displayName_sproject
authenticationContext_externalSessionId_sproject
failuresproject
findingproject
firsttimeproject
lasttimeproject
outcome_result_sproject
pushesproject
secondsproject
successesproject
totalattemptsproject
AccountNameextend
AccountUPNSuffixextend