Detection rules › Kusto

MFA Spamming followed by Successful login

Status
available
Severity
high
Time window
1d
Group by
City, IPAddress, Region, State, UserPrincipalName
Source
github.com/Azure/Azure-Sentinel

Identifies MFA Spamming followed by Successful logins and by a successful authentication within a given time window. Default Failure count is 10 and 1 successful login with default Time Window is 5 minutes.

MITRE ATT&CK coverage

TacticTechniques
Credential Access

Telemetry coverage

Rule body

id: a8cc6d5c-4e7e-4b48-b4ac-d8a116c62a8b
name: MFA Spamming followed by Successful login
description: |
  'Identifies MFA Spamming followed by Successful logins and by a successful authentication within a given time window.
  Default Failure count is 10 and 1 successful login with default Time Window is 5 minutes.'
severity: High
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - SigninLogs
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - CredentialAccess
relevantTechniques:
  - T1110
query: |
  // Filter for sign-in logs ingested within the last day
  SigninLogs
  | where ingestion_time() > ago(1d)
  // Filter for records with AuthenticationRequirement set to multiFactorAuthentication
  | where AuthenticationRequirement == "multiFactorAuthentication"
  // Extract information from dynamic columns DeviceDetail and LocationDetails
  | extend DeviceDetail = todynamic(DeviceDetail), LocationDetails = todynamic(LocationDetails)
  // Extract specific attributes from DeviceDetail and LocationDetails
  | extend
        OS = tostring(DeviceDetail.operatingSystem),
        Browser = tostring(DeviceDetail.browser),
        State = tostring(LocationDetails.state),
        City = tostring(LocationDetails.city),
        Region = tostring(LocationDetails.countryOrRegion)
  // Expand multi-value property AuthenticationDetails into separate records
  | mv-expand todynamic(AuthenticationDetails)
  // Parse AuthResult from JSON in AuthenticationDetails and convert to string
  | extend AuthResult = tostring(parse_json(AuthenticationDetails).authenticationStepResultDetail)
  // Summarize data by aggregating statistics for each user, IP, and AuthResult
  | summarize FailedAttempts = countif(AuthResult == "MFA denied; user declined the authentication" or AuthResult == "MFA denied; user did not respond to mobile app notification"), SuccessfulAttempts = countif(AuthResult == "MFA successfully completed"), InvolvedOS = make_set(OS, 5), InvolvedBrowser = make_set(Browser), StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by UserPrincipalName, IPAddress, State, City, Region
  // Calculate AuthenticationWindow by finding time difference between start and end times
  | extend AuthenticationWindow = (EndTime - StartTime)
  // Filter for records with more than 10 failed attempts in 5-minute window and at least 1 successful attempt
  | where FailedAttempts > 10 and AuthenticationWindow <= 5m and SuccessfulAttempts >= 1
  // Extract user's name and UPN suffix using split function
  | extend Name = tostring(split(UserPrincipalName, '@', 0)[0]), UPNSuffix = tostring(split(UserPrincipalName, '@', 1)[0])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: UserPrincipalName
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPAddress
version: 1.0.4
kind: Scheduled

Stages and Predicates

Stage 1: source

SigninLogs

Stage 2: where

| where ingestion_time() > ago(1d)

Stage 3: where

| where AuthenticationRequirement == "multiFactorAuthentication"

Stage 4: extend

| extend DeviceDetail = todynamic(DeviceDetail), LocationDetails = todynamic(LocationDetails)

Stage 5: extend

| extend
      OS = tostring(DeviceDetail.operatingSystem),
      Browser = tostring(DeviceDetail.browser),
      State = tostring(LocationDetails.state),
      City = tostring(LocationDetails.city),
      Region = tostring(LocationDetails.countryOrRegion)

Stage 6: mv-expand

| mv-expand todynamic(AuthenticationDetails)

Stage 7: extend

| extend AuthResult = tostring(parse_json(AuthenticationDetails).authenticationStepResultDetail)

Stage 8: summarize

| summarize FailedAttempts = countif(AuthResult == "MFA denied; user declined the authentication" or AuthResult == "MFA denied; user did not respond to mobile app notification"), SuccessfulAttempts = countif(AuthResult == "MFA successfully completed"), InvolvedOS = make_set(OS, 5), InvolvedBrowser = make_set(Browser), StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by UserPrincipalName, IPAddress, State, City, Region

Stage 9: extend

| extend AuthenticationWindow = (EndTime - StartTime)

Stage 10: where

| where FailedAttempts > 10 and AuthenticationWindow <= 5m and SuccessfulAttempts >= 1

Stage 11: extend

| extend Name = tostring(split(UserPrincipalName, '@', 0)[0]), UPNSuffix = tostring(split(UserPrincipalName, '@', 1)[0])

Indicators

These rows show field, operator, and value matches.

Output fields

These fields are emitted when the rule matches.

FieldSource
Citysummarize
EndTimesummarize
FailedAttemptssummarize
IPAddresssummarize
InvolvedBrowsersummarize
InvolvedOSsummarize
Regionsummarize
StartTimesummarize
Statesummarize
SuccessfulAttemptssummarize
UserPrincipalNamesummarize
AuthenticationWindowextend
Nameextend
UPNSuffixextend