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.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Credential Access | T1110.003 Brute Force: Password Spraying |
Event coverage
| Provider | Event | Title |
|---|---|---|
| Security-Auditing | Event ID 4625 | An account failed to log on. |
Rule body splunk
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/
drilldown_searches:
- name: View the detection results for - "$sourcetype$"
search: '%original_detection_search% | search sourcetype = "$sourcetype$"'
earliest_offset: $info_min_time$
latest_offset: $info_max_time$
- name: View risk events for the last 7 days for - "$sourcetype$"
search: '| from datamodel Risk.All_Risk | search normalized_risk_object IN ("$sourcetype$") | stats count min(_time) as firstTime max(_time) as lastTime values(search_name) as "Search Name" values(risk_message) as "Risk Message" values(analyticstories) as "Analytic Stories" values(annotations._all) as "Annotations" values(annotations.mitre_attack.mitre_tactic) as "ATT&CK Tactics" by normalized_risk_object | `security_content_ctime(firstTime)` | `security_content_ctime(lastTime)`'
earliest_offset: 7d
latest_offset: "0"
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
tests:
- name: True Positive Test
attack_data:
- data: https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1110.003/purplesharp_invalid_users_kerberos_xml/windows-security.log
source: XmlWinEventLog:Security
sourcetype: XmlWinEventLog
test_type: unit
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 =unique_accounts > 30 AND unique_accounts >= upperBound10Stage 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
Top-level NOT(...) conjuncts: predicates this rule actively suppresses.
| Field | Kind | Excluded values |
|---|---|---|
Authentication.src | in | "-", "unknown" |
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.
| Field | Kind | Values |
|---|---|---|
Authentication.action | eq |
|
isOutlier | eq |
|
Search terms
Bare-string tokens in the SPL search body. Splunk matches each token against _raw (the untyped raw event text) anywhere it appears, not against a specific field. These don't surface in the Indicators table because they aren't predicates on a known field.
| Stage | Term |
|---|---|
| 16 | foreach |
| 16 | * |