threat detection

Detecting RDP Brute Force Attacks

Detect RDP brute force attacks by analyzing Windows Security Event Logs for failed authentication patterns (Event ID 4625), successful logons after failures (Event ID 4624), NLA failures, and source IP frequency analysis.

blue-teambrute-forcerdpsiemthreat-detectionwindows-event-logs
Install this skill
npx skills add mukul975/Anthropic-Cybersecurity-Skills
Framework mappings

Overview

RDP brute force attacks target Windows Remote Desktop Protocol services by attempting rapid credential guessing against exposed RDP endpoints. Detection relies on analyzing Windows Security Event Logs for Event ID 4625 (failed logon with Logon Type 10 or 3) and correlating with Event ID 4624 (successful logon) to identify compromised accounts. This skill covers parsing EVTX files with python-evtx, identifying attack patterns through source IP frequency analysis, detecting NLA bypass attempts, and generating actionable detection reports.

When to Use

  • When investigating security incidents that require detecting rdp brute force attacks
  • When building detection rules or threat hunting queries for this domain
  • When SOC analysts need structured procedures for this analysis type
  • When validating security monitoring coverage for related attack techniques

Prerequisites

  • Python 3.9+ with python-evtx, lxml libraries
  • Windows Security EVTX log files (exported from Event Viewer or collected via WEF)
  • Understanding of Windows authentication Event IDs (4624, 4625, 4776)
  • Familiarity with RDP Logon Types (Type 3 for NLA, Type 10 for RemoteInteractive)

Steps

Step 1: Export Security Event Logs

Export Windows Security logs to EVTX format using Event Viewer or wevtutil:

wevtutil epl Security C:\logs\security.evtx

Step 2: Parse Failed Logon Events

Use python-evtx to parse Event ID 4625 entries, extracting source IP, target username, failure reason (Sub Status), and Logon Type fields.

Step 3: Analyze Attack Patterns

Identify brute force patterns by:

  • Counting failed logons per source IP within time windows
  • Detecting username spray attacks (many usernames from one IP)
  • Correlating 4625 failures with subsequent 4624 success from same IP

Step 4: Generate Detection Report

Produce a JSON report with top attacking IPs, targeted accounts, time-based analysis, and compromise indicators.

Expected Output

JSON report containing:

  • Total failed logon events and unique source IPs
  • Top attacking IPs ranked by failure count
  • Targeted usernames and failure sub-status codes
  • Successful logons following brute force attempts (potential compromises)
  • Time-series analysis of attack intensity
Source materials

References and resources

Everything below is rendered for inspection. Script files are read-only and never run.

References 1

api-reference.md2.1 KB

API Reference: Detecting RDP Brute Force Attacks

Windows Security Event IDs

Event ID Description Key Fields
4625 Failed logon attempt TargetUserName, IpAddress, SubStatus, LogonType
4624 Successful logon TargetUserName, IpAddress, LogonType
4776 NTLM credential validation TargetUserName, Workstation, Status
4771 Kerberos pre-auth failed TargetUserName, IpAddress, Status

Logon Types for RDP

Type Name Context
3 Network RDP with NLA enabled (pre-auth)
10 RemoteInteractive RDP session after NLA

Failure Sub-Status Codes

Sub-Status Meaning
0xC0000064 User does not exist
0xC000006A Wrong password
0xC0000234 Account locked out
0xC0000072 Account disabled
0xC0000193 Account expired
0xC0000071 Password expired

python-evtx Library Usage

import Evtx.Evtx as evtx
 
with evtx.Evtx("Security.evtx") as log:
    for record in log.records():
        xml_str = record.xml()

Install: pip install python-evtx lxml

wevtutil Export Commands

# Export Security log to EVTX
wevtutil epl Security C:\logs\security.evtx
 
# Query failed RDP logons
wevtutil qe Security /q:"*[System[(EventID=4625)] and EventData[Data[@Name='LogonType']='10']]" /f:text
 
# Count recent failed logons
wevtutil qe Security /q:"*[System[(EventID=4625)]]" /c:100 /rd:true /f:text

Detection Thresholds

Pattern Threshold Indicator
Brute force >10 failures/IP in 15 min Single-target credential guessing
Password spray >5 unique users/IP Multi-user single-password attack
Compromise 4625 followed by 4624 from same IP Successful brute force

References

Scripts 1

agent.py6.6 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
"""Agent for detecting RDP brute force attacks from Windows Event Logs.

Parses EVTX files for Event ID 4625 (failed logon) and 4624
(successful logon) to identify brute force patterns, source IP
frequency, username spraying, and post-compromise indicators.
"""
# For authorized security monitoring and blue team use only

import argparse
import json
import os
import xml.etree.ElementTree as ET
from collections import Counter, defaultdict
from datetime import datetime
from pathlib import Path

try:
    import Evtx.Evtx as evtx
except ImportError:
    evtx = None

NS = "{http://schemas.microsoft.com/win/2004/08/events/event}"
FAILURE_SUBSTATUS = {
    "0xc0000064": "user_does_not_exist",
    "0xc000006a": "wrong_password",
    "0xc0000072": "account_disabled",
    "0xc000006f": "logon_outside_hours",
    "0xc0000070": "workstation_restriction",
    "0xc0000071": "password_expired",
    "0xc0000234": "account_locked",
    "0xc0000193": "account_expired",
}
RDP_LOGON_TYPES = {"3", "10"}


