Detection rules › Kusto

Brute force attack against user credentials (Uses Authentication Normalization)

Severity
medium
Time window
20m
Group by
TargetUserId, TargetUserType, TargetUsername
Author
Ofer Shezaf
Source
github.com/Azure/Azure-Sentinel

Identifies evidence of brute force activity against a user based on multiple authentication failures and at least one successful authentication within a given time window. Note that the query does not enforce any sequence, and does not require the successful authentication to occur last. The default failure threshold is 10, success threshold is 1, and the default time window is 20 minutes. To use this analytics rule, make sure you have deployed the ASIM normalization parsers

MITRE ATT&CK coverage

TacticTechniques
Credential AccessT1110 Brute Force

Event coverage

Rule body kusto

id: a6c435a2-b1a0-466d-b730-9f8af69262e8
name: Brute force attack against user credentials (Uses Authentication Normalization)
description: |
  'Identifies evidence of brute force activity against a user based on multiple authentication failures and at least one successful authentication within a given time window.
  Note that the query does not enforce any sequence, and does not require the successful authentication to occur last.
  The default failure threshold is 10, success threshold is 1, and the default time window is 20 minutes.
  To use this analytics rule, make sure you have deployed the [ASIM normalization parsers](https://aka.ms/ASimAuthentication)'
severity: Medium
requiredDataConnectors: []
queryFrequency: 20m
queryPeriod: 20m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CredentialAccess
relevantTechniques:
  - T1110
tags:
  - Id: 28b42356-45af-40a6-a0b4-a554cdfd5d8a
    version: 1.0.0
  - Schema: ASIMAuthentication
    SchemaVersion: 0.1.0
query: |
  let failureCountThreshold = 10;
  let successCountThreshold = 1;
  // let authenticationWindow = 20m; // Implicit in the analytic rule query period 
  imAuthentication
  | where TargetUserType != "NonInteractive"
  | summarize 
        StartTime = min(TimeGenerated), 
        EndTime = max(TimeGenerated), 
        IpAddresses = make_set (SrcDvcIpAddr, 100),
        ReportedBy = make_set (strcat (EventVendor, "/", EventProduct), 100),
        FailureCount = countif(EventResult=='Failure'),
        SuccessCount = countif(EventResult=='Success')
    by 
        TargetUserId, TargetUsername, TargetUserType 
  | where FailureCount >= failureCountThreshold and SuccessCount >= successCountThreshold
  | extend
        IpAddresses = strcat_array(IpAddresses, ", "), 
        ReportedBy = strcat_array(ReportedBy, ", ")
  | extend
    Name = iif(
        TargetUsername contains "@"
            , tostring(split(TargetUsername, '@', 0)[0])
            , TargetUsername
        ),
    UPNSuffix = iif(
        TargetUsername contains "@"
        , tostring(split(TargetUsername, '@', 1)[0])
        , ""
    )
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: TargetUserName
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix

customDetails:
  IpAddresses: IpAddresses
  ReportedBy: ReportedBy
version: 1.2.5
kind: Scheduled
metadata:
    source:
        kind: Community
    author:
        name: Ofer Shezaf
    support:
        tier: Community
    categories:
        domains: [ "Security - Others", "Identity" ]

Stages and Predicates

Parameters

let failureCountThreshold = 10;
let successCountThreshold = 1;

Stage 1: source

imAuthentication

Stage 2: where

| where TargetUserType != "NonInteractive"

Stage 3: summarize

| summarize 
      StartTime = min(TimeGenerated), 
      EndTime = max(TimeGenerated), 
      IpAddresses = make_set (SrcDvcIpAddr, 100),
      ReportedBy = make_set (strcat (EventVendor, "/", EventProduct), 100),
      FailureCount = countif(EventResult=='Failure'),
      SuccessCount = countif(EventResult=='Success')
  by 
      TargetUserId, TargetUsername, TargetUserType
Threshold
ge 10

Stage 4: where

| where FailureCount >= failureCountThreshold and SuccessCount >= successCountThreshold

Stage 5: extend

| extend
      IpAddresses = strcat_array(IpAddresses, ", "), 
      ReportedBy = strcat_array(ReportedBy, ", ")

Stage 6: extend

| extend
  Name = iif(
      TargetUsername contains "@"
          , tostring(split(TargetUsername, '@', 0)[0])
          , TargetUsername
      ),
  UPNSuffix = iif(
      TargetUsername contains "@"
      , tostring(split(TargetUsername, '@', 1)[0])
      , ""
  )

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
FailureCountge
  • 10 transforms: cased corpus 2 (kusto 2)
SuccessCountge
  • 1 transforms: cased corpus 2 (kusto 2)
TargetUserTypene
  • NonInteractive 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
EndTimesummarize
FailureCountsummarize
IpAddressesextend
ReportedByextend
StartTimesummarize
SuccessCountsummarize
TargetUserIdsummarize
TargetUserTypesummarize
TargetUsernamesummarize
Nameextend
UPNSuffixextend