Detection rules › Kusto

MFA Rejected by User

Status
available
Severity
medium
Time window
1h
Group by
AADTenantId, FailedIPAddress, Name, UserId, UserPrincipalName
Source
github.com/Azure/Azure-Sentinel

Identifies occurances where a user has rejected an MFA prompt. This could be an indicator that a threat actor has compromised the username and password of this user account and is using it to try and log into the account. Ref : https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-user-accounts#monitoring-for-failed-unusual-sign-ins This query has also been updated to include UEBA logs IdentityInfo and BehaviorAnalytics for contextual information around the results. Please note, MFA Failed logons from known IP ranges can be benign depending on the conditional access policies. In case of noisy behavior, consider tuning the source IP ranges or location filter after careful consideration

MITRE ATT&CK coverage

TacticTechniques
Initial Access

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Rule body

id: d99cf5c3-d660-436c-895b-8a8f8448da23
name: MFA Rejected by User
description: |
  'Identifies occurances where a user has rejected an MFA prompt. This could be an indicator that a threat actor has compromised the username and password of this user account and is using it to try and log into the account.
  Ref : https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-user-accounts#monitoring-for-failed-unusual-sign-ins
  This query has also been updated to include UEBA logs IdentityInfo and BehaviorAnalytics for contextual information around the results. 
  Please note, MFA Failed logons from known IP ranges can be benign depending on the conditional access policies. In case of noisy behavior, consider tuning the source IP ranges or location filter after careful consideration'
severity: Medium
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - SigninLogs
  - connectorId: BehaviorAnalytics
    dataTypes:
      - BehaviorAnalytics
  - connectorId: BehaviorAnalytics
    dataTypes:
      - IdentityInfo
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - InitialAccess
relevantTechniques:
  - T1078.004
tags:
  - AADSecOpsGuide
query: |
  let riskScoreCutoff = 3; //Adjust this score threshold based on volume of results. Activities identified as the most abnormal receive the highest scores (on a scale of 0-10)
  SigninLogs
  | where ResultType == 500121
  | extend additionalDetails_ = tostring(Status.additionalDetails)
  | extend UserPrincipalName = tolower(UserPrincipalName)
  | where additionalDetails_ =~ "MFA denied; user declined the authentication" or additionalDetails_ has "fraud"
  | summarize StartTime = min(TimeGenerated), EndTIme = max(TimeGenerated) by UserPrincipalName, UserId, AADTenantId, FailedIPAddress = IPAddress
  | extend Name = tostring(split(UserPrincipalName,'@',0)[0]), UPNSuffix = tostring(split(UserPrincipalName,'@',1)[0])
  | join kind=leftouter (
      IdentityInfo
      | summarize LatestReportTime = arg_max(TimeGenerated, *) by AccountUPN
      | project AccountUPN, Tags, JobTitle, GroupMembership, AssignedRoles, UserType, IsAccountEnabled
      | summarize
          Tags = make_set(Tags, 1000),
          GroupMembership = make_set(GroupMembership, 1000),
          AssignedRoles = make_set(AssignedRoles, 1000),
          UserType = make_set(UserType, 1000),
          UserAccountControl = make_set(UserType, 1000)
      by AccountUPN
      | extend UserPrincipalName=tolower(AccountUPN)
  ) on UserPrincipalName
  //Below it will be joined with BehaviorAnalytics table to the Failed IP Addresses
  | join kind=leftouter (
      BehaviorAnalytics
      | where ActivityType in ("FailedLogOn", "LogOn")
      | where isnotempty(SourceIPAddress)
      | project UsersInsights, DevicesInsights, ActivityInsights, InvestigationPriority, SourceIPAddress, UserName
      | project-rename FailedIPAddress = SourceIPAddress, Name = UserName
      | summarize
        MaxInvestigationScore = max(InvestigationPriority)  // Only retrieve maximum Investigation Property score for both FailedIP and User
      by FailedIPAddress, Name)
  on FailedIPAddress, Name  // Joining on both IP and User so as to only return context associated with same user
  | extend UEBARiskScore = MaxInvestigationScore
  | project-away *1 // removing duplicate columns post outer join from output
  | where  UEBARiskScore > riskScoreCutoff
  | sort by UEBARiskScore desc 
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: UserPrincipalName
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: Account
    fieldMappings:
      - identifier: AadUserId
        columnName: UserId
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: FailedIPAddress
version: 2.0.4
kind: Scheduled

Stages and Predicates

Parameters

let riskScoreCutoff = 3;

Stage 1: source

SigninLogs

Stage 2: where

| where ResultType == 500121

Stage 3: extend

| extend additionalDetails_ = tostring(Status.additionalDetails)

Stage 4: extend

| extend UserPrincipalName = tolower(UserPrincipalName)

Stage 5: where

| where additionalDetails_ =~ "MFA denied; user declined the authentication" or additionalDetails_ has "fraud"

Stage 6: summarize

| summarize StartTime = min(TimeGenerated), EndTIme = max(TimeGenerated) by UserPrincipalName, UserId, AADTenantId, FailedIPAddress = IPAddress

Stage 7: extend

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

Stage 8: join

| join kind=leftouter (
    IdentityInfo
    | summarize LatestReportTime = arg_max(TimeGenerated, *) by AccountUPN
    | project AccountUPN, Tags, JobTitle, GroupMembership, AssignedRoles, UserType, IsAccountEnabled
    | summarize
        Tags = make_set(Tags, 1000),
        GroupMembership = make_set(GroupMembership, 1000),
        AssignedRoles = make_set(AssignedRoles, 1000),
        UserType = make_set(UserType, 1000),
        UserAccountControl = make_set(UserType, 1000)
    by AccountUPN
    | extend UserPrincipalName=tolower(AccountUPN)
) on UserPrincipalName

Stage 9: join

| join kind=leftouter (
    BehaviorAnalytics
    | where ActivityType in ("FailedLogOn", "LogOn")
    | where isnotempty(SourceIPAddress)
    | project UsersInsights, DevicesInsights, ActivityInsights, InvestigationPriority, SourceIPAddress, UserName
    | project-rename FailedIPAddress = SourceIPAddress, Name = UserName
    | summarize
      MaxInvestigationScore = max(InvestigationPriority)
    by FailedIPAddress, Name)
on FailedIPAddress, Name

Stage 10: extend

| extend UEBARiskScore = MaxInvestigationScore

Stage 11: project-away

| project-away *1

Stage 12: where

| where  UEBARiskScore > riskScoreCutoff

Stage 13: sort

| sort by UEBARiskScore desc

Indicators

These rows show field, operator, and value matches.

Output fields

These fields are emitted when the rule matches.

FieldSource
AADTenantIdsummarize
EndTImesummarize
FailedIPAddresssummarize
StartTimesummarize
UserIdsummarize
UserPrincipalNamesummarize
Nameextend
UPNSuffixextend
UEBARiskScoreextend