def _extract_event_data(xml_str):
    """Extract structured fields from a single EVTX record XML."""
    root = ET.fromstring(xml_str)
    sys_node = root.find(f"{NS}System")
    event_id = sys_node.find(f"{NS}EventID").text if sys_node is not None else None
    time_created = None
    tc = sys_node.find(f"{NS}TimeCreated") if sys_node is not None else None
    if tc is not None:
        time_created = tc.get("SystemTime", "")

    data = {}
    for d in root.iter(f"{NS}Data"):
        name = d.get("Name", "")
        data[name] = d.text or ""
    return event_id, time_created, data


class RDPBruteForceDetector:
    """Detects RDP brute force attacks from Windows Security EVTX logs."""

    def __init__(self, evtx_path, threshold=10, time_window_minutes=15,
                 output_dir="./rdp_brute_force_report"):
        self.evtx_path = evtx_path
        self.threshold = threshold
        self.time_window = time_window_minutes
        self.output_dir = Path(output_dir)
        self.output_dir.mkdir(parents=True, exist_ok=True)
        self.failed_logons = []
        self.successful_logons = []

    def parse_events(self):
        """Parse EVTX file and extract logon events."""
        if evtx is None:
            raise RuntimeError("python-evtx not installed: pip install python-evtx")
        with evtx.Evtx(self.evtx_path) as log:
            for record in log.records():
                try:
                    xml_str = record.xml()
                except Exception:
                    continue
                event_id, ts, data = _extract_event_data(xml_str)
                logon_type = data.get("LogonType", "")
                if event_id == "4625" and logon_type in RDP_LOGON_TYPES:
                    self.failed_logons.append({
                        "timestamp": ts,
                        "source_ip": data.get("IpAddress", "-"),
                        "target_user": data.get("TargetUserName", ""),
                        "target_domain": data.get("TargetDomainName", ""),
                        "sub_status": data.get("SubStatus", "").lower(),
                        "logon_type": logon_type,
                    })
                elif event_id == "4624" and logon_type in RDP_LOGON_TYPES:
                    self.successful_logons.append({
                        "timestamp": ts,
                        "source_ip": data.get("IpAddress", "-"),
                        "target_user": data.get("TargetUserName", ""),
                        "logon_type": logon_type,
                    })

    def analyze(self):
        """Analyze parsed events for brute force patterns."""
        ip_counter = Counter(e["source_ip"] for e in self.failed_logons)
        user_counter = Counter(e["target_user"] for e in self.failed_logons)
        substatus_counter = Counter(
            FAILURE_SUBSTATUS.get(e["sub_status"], e["sub_status"])
            for e in self.failed_logons
        )

        brute_force_ips = {ip: cnt for ip, cnt in ip_counter.items()
                          if cnt >= self.threshold and ip != "-"}

        success_ips = {e["source_ip"] for e in self.successful_logons}
        compromised = []
        for ip in brute_force_ips:
            if ip in success_ips:
                user_list = [e["target_user"] for e in self.successful_logons
                             if e["source_ip"] == ip]
                compromised.append({"ip": ip, "users": list(set(user_list)),
                                    "failed_attempts": brute_force_ips[ip]})

        users_per_ip = defaultdict(set)
        for e in self.failed_logons:
            users_per_ip[e["source_ip"]].add(e["target_user"])
        spray_ips = {ip: len(users) for ip, users in users_per_ip.items()
                     if len(users) >= 5}

        return {
            "total_failed_logons": len(self.failed_logons),
            "total_successful_logons": len(self.successful_logons),
            "unique_source_ips": len(ip_counter),
            "top_attacking_ips": ip_counter.most_common(20),
            "targeted_users": user_counter.most_common(20),
            "failure_reasons": dict(substatus_counter),
            "brute_force_ips": brute_force_ips,
            "password_spray_ips": spray_ips,
            "potential_compromises": compromised,
        }

    def generate_report(self):
        """Parse events and generate full detection report."""
        self.parse_events()
        analysis = self.analyze()

        report = {
            "report_date": datetime.utcnow().isoformat(),
            "evtx_file": str(self.evtx_path),
            "detection_threshold": self.threshold,
            **analysis,
        }
        out = self.output_dir / "rdp_brute_force_report.json"
        with open(out, "w") as f:
            json.dump(report, f, indent=2, default=str)
        print(json.dumps(report, indent=2, default=str))
        return report


def main():
    parser = argparse.ArgumentParser(
        description="Detect RDP brute force attacks from Windows Security EVTX logs"
    )
    parser.add_argument("evtx_file", help="Path to Security.evtx log file")
    parser.add_argument("--threshold", type=int, default=10,
                        help="Min failed logons per IP to flag as brute force (default: 10)")
    parser.add_argument("--output-dir", default="./rdp_brute_force_report",
                        help="Output directory for report")
    args = parser.parse_args()

    os.makedirs(args.output_dir, exist_ok=True)
    detector = RDPBruteForceDetector(args.evtx_file, args.threshold,
                                     output_dir=args.output_dir)
    detector.generate_report()


if __name__ == "__main__":
    main()
Keep exploring