Detection rules › Kusto

Password spray attack against ADFSSignInLogs

Status
available
Severity
medium
Time window
1h
Group by
IPAddress
Source
github.com/Azure/Azure-Sentinel

'Identifies evidence of password spray activity against Connect Health for AD FS sign-in events by looking for failures from multiple accounts from the same IP address within a time window. Reference: https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/Microsoft Entra ID/ADFSSignInLogsPasswordSpray.md'

MITRE ATT&CK coverage

TacticTechniques
Credential AccessT1110 Brute Force

Rule body kusto

id: 5533fe80-905e-49d5-889a-df27d2c3976d
name: Password spray attack against ADFSSignInLogs
description: |
  'Identifies evidence of password spray activity against Connect Health for AD FS sign-in events by looking for failures from multiple accounts from the same IP address within a time window. Reference: https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/Microsoft%20Entra%20ID/ADFSSignInLogsPasswordSpray.md'
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - ADFSSignInLogs
severity: Medium
queryFrequency: 30m
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - CredentialAccess
relevantTechniques:
  - T1110
query: |
  let queryfrequency = 30m;
  let accountthreshold = 10;
  let successCodes = dynamic([0, 50144]);
  ADFSSignInLogs
  | extend IngestionTime = ingestion_time()
  | where IngestionTime > ago(queryfrequency)
  | where not(todynamic(AuthenticationDetails)[0].authenticationMethod == "Integrated Windows Authentication")
  | summarize
      DistinctFailureCount = dcountif(UserPrincipalName, ResultType !in (successCodes)),
      DistinctSuccessCount = dcountif(UserPrincipalName, ResultType in (successCodes)),
      SuccessAccounts = make_set_if(UserPrincipalName, ResultType in (successCodes), 250),
      arg_min(TimeGenerated, *)
      by IPAddress
  | where DistinctFailureCount > DistinctSuccessCount and DistinctFailureCount >= accountthreshold
  //| extend SuccessAccounts = iff(array_length(SuccessAccounts) != 0, SuccessAccounts, dynamic(["null"]))
  //| mv-expand SuccessAccounts
  | project TimeGenerated, Category, OperationName, IPAddress, DistinctFailureCount, DistinctSuccessCount, SuccessAccounts, AuthenticationRequirement, ConditionalAccessStatus, IsInteractive, UserAgent, NetworkLocationDetails, DeviceDetail, TokenIssuerType, TokenIssuerName, ResourceIdentity
entityMappings:
- entityType: IP
  fieldMappings:
    - identifier: Address
      columnName: IPAddress
version: 1.0.2
kind: Scheduled

Stages and Predicates

Parameters

let queryfrequency = 30m;
let accountthreshold = 10;
let successCodes = dynamic([0, 50144]);

Stage 1: source

ADFSSignInLogs

Stage 2: extend

| extend IngestionTime = ingestion_time()

Stage 3: where

| where IngestionTime > ago(queryfrequency)

Stage 4: where

| where not(todynamic(AuthenticationDetails)[0].authenticationMethod == "Integrated Windows Authentication")

Stage 5: summarize

| summarize
    DistinctFailureCount = dcountif(UserPrincipalName, ResultType !in (successCodes)),
    DistinctSuccessCount = dcountif(UserPrincipalName, ResultType in (successCodes)),
    SuccessAccounts = make_set_if(UserPrincipalName, ResultType in (successCodes), 250),
    arg_min(TimeGenerated, *)
    by IPAddress
Threshold
gt DistinctSuccessCount

Stage 6: where

| where DistinctFailureCount > DistinctSuccessCount and DistinctFailureCount >= accountthreshold

Stage 7: project

| project TimeGenerated, Category, OperationName, IPAddress, DistinctFailureCount, DistinctSuccessCount, SuccessAccounts, AuthenticationRequirement, ConditionalAccessStatus, IsInteractive, UserAgent, NetworkLocationDetails, DeviceDetail, TokenIssuerType, TokenIssuerName, ResourceIdentity

Exclusions

Top-level NOT(...) conjuncts: predicates this rule actively suppresses.

FieldKindExcluded values
authenticationMethodeqIntegrated Windows Authentication

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
DistinctFailureCountge
  • 10 transforms: cased
DistinctFailureCountgt
  • DistinctSuccessCount 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
AuthenticationRequirementproject
Categoryproject
ConditionalAccessStatusproject
DeviceDetailproject
DistinctFailureCountproject
DistinctSuccessCountproject
IPAddressproject
IsInteractiveproject
NetworkLocationDetailsproject
OperationNameproject
ResourceIdentityproject
SuccessAccountsproject
TimeGeneratedproject
TokenIssuerNameproject
TokenIssuerTypeproject
UserAgentproject