Files
Anthropic-Cybersecurity-Skills/skills/hunting-for-dcom-lateral-movement/scripts/agent.py
T

349 lines
14 KiB
Python

#!/usr/bin/env python3
"""DCOM Lateral Movement Detection Agent - Hunts for DCOM object abuse via Sysmon event correlation."""
import json
import logging
import argparse
import os
import sys
import subprocess
from collections import defaultdict
from datetime import datetime, timedelta
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger(__name__)
# DCOM COM object CLSIDs used for lateral movement
DCOM_CLSIDS = {
"{49B2791A-B1AE-4C90-9B8E-E860BA07F889}": "MMC20.Application",
"{9BA05972-F6A8-11CF-A442-00A0C90A8F39}": "ShellWindows",
"{C08AFD90-F2A1-11D1-8455-00A0C91F3880}": "ShellBrowserWindow",
"{00024500-0000-0000-C000-000000000046}": "Excel.Application",
"{0006F03A-0000-0000-C000-000000000046}": "Outlook.Application",
}
DCOM_PARENT_PROCESSES = ["mmc.exe", "dllhost.exe", "explorer.exe"]
SUSPICIOUS_CHILDREN = [
"cmd.exe", "powershell.exe", "pwsh.exe", "wscript.exe",
"cscript.exe", "mshta.exe", "rundll32.exe", "regsvr32.exe",
"certutil.exe", "bitsadmin.exe",
]
SYSMON_NS = "http://schemas.microsoft.com/win/2004/08/events/event"
EVTX_PARSE_TIMEOUT = 300 # seconds
def parse_evtx_records(evtx_path):
"""Parse Sysmon EVTX file into structured events using python-evtx."""
try:
from Evtx.Evtx import FileHeader
from lxml import etree
except ImportError:
logger.error("Required packages missing. Install: pip install python-evtx lxml")
sys.exit(1)
events = []
ns = {"evt": SYSMON_NS}
with open(evtx_path, "rb") as f:
fh = FileHeader(f)
for record in fh.records():
try:
xml = record.xml()
root = etree.fromstring(xml.encode("utf-8"))
event_id_elem = root.find(".//evt:System/evt:EventID", ns)
if event_id_elem is None:
continue
eid = int(event_id_elem.text)
if eid not in (1, 3, 7):
continue
data = {}
for elem in root.findall(".//evt:EventData/evt:Data", ns):
data[elem.get("Name", "")] = elem.text or ""
time_elem = root.find(".//evt:System/evt:TimeCreated", ns)
timestamp = time_elem.get("SystemTime", "") if time_elem is not None else ""
comp_elem = root.find(".//evt:System/evt:Computer", ns)
computer = comp_elem.text if comp_elem is not None else ""
data["EventID"] = eid
data["TimeCreated"] = timestamp
data["Computer"] = computer
events.append(data)
except Exception:
continue
logger.info("Parsed %d Sysmon events (EID 1,3,7) from %s", len(events), evtx_path)
return events
def detect_mmc20_lateral(events):
"""Detect MMC20.Application DCOM lateral movement: mmc.exe spawning suspicious children."""
findings = []
for ev in events:
if ev.get("EventID") != 1:
continue
parent = ev.get("ParentImage", "").lower()
image = ev.get("Image", "").lower()
if "mmc.exe" not in parent:
continue
if not any(child in image for child in SUSPICIOUS_CHILDREN):
continue
findings.append({
"detection": "MMC20.Application DCOM Lateral Movement",
"severity": "HIGH",
"mitre": "T1021.003",
"timestamp": ev.get("TimeCreated"),
"computer": ev.get("Computer"),
"parent_image": ev.get("ParentImage"),
"parent_cmdline": ev.get("ParentCommandLine"),
"child_image": ev.get("Image"),
"child_cmdline": ev.get("CommandLine"),
"user": ev.get("User"),
"clsid": "{49B2791A-B1AE-4C90-9B8E-E860BA07F889}",
})
logger.info("MMC20 detections: %d", len(findings))
return findings
def detect_shell_dcom_lateral(events):
"""Detect ShellWindows/ShellBrowserWindow: explorer.exe spawning cmd/powershell."""
findings = []
for ev in events:
if ev.get("EventID") != 1:
continue
parent = ev.get("ParentImage", "").lower()
image = ev.get("Image", "").lower()
if "explorer.exe" not in parent:
continue
if not any(child in image for child in ["cmd.exe", "powershell.exe", "pwsh.exe",
"mshta.exe", "wscript.exe", "cscript.exe"]):
continue
findings.append({
"detection": "ShellWindows/ShellBrowserWindow DCOM Lateral Movement",
"severity": "MEDIUM",
"mitre": "T1021.003",
"timestamp": ev.get("TimeCreated"),
"computer": ev.get("Computer"),
"parent_image": ev.get("ParentImage"),
"child_image": ev.get("Image"),
"child_cmdline": ev.get("CommandLine"),
"user": ev.get("User"),
"clsid": "{9BA05972} or {C08AFD90}",
})
logger.info("ShellWindows/ShellBrowserWindow detections: %d", len(findings))
return findings
def detect_dllhost_lateral(events):
"""Detect DCOM via dllhost.exe spawning suspicious children."""
findings = []
for ev in events:
if ev.get("EventID") != 1:
continue
parent = ev.get("ParentImage", "").lower()
image = ev.get("Image", "").lower()
if "dllhost.exe" not in parent:
continue
if not any(child in image for child in SUSPICIOUS_CHILDREN):
continue
parent_cmdline = ev.get("ParentCommandLine", "")
clsid = "Unknown"
if "/Processid:" in parent_cmdline:
start = parent_cmdline.find("/Processid:") + len("/Processid:")
clsid_raw = parent_cmdline[start:].strip().strip("{}")
clsid = "{" + clsid_raw + "}"
dcom_name = DCOM_CLSIDS.get(clsid.upper(), "Unknown DCOM Object")
findings.append({
"detection": f"DCOM via dllhost.exe ({dcom_name})",
"severity": "HIGH",
"mitre": "T1021.003",
"timestamp": ev.get("TimeCreated"),
"computer": ev.get("Computer"),
"parent_image": ev.get("ParentImage"),
"parent_cmdline": parent_cmdline,
"child_image": ev.get("Image"),
"child_cmdline": ev.get("CommandLine"),
"user": ev.get("User"),
"clsid": clsid,
"dcom_object": dcom_name,
})
logger.info("dllhost.exe DCOM detections: %d", len(findings))
return findings
def detect_rpc_connections(events):
"""Detect inbound RPC endpoint mapper connections (port 135) from Sysmon Event ID 3."""
rpc_connections = []
for ev in events:
if ev.get("EventID") != 3:
continue
dest_port = ev.get("DestinationPort", "")
initiated = ev.get("Initiated", "").lower()
if dest_port == "135" and initiated == "false":
rpc_connections.append({
"detection": "Inbound RPC Connection (DCOM Prerequisite)",
"severity": "LOW",
"timestamp": ev.get("TimeCreated"),
"computer": ev.get("Computer"),
"source_ip": ev.get("SourceIp"),
"dest_ip": ev.get("DestinationIp"),
"dest_port": dest_port,
"image": ev.get("Image"),
})
logger.info("Inbound RPC (port 135) connections: %d", len(rpc_connections))
return rpc_connections
def correlate_rpc_with_process(rpc_events, process_findings, window_seconds=60):
"""Correlate RPC connections with DCOM process creation for high-confidence detections."""
correlated = []
for proc in process_findings:
proc_time_str = proc.get("timestamp", "")
proc_computer = proc.get("computer", "")
if not proc_time_str:
continue
try:
proc_dt = datetime.fromisoformat(proc_time_str.replace("Z", "+00:00"))
except (ValueError, TypeError):
continue
for rpc in rpc_events:
rpc_time_str = rpc.get("timestamp", "")
rpc_computer = rpc.get("computer", "")
if not rpc_time_str or rpc_computer != proc_computer:
continue
try:
rpc_dt = datetime.fromisoformat(rpc_time_str.replace("Z", "+00:00"))
except (ValueError, TypeError):
continue
delta = (proc_dt - rpc_dt).total_seconds()
if 0 <= delta <= window_seconds:
correlated.append({
"detection": "CORRELATED: RPC Connection -> DCOM Process Creation",
"severity": "CRITICAL",
"mitre": "T1021.003",
"computer": proc_computer,
"source_ip": rpc.get("source_ip"),
"rpc_time": rpc_time_str,
"process_time": proc_time_str,
"time_delta_seconds": round(delta, 2),
"dcom_detection": proc.get("detection"),
"child_image": proc.get("child_image"),
"child_cmdline": proc.get("child_cmdline"),
"user": proc.get("user"),
})
break
logger.info("Correlated RPC->Process chains: %d", len(correlated))
return correlated
def audit_dcom_config():
"""Audit local DCOM configuration for high-risk COM objects (Windows only)."""
if sys.platform != "win32":
logger.info("DCOM config audit only available on Windows")
return []
audit_results = []
for clsid, name in DCOM_CLSIDS.items():
try:
result = subprocess.run(
["reg", "query", f"HKLM\\SOFTWARE\\Classes\\CLSID\\{clsid}"],
capture_output=True, text=True, timeout=10
)
exists = result.returncode == 0
audit_results.append({
"clsid": clsid,
"name": name,
"registered": exists,
"risk": "HIGH" if exists else "N/A",
})
except subprocess.TimeoutExpired:
audit_results.append({"clsid": clsid, "name": name, "registered": "TIMEOUT", "risk": "UNKNOWN"})
except Exception as e:
audit_results.append({"clsid": clsid, "name": name, "registered": f"ERROR: {e}", "risk": "UNKNOWN"})
# Check if DCOM is enabled
try:
result = subprocess.run(
["reg", "query", "HKLM\\SOFTWARE\\Microsoft\\Ole", "/v", "EnableDCOM"],
capture_output=True, text=True, timeout=10
)
dcom_enabled = "Y" in result.stdout if result.returncode == 0 else "UNKNOWN"
audit_results.append({"check": "DCOM Enabled", "value": dcom_enabled,
"risk": "HIGH" if dcom_enabled == "Y" else "LOW"})
except (subprocess.TimeoutExpired, Exception):
pass
return audit_results
def generate_report(all_findings, dcom_audit, output_path):
"""Generate JSON detection report."""
report = {
"scan_timestamp": datetime.utcnow().isoformat() + "Z",
"mitre_technique": "T1021.003",
"summary": {
"total_findings": len(all_findings),
"critical": len([f for f in all_findings if f.get("severity") == "CRITICAL"]),
"high": len([f for f in all_findings if f.get("severity") == "HIGH"]),
"medium": len([f for f in all_findings if f.get("severity") == "MEDIUM"]),
"low": len([f for f in all_findings if f.get("severity") == "LOW"]),
},
"findings": all_findings,
"dcom_config_audit": dcom_audit,
}
with open(output_path, "w") as f:
json.dump(report, f, indent=2, default=str)
logger.info("Report saved to %s", output_path)
s = report["summary"]
print(f"\nDCOM LATERAL MOVEMENT DETECTION REPORT")
print(f" Total findings: {s['total_findings']}")
print(f" Critical: {s['critical']}, High: {s['high']}, Medium: {s['medium']}, Low: {s['low']}")
if s["critical"] > 0:
print(" [!!!] CRITICAL: Correlated RPC + process creation chains detected")
return report
def main():
parser = argparse.ArgumentParser(
description="DCOM Lateral Movement Detection Agent (T1021.003)"
)
parser.add_argument("--evtx", required=True, help="Path to Sysmon .evtx log file")
parser.add_argument("--output", "-o", default="dcom_detection_report.json",
help="Output JSON report path (default: dcom_detection_report.json)")
parser.add_argument("--correlation-window", type=int, default=60,
help="Seconds window for RPC-to-process correlation (default: 60)")
parser.add_argument("--audit-dcom", action="store_true",
help="Audit local DCOM object registration (Windows only)")
parser.add_argument("--verbose", "-v", action="store_true", help="Enable debug logging")
args = parser.parse_args()
if args.verbose:
logging.getLogger().setLevel(logging.DEBUG)
if not os.path.isfile(args.evtx):
logger.error("EVTX file not found: %s", args.evtx)
sys.exit(1)
logger.info("Parsing Sysmon events from: %s", args.evtx)
events = parse_evtx_records(args.evtx)
mmc_findings = detect_mmc20_lateral(events)
shell_findings = detect_shell_dcom_lateral(events)
dllhost_findings = detect_dllhost_lateral(events)
rpc_connections = detect_rpc_connections(events)
all_process_findings = mmc_findings + shell_findings + dllhost_findings
correlated = correlate_rpc_with_process(
rpc_connections, all_process_findings, args.correlation_window
)
all_findings = correlated + all_process_findings
all_findings.sort(key=lambda x: x.get("severity", ""), reverse=True)
dcom_audit = audit_dcom_config() if args.audit_dcom else []
generate_report(all_findings, dcom_audit, args.output)
if __name__ == "__main__":
main()