Detection rules › Kusto

Potential Password Spray Attack

Status
available
Severity
medium
Time window
5m
Group by
City, Country, TimeGenerated, client_ipAddress_s
Source
github.com/Azure/Azure-Sentinel

This query searches for failed attempts to log into the Okta console from more than 15 various users within a 5 minute timeframe from the same source. This is a potential indication of a password spray attack.

MITRE ATT&CK coverage

TacticTechniques
Credential Access

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Rule body

id: e27dd7e5-4367-4c40-a2b7-fcd7e7a8a508
name: Potential Password Spray Attack
description: |
  This query searches for failed attempts to log into the Okta console from more than 15 various users within a 5 minute timeframe from the same source. This is a potential indication of a password spray attack.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: OktaSSO
    dataTypes:
      - Okta_CL
  - connectorId: OktaSSOv2
    dataTypes:
      - OktaSSO
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CredentialAccess
relevantTechniques:
  - T1110.003
query: |
  // Adjust threshold for failed logins to reduce noise
  let FailureThreshold = 15;
  let FailedEvents = OktaSSO
  | where eventType_s =~ "user.session.start" and outcome_reason_s in ("VERIFICATION_ERROR","INVALID_CREDENTIALS")
  | summarize dcount(actor_alternateId_s) by client_ipAddress_s, bin(TimeGenerated, 5m)
  | where dcount_actor_alternateId_s > FailureThreshold
  | project client_ipAddress_s, TimeGenerated;
  OktaSSO
  | where eventType_s =~ "user.session.start" and outcome_reason_s in ("VERIFICATION_ERROR","INVALID_CREDENTIALS")
  | summarize Users = make_set(actor_alternateId_s) by client_ipAddress_s, City = column_ifexists('client_geographicalContext_city_s', ""), Country = column_ifexists('client_geographicalContext_country_s', ""), bin(TimeGenerated, 5m)
  | join kind=inner (FailedEvents) on client_ipAddress_s, TimeGenerated
  | project TimeGenerated, client_ipAddress_s, Users, DistinctUsers = array_length(Users), City, Country
  | sort by TimeGenerated desc
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: client_ipAddress_s
customDetails:
  Users: Users
  City: City
  Country: Country
alertDetailsOverride:
  alertDisplayNameFormat: 'Potential password spray attack from IP {{client_ipAddress_s}}'
  alertDescriptionFormat: '{{DistinctUsers}} distinct users failed authentication from {{client_ipAddress_s}} in a 5 minute window.'
version: 1.1.2
kind: Scheduled

Stages and Predicates

Parameters

let FailureThreshold = 15;

Let binding: FailedEvents used in Stage 4

let FailedEvents = OktaSSO
| where eventType_s =~ "user.session.start" and outcome_reason_s in ("VERIFICATION_ERROR","INVALID_CREDENTIALS")
| summarize dcount(actor_alternateId_s) by client_ipAddress_s, bin(TimeGenerated, 5m)
| where dcount_actor_alternateId_s > FailureThreshold
| project client_ipAddress_s, TimeGenerated;

Stage 1: source

OktaSSO

Stage 2: where

| where eventType_s =~ "user.session.start" and outcome_reason_s in ("VERIFICATION_ERROR","INVALID_CREDENTIALS")

Stage 3: summarize

| summarize Users = make_set(actor_alternateId_s) by client_ipAddress_s, City = column_ifexists('client_geographicalContext_city_s', ""), Country = column_ifexists('client_geographicalContext_country_s', ""), bin(TimeGenerated, 5m)

Stage 4: join

| join kind=inner (FailedEvents) on client_ipAddress_s, TimeGenerated

Stage 5: project

| project TimeGenerated, client_ipAddress_s, Users, DistinctUsers = array_length(Users), City, Country

Stage 6: sort

| sort by TimeGenerated desc

Indicators

These rows show field, operator, and value matches.

Output fields

These fields are emitted when the rule matches.

FieldSource
Cityproject
Countryproject
DistinctUsersproject
TimeGeneratedproject
Usersproject
client_ipAddress_sproject