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
| Signal | Where | ATT&CK |
|---|---|---|
| Many 4625 failures from one source in minutes | Security 4625 | T1110.001 |
| Few failures per account across many accounts (spray) | Security 4625 | T1110.003 |
| Successful logon from a source that previously failed | 4624 after 4625 | T1078 |
| RDP logon type 10 or 3 from a public address | 4624, firewall | T1021.001 |
| Internet-exposed 3389 discovered on a host | Attack-surface scans | T1133 |
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
- Put RDP behind a VPN or gateway with multifactor authentication. Detection is the second line; removing exposure is the first.
- Run an external attack-surface scan regularly and alert when 3389 appears on a new address.
- Lock out or block the source at the firewall on a confirmed burst, and force a reset for any account that succeeded.
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 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.
Stay ahead of emerging threats. Get notified when we publish new intelligence reports and advisories.
Subscribe to Alerts