HackBlue

Tools · Identity defense · 05 / 05

Hunting password spray in Entra sign-in logs, then testing a policy in report-only

Write KQL over SigninLogs that separates password spray from one user’s typos, then use Conditional Access report-only results to see who a new policy would block before enforcing it.

12 min read · practice only with authorization

What spray looks like in the data

A typo storm is many failures for one account. Password spray is the opposite shape: one or a few source addresses, a handful of attempts per account, across many accounts, often with error 50126 and occasionally a success. Counting distinct accounts per source is the core of the detection.

The hunting query

This query groups failures by source address in one-hour bins and keeps sources that tried many accounts. A second step checks whether any of those sources also produced a success, which turns a noisy hunt into a short list.

Snippet
let window = 1h;
let spray = SigninLogs
  | where TimeGenerated > ago(7d) and ResultType == "50126"
  | summarize Accounts = dcount(UserPrincipalName), Attempts = count() by IPAddress, bin(TimeGenerated, window)
  | where Accounts >= 15 and Attempts / Accounts <= 3;
spray
| join kind=inner (
    SigninLogs | where TimeGenerated > ago(7d) and ResultType == "0"
    | project IPAddress, SuccessUser = UserPrincipalName, SuccessTime = TimeGenerated
  ) on IPAddress
| project IPAddress, Accounts, Attempts, SuccessUser, SuccessTime

Report-only before enforcement

A new policy, “Block legacy authentication”, runs in report-only mode for a week. Sign-in records then include a reportOnlyFailure result for that policy where it would have blocked. Counting those per user and app shows who would break.

Snippet
SigninLogs
| where TimeGenerated > ago(7d)
| mv-expand P = ConditionalAccessPolicies
| where tostring(P.displayName) == "Block legacy authentication" and tostring(P.result) == "reportOnlyFailure"
| summarize Count = count() by UserPrincipalName, ClientAppUsed, AppDisplayName
| order by Count desc

UserPrincipalName           ClientAppUsed         AppDisplayName       Count
scanner@hackblue-lab.org    Authenticated SMTP    Office 365 Exchange  412
kim@hackblue-lab.org        IMAP4                 Office 365 Exchange  3

Worked outcome

The hunt finds one source that tried 61 accounts over two nights with no successes. The address is blocked with a named location, and the accounts get no action beyond a note. Report-only shows that a multifunction printer uses SMTP AUTH with a shared account. That printer moves to a connector before the policy is enforced, and kim’s three IMAP attempts match the 03:14 failure from an unfamiliar address, so her password is reset as a precaution.

Diagram

Typo storm versus password spray

Same error code, different shape.

Typo storm

Accounts. One

Attempts per account. Many

Sources. The user’s usual device

Password spray

Accounts. Many

Attempts per account. One to three

Sources. Few, often unfamiliar addresses

Check yourself

1. Which aggregation best separates spray from typos?

2. What does reportOnlyFailure mean for a policy in a sign-in record?

3. Report-only shows a printer using SMTP AUTH. What should happen before enforcement?