Detection rules › Splunk
Detect Password Spray Attempts
This analytic employs the 3-sigma approach to detect an unusual volume of failed authentication attempts from a single source. A password spray attack is a type of brute force attack where an attacker tries a few common passwords across many different accounts to avoid detection and account lockouts. By utilizing the Authentication Data Model, this detection is effective for all CIM-mapped authentication events, providing comprehensive coverage and enhancing security against these attacks.
Known false positives
- No false positives have been identified at this time.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Credential Access |
Telemetry coverage
| Provider | Record / event type |
|---|---|
| Security-Auditing | Event ID 4625: An account failed to log on. |
Rule body
name: Detect Password Spray Attempts
id: 086ab581-8877-42b3-9aee-4a7ecb0923af
version: 12
creation_date: '2024-07-01'
modification_date: '2026-05-13'
author: Dean Luxton
status: production
type: TTP
description: This analytic employs the 3-sigma approach to detect an unusual volume of failed authentication attempts from a single source. A password spray attack is a type of brute force attack where an attacker tries a few common passwords across many different accounts to avoid detection and account lockouts. By utilizing the Authentication Data Model, this detection is effective for all CIM-mapped authentication events, providing comprehensive coverage and enhancing security against these attacks.
data_source:
- Windows Event Log Security 4625
search: "| tstats `security_content_summariesonly` values(Authentication.user) AS unique_user_names dc(Authentication.user) AS unique_accounts values(Authentication.app) as app count(Authentication.user) as total_failures from datamodel=Authentication.Authentication where Authentication.action=\"failure\" NOT Authentication.src IN (\"-\",\"unknown\") by Authentication.action Authentication.app Authentication.authentication_method Authentication.dest \n Authentication.signature Authentication.signature_id Authentication.src sourcetype _time span=5m \n| `drop_dm_object_name(\"Authentication\")`\n ```fill out time buckets for 0-count events during entire search length```\n| appendpipe [| timechart limit=0 span=5m count | table _time] | fillnull value=0 unique_accounts\n ``` Create aggregation field & apply to all null events```\n| eval counter=src+\"__\"+sourcetype+\"__\"+signature_id | eventstats values(counter) as fnscounter | eval counter=coalesce(counter,fnscounter) \n ``` stats version of mvexpand ```\n| stats values(app) as app values(unique_user_names) as unique_user_names values(total_failures) as total_failures values(src) as src values(signature_id) as signature_id values(sourcetype) as sourcetype count by counter unique_accounts _time\n ``` remove duplicate time buckets for each unique source```\n| sort - _time unique_accounts | dedup _time counter\n ```Find the outliers```\n| eventstats avg(unique_accounts) as comp_avg , stdev(unique_accounts) as comp_std by counter | eval upperBound=(comp_avg+comp_std*3) | eval isOutlier=if(unique_accounts > 30 and unique_accounts >= upperBound, 1, 0) | replace \"::ffff:*\" with * in src | where isOutlier=1 | foreach * \n [ eval <<FIELD>> = if(<<FIELD>>=\"null\",null(),<<FIELD>>)] \n| table _time, src, action, app, unique_accounts, unique_user_names, total_failures, sourcetype, signature_id, counter | `detect_password_spray_attempts_filter`"
how_to_implement: 'Ensure in-scope authentication data is CIM mapped and the src field is populated with the source device. Also ensure fill_nullvalue is set within the macro security_content_summariesonly. This search opporates best on a 5 minute schedule, looking back over the past 70 minutes. Configure 70 minute throttling on the two fields _time and counter. '
known_false_positives: No false positives have been identified at this time.
references:
- https://attack.mitre.org/techniques/T1110/003/
finding:
title: Potential Password Spraying attack from $src$ targeting $unique_accounts$ unique accounts.
entity:
field: unique_user_names
type: user
score: 50
threat_objects:
- field: src
type: system
analytic_story:
- Compromised User Account
- Active Directory Password Spraying
asset_type: Endpoint
atomic_guid:
- 90bc2e54-6c84-47a5-9439-0a2a92b4b175
mitre_attack_id:
- T1110.003
product:
- Splunk Enterprise
- Splunk Enterprise Security
- Splunk Cloud
category: application
security_domain: access
Stages and Predicates
Stage 1: tstats
| tstats `security_content_summariesonly` values(Authentication.user) AS unique_user_names dc(Authentication.user) AS unique_accounts values(Authentication.app) as app count(Authentication.user) as total_failures from datamodel=Authentication.Authentication where Authentication.action="failure" NOT Authentication.src IN ("-","unknown") by Authentication.action Authentication.app Authentication.authentication_method Authentication.dest
Authentication.signature Authentication.signature_id Authentication.src sourcetype _time span=5m
Stage 2: search
| `drop_dm_object_name("Authentication")`
Stage 3: appendpipe
| appendpipe [| timechart limit=0 span=5m count | table _time]
Stage 4: fillnull
| fillnull value=0 unique_accounts
Stage 5: eval
| eval counter=src+"__"+sourcetype+"__"+signature_id
Stage 6: eventstats
| eventstats values(counter) as fnscounter
Stage 7: eval
| eval counter=coalesce(counter,fnscounter)
Stage 8: stats
| stats values(app) as app values(unique_user_names) as unique_user_names values(total_failures) as total_failures values(src) as src values(signature_id) as signature_id values(sourcetype) as sourcetype count by counter unique_accounts _time
Stage 9: sort
| sort - _time unique_accounts
Stage 10: dedup
| dedup _time counter
Stage 11: eventstats
| eventstats avg(unique_accounts) as comp_avg , stdev(unique_accounts) as comp_std by counter
Stage 12: eval
| eval upperBound=(comp_avg+comp_std*3)
Stage 13: eval
| eval isOutlier=if(unique_accounts > 30 and unique_accounts >= upperBound, 1, 0)
isOutlier =if
unique_accounts > 30 AND unique_accounts >= upperBound1else
0Stage 14: replace
| replace "::ffff:*" with * in src
Stage 15: where
| where isOutlier=1
Stage 16: search
| foreach *
[ eval <<FIELD>> = if(<<FIELD>>="null",null(),<<FIELD>>)]
Stage 17: table
| table _time, src, action, app, unique_accounts, unique_user_names, total_failures, sourcetype, signature_id, counter
Stage 18: search
| `detect_password_spray_attempts_filter`
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
Authentication.src | in | "-", "unknown" | excludes:Authentication.src |
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Authentication.action | eq |
| field:"event_action" kind:eq |
isOutlier | eq |
| field:"isOutlier" kind:eq value:"1" |
Search terms
These SPL tokens match against raw event text.
| Stage | Term |
|---|---|
| 16 | foreach |
| 16 | * |