Detection rules › Kusto

Brute force attack against an Entra-authenticated Windows device

Status
available
Severity
medium
Time window
20m
Group by
Anomalies, Baseline, FailureToSuccessDiff, Score, UserDisplayName, UserPrincipalName
Source
github.com/Azure/Azure-Sentinel

Identifies evidence of brute force activity against Windows devices authenticated via Entra ID (including Entra-joined, hybrid-joined, and Windows 365 Cloud PCs) by detecting multiple authentication failures followed by a successful authentication within a defined time window.

MITRE ATT&CK coverage

TacticTechniques
Credential Access

Telemetry coverage

Rule body

id: 3fbc20a4-04c4-464e-8fcb-6667f53e4987
name: Brute force attack against an Entra-authenticated Windows device
description: |
  'Identifies evidence of brute force activity against Windows devices authenticated via Entra ID (including Entra-joined, hybrid-joined, and Windows 365 Cloud PCs) by detecting multiple authentication failures followed by a successful authentication within a defined time window.'
severity: Medium
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - SigninLogs
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - CredentialAccess
relevantTechniques:
  - T1110
query: |
  let authenticationWindow = 20m;
  let sensitivity = 2.5;
  SigninLogs
  | where AppDisplayName =~ "Windows Sign In"
  | extend FailureOrSuccess = iff(ResultType in ("0", "50125", "50140", "70043", "70044"), "Success", "Failure")
  | summarize FailureCount = countif(FailureOrSuccess=="Failure"), SuccessCount = countif(FailureOrSuccess=="Success"), IPAddresses = make_set(IPAddress,1000)
                            by bin(TimeGenerated, authenticationWindow), UserDisplayName, UserPrincipalName
  | extend FailureSuccessDiff = FailureCount - SuccessCount
  | where FailureSuccessDiff > 0
  | sort by UserPrincipalName asc, TimeGenerated asc
  | serialize
  | summarize Diff = make_list(FailureSuccessDiff, 10000), TimeStamp = make_list(TimeGenerated, 10000) by UserDisplayName, UserPrincipalName//, tostring(IPAddresses)
  | extend (Anomalies, Score, Baseline) = series_decompose_anomalies(Diff, sensitivity, -1, 'linefit')  
  | mv-expand Diff to typeof(double), TimeStamp to typeof(datetime), Anomalies to typeof(double), Score to typeof(double), Baseline to typeof(long)
  | where Anomalies > 0
  | summarize by UserDisplayName, UserPrincipalName, Anomalies, Score, Baseline, FailureToSuccessDiff = Diff
  | join kind=leftouter (
        SigninLogs
        | where AppDisplayName =~ "Windows Sign In"
        | extend OS = DeviceDetail.operatingSystem, Browser = DeviceDetail.browser
        | extend StatusCode = tostring(Status.errorCode), StatusDetails = tostring(Status.additionalDetails)
        | extend State = tostring(LocationDetails.state), City = tostring(LocationDetails.city)
        | summarize StartTime = min(TimeGenerated), 
                    EndTime = max(TimeGenerated), 
                    IPAddresses = make_set(IPAddress,100), 
                    OS = make_set(OS,20), 
                    Browser = make_set(Browser,20), 
                    City = make_set(City,100), 
                    ResultType = make_set(ResultType,100)
                by UserDisplayName, UserPrincipalName, UserId, AppDisplayName
    ) on UserDisplayName, UserPrincipalName
  | project-away UserDisplayName1, UserPrincipalName1
  | extend IPAddressFirst = tostring(IPAddresses[0])
  | 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: Account
    fieldMappings:
      - identifier: AadUserId
        columnName: UserId
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPAddressFirst    
version: 2.0.3
kind: Scheduled

Stages and Predicates

Stage 0: let

let authenticationWindow = 20m;
let sensitivity = 2.5;

Stage 1: source

SigninLogs

Stage 2: where

| where AppDisplayName =~ "Windows Sign In"

Stage 3: extend

| extend FailureOrSuccess = iff(ResultType in ("0", "50125", "50140", "70043", "70044"), "Success", "Failure")
FailureOrSuccess =
ifResultType in (0, 50125, 50140, 70043, 70044)"Success"
else"Failure"

Stage 4: summarize

| summarize FailureCount = countif(FailureOrSuccess=="Failure"), SuccessCount = countif(FailureOrSuccess=="Success"), IPAddresses = make_set(IPAddress,1000)
                          by bin(TimeGenerated, authenticationWindow), UserDisplayName, UserPrincipalName

Stage 5: extend

| extend FailureSuccessDiff = FailureCount - SuccessCount

Stage 6: where

| where FailureSuccessDiff > 0

Stage 7: sort

| sort by UserPrincipalName asc, TimeGenerated asc

Stage 8: kusto:serialize

| serialize

Stage 9: summarize

| summarize Diff = make_list(FailureSuccessDiff, 10000), TimeStamp = make_list(TimeGenerated, 10000) by UserDisplayName, UserPrincipalName

The stages below score time-series anomalies (make-series, series_decompose_anomalies).

Stage 10: extend

| extend (Anomalies, Score, Baseline) = series_decompose_anomalies(Diff, sensitivity, -1, 'linefit')

Stage 11: mv-expand

| mv-expand Diff to typeof(double), TimeStamp to typeof(datetime), Anomalies to typeof(double), Score to typeof(double), Baseline to typeof(long)

Stage 12: where

| where Anomalies > 0

Stage 13: summarize

| summarize by UserDisplayName, UserPrincipalName, Anomalies, Score, Baseline, FailureToSuccessDiff = Diff

Stage 14: join

| join kind=leftouter (
      SigninLogs
      | where AppDisplayName =~ "Windows Sign In"
      | extend OS = DeviceDetail.operatingSystem, Browser = DeviceDetail.browser
      | extend StatusCode = tostring(Status.errorCode), StatusDetails = tostring(Status.additionalDetails)
      | extend State = tostring(LocationDetails.state), City = tostring(LocationDetails.city)
      | summarize StartTime = min(TimeGenerated), 
                  EndTime = max(TimeGenerated), 
                  IPAddresses = make_set(IPAddress,100), 
                  OS = make_set(OS,20), 
                  Browser = make_set(Browser,20), 
                  City = make_set(City,100), 
                  ResultType = make_set(ResultType,100)
              by UserDisplayName, UserPrincipalName, UserId, AppDisplayName
  ) on UserDisplayName, UserPrincipalName

Stage 15: project-away

project-away UserDisplayName1, UserPrincipalName1

Stage 16: extend

extend IPAddressFirst

Stage 17: extend

extend Name, UPNSuffix

Indicators

These rows show field, operator, and value matches.

Output fields

These fields are emitted when the rule matches.

FieldSource
Anomaliessummarize
Baselinesummarize
FailureToSuccessDiffsummarize
Scoresummarize
UserDisplayNamesummarize
UserPrincipalNamesummarize
IPAddressFirstextend
Nameextend
UPNSuffixextend