Distributed password spray
Many DISTINCT accounts failed authentication across the tenant in a short window — a password spray, counted the only way a spray can be counted.
What it detects
Many DISTINCT accounts failed authentication across the tenant in a short window — a password spray, counted the only way a spray can be counted.
A spray tries one or two common passwords against a large list of accounts. Modern ones distribute the attempts across many source addresses as well, and the result defeats every per-entity threshold by construction:
* group by USER → one or two failures per account. Under any sane threshold. * group by SOURCE IP → one or two failures per address. Under any sane threshold.
Both of our existing brute-force rules group by one of those axes, so a distributed spray is invisible to them by design, not by oversight. Measured on a benchmark corpus: 30 accounts, 29 distinct source addresses, every attempt a failure, all inside 25 seconds — and not one rule fired.
The signal only exists at the level of the tenant: the BREADTH of accounts being probed at once. Nobody legitimately fails to authenticate as twenty different people in five minutes. So this rule counts distinct accounts tenant-wide, which is what threshold.group_by: [] means.
MITRE ATT&CK mapping
- T1110.003Brute Force: Password Spraying· Credential Access
False positives and triage
The two things that genuinely look like this are a mass password-policy change and an outage: after a forced reset, or when a directory or MFA provider is failing, many accounts fail at once from ordinary locations. Check whether the failures carry a credential error (a spray does) or a service/timeout error (an outage does), and whether the sources are the organisation's own egress addresses.
Onboarding a new SSO integration can also produce a broad burst as clients re-authenticate. Correlate with change records.
The count of 20 in 5 minutes is set to sit above ordinary human error at small-to-mid tenant size; a very large tenant may legitimately exceed it during business-hours peaks and should raise it after measuring its own baseline. Read the ratio too: a spray shows near-100% failure across the accounts it touched, while an outage shows failures mixed with successes.
Because this counts tenant-wide, one alert describes the whole spray rather than one per account — that is deliberate, and the evidence lists every account probed.
The rule
Declarative YAML read by the detection engine — data, never executed. Confidence 70%. Source: rules/multi/distributed_password_spray.yaml, licensed under the Apache License 2.0.
id: multi.authentication.distributed_password_spray
version: 1.0.0
status: stable
severity: high
confidence: 0.7
platforms: [windows, linux, macos, network, application]
mitre_techniques: [T1110.003]
description: |
Many DISTINCT accounts failed authentication across the tenant in a short window —
a password spray, counted the only way a spray can be counted.
A spray tries one or two common passwords against a large list of accounts. Modern
ones distribute the attempts across many source addresses as well, and the result
defeats every per-entity threshold by construction:
* group by USER → one or two failures per account. Under any sane threshold.
* group by SOURCE IP → one or two failures per address. Under any sane threshold.
Both of our existing brute-force rules group by one of those axes, so a distributed
spray is invisible to them by design, not by oversight. Measured on a benchmark
corpus: 30 accounts, 29 distinct source addresses, every attempt a failure, all
inside 25 seconds — and not one rule fired.
The signal only exists at the level of the tenant: the BREADTH of accounts being
probed at once. Nobody legitimately fails to authenticate as twenty different people
in five minutes. So this rule counts distinct accounts tenant-wide, which is what
`threshold.group_by: []` means.
match:
event_category: authentication
event_outcome: failure
threshold:
# Distinct ACCOUNTS, not events: twenty failures for one unlucky user is a
# different finding (and a different rule).
distinct: user_name
count: 20
window_seconds: 300
# Tenant-wide. Grouping by anything is what a spray is built to evade.
group_by: []
correlation_keys: [source_ip]
false_positive_guidance: |
The two things that genuinely look like this are a mass password-policy change and
an outage: after a forced reset, or when a directory or MFA provider is failing,
many accounts fail at once from ordinary locations. Check whether the failures carry
a credential error (a spray does) or a service/timeout error (an outage does), and
whether the sources are the organisation's own egress addresses.
Onboarding a new SSO integration can also produce a broad burst as clients
re-authenticate. Correlate with change records.
The count of 20 in 5 minutes is set to sit above ordinary human error at
small-to-mid tenant size; a very large tenant may legitimately exceed it during
business-hours peaks and should raise it after measuring its own baseline. Read
the ratio too: a spray shows near-100% failure across the accounts it touched,
while an outage shows failures mixed with successes.
Because this counts tenant-wide, one alert describes the whole spray rather than
one per account — that is deliberate, and the evidence lists every account probed.
Related detections
Run this rule on your own logs — offline
KIROSEC runs this rule and the rest of the library over Windows, Linux, macOS and cloud audit logs on the analyst's own machine. Every alert links to the events that raised it, and nothing leaves the device.
