RDP
BRUTE FORCE
4625
PASSWORD SPRAY
DetectionBlue TeamSOCOctober 12, 2026•9 min read

Detecting RDP Brute Force and Password Spray Entry

Exposed RDP remains a dependable way into a network. The attack is noisy for hours before it succeeds, and that noise is your warning.

Scroll

Guessing passwords against internet-facing RDP is cheap, automated and still productive. Most attempts fail first, which produces a burst of failed logons followed, if you are unlucky, by one that works. Alerting on the burst and especially on the first success afterward moves detection ahead of any post-compromise activity.

Signals at a Glance

SignalWhereATT&CK
Many 4625 failures from one source in minutesSecurity 4625T1110.001
Few failures per account across many accounts (spray)Security 4625T1110.003
Successful logon from a source that previously failed4624 after 4625T1078
RDP logon type 10 or 3 from a public address4624, firewallT1021.001
Internet-exposed 3389 discovered on a hostAttack-surface scansT1133

Failure Bursts and Spray

With Network Level Authentication, failed RDP attempts commonly appear as type 3 rather than type 10, so do not filter on type 10 alone. Measure both per-source volume and the number of distinct accounts targeted:

index=wineventlog EventCode=4625 (Logon_Type=3 OR Logon_Type=10)
| bin _time span=10m
| stats count as failures dc(Account_Name) as accounts values(Failure_Reason) as reasons by _time Source_Network_Address
| eval pattern=case(failures>=30 AND accounts<=3, "brute_force", accounts>=10 AND failures<=accounts*3, "spray", true(), "other")
| where pattern!="other"
| sort - failures
`comment("tune/validate: thresholds depend on exposure; exclude known NAT egress and jump hosts")`

The Failure-Then-Success Sequence

The most important alert is not the failures. It is the success that follows them, because it marks the moment an external actor holds a valid session:

// Sentinel (KQL) - success after a failure burst from the same source
let fails = SecurityEvent
| where EventID == 4625
| summarize failures=count(), lastFail=max(TimeGenerated) by IpAddress, bin(TimeGenerated, 1h)
| where failures >= 20;
SecurityEvent
| where EventID == 4624 and LogonType in (3, 10)
| join kind=inner fails on IpAddress
| where TimeGenerated between (lastFail .. lastFail + 30m)
| project TimeGenerated, Computer, TargetUserName, IpAddress, LogonType, failures
// tune/validate: IpAddress is empty for some logons; fall back to WorkstationName
Vetted sources

SigmaHQ and Elastic brute-force and password-spray rules, Splunk ESCU authentication analytics, MITRE CAR, and CISA #StopRansomware guidance on securing remote services. Your own failure baseline, including VPN and jump-host noise, sets the thresholds.

Build Detections That Fire

Mjolnir Security helps blue teams turn threat intelligence into high-fidelity detections across endpoint, network, identity and cloud telemetry.

Detection EngineeringManaged Detection & ResponseThreat HuntingIncident ResponsePurple Teaming
  • Detection Engineering We build and tune detections mapped to ATT&CK from your own telemetry.
  • Threat Hunting as a Service Hypothesis-driven hunts for the behaviours on this page.
  • 24/7 Incident Response Containment when a detection fires. Call +1 833 403 5875.
Written by: Mjolnir Threat Research (MTAC)  |  Published: October 12, 2026  |  Last reviewed: October 12, 2026