Detection rules › Splunk

Detect Password Spray Attempts

Status
production
Severity
medium
Group by
_time, app, authentication_method, counter, dest, event_action, signature, signature_id, sourcetype, src, unique_accounts
Author
Dean Luxton
Source
github.com/splunk/security_content

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

TacticTechniques
Credential AccessT1110.003 Brute Force: Password Spraying

Event coverage

ProviderEventTitle
Security-AuditingEvent ID 4625An 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 =
ifunique_accounts > 30 AND unique_accounts >= upperBound1
else0

Stage 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.

FieldKindExcluded values
Authentication.srcin"-", "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.

FieldKindValues
Authentication.actioneq
  • "failure" corpus 5 (splunk 5)
isOutliereq
  • 1 corpus 28 (splunk 28)

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.

StageTerm
16foreach
16*