mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-07-18 05:29:40 +03:00
Add 30 new production-grade cybersecurity skills: AI security, supply chain, firmware, cloud-native, compliance, deception, crypto, threat hunting, purple team, OT, privacy
This commit is contained in:
@@ -0,0 +1,348 @@
|
||||
#!/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()
|
||||
@@ -0,0 +1,495 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
DCOM Lateral Movement Detection Script
|
||||
Parses Windows Security and Sysmon event logs to detect DCOM-based lateral movement
|
||||
via MMC20.Application, ShellWindows, and ShellBrowserWindow COM object abuse.
|
||||
|
||||
MITRE ATT&CK: T1021.003 (Remote Services: Distributed Component Object Model)
|
||||
|
||||
Usage:
|
||||
python detect_dcom_lateral_movement.py --evtx <path_to_sysmon_evtx>
|
||||
python detect_dcom_lateral_movement.py --evtx <sysmon.evtx> --security <security.evtx>
|
||||
python detect_dcom_lateral_movement.py --evtx <sysmon.evtx> --json --output results.json
|
||||
|
||||
Requirements:
|
||||
pip install python-evtx lxml
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
from datetime import datetime, timedelta
|
||||
from collections import defaultdict
|
||||
|
||||
try:
|
||||
import Evtx.Evtx as evtx
|
||||
import Evtx.Views as evtx_views
|
||||
from lxml import etree
|
||||
except ImportError:
|
||||
print("[!] Required packages not found. Install with: pip install python-evtx lxml")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# DCOM-related COM object CLSIDs
|
||||
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",
|
||||
}
|
||||
|
||||
# Suspicious child processes when spawned by DCOM parent processes
|
||||
SUSPICIOUS_CHILDREN = [
|
||||
"cmd.exe", "powershell.exe", "pwsh.exe", "wscript.exe",
|
||||
"cscript.exe", "mshta.exe", "rundll32.exe", "regsvr32.exe",
|
||||
"certutil.exe", "bitsadmin.exe", "msbuild.exe",
|
||||
]
|
||||
|
||||
# DCOM parent processes that spawn child processes during lateral movement
|
||||
DCOM_PARENTS = ["mmc.exe", "dllhost.exe", "explorer.exe", "svchost.exe"]
|
||||
|
||||
SYSMON_NS = "http://schemas.microsoft.com/win/2004/08/events/event"
|
||||
|
||||
|
||||
def parse_sysmon_event(record_xml):
|
||||
"""Parse a Sysmon event record XML into a dictionary."""
|
||||
try:
|
||||
root = etree.fromstring(record_xml)
|
||||
except etree.XMLSyntaxError:
|
||||
return None
|
||||
|
||||
ns = {"e": SYSMON_NS}
|
||||
event = {}
|
||||
|
||||
system = root.find(".//e:System", ns)
|
||||
if system is not None:
|
||||
event_id_elem = system.find("e:EventID", ns)
|
||||
event["EventID"] = int(event_id_elem.text) if event_id_elem is not None else 0
|
||||
time_elem = system.find("e:TimeCreated", ns)
|
||||
if time_elem is not None:
|
||||
event["TimeCreated"] = time_elem.get("SystemTime", "")
|
||||
computer_elem = system.find("e:Computer", ns)
|
||||
event["Computer"] = computer_elem.text if computer_elem is not None else ""
|
||||
|
||||
event_data = root.find(".//e:EventData", ns)
|
||||
if event_data is not None:
|
||||
for data in event_data.findall("e:Data", ns):
|
||||
name = data.get("Name", "")
|
||||
value = data.text or ""
|
||||
event[name] = value
|
||||
|
||||
return event
|
||||
|
||||
|
||||
def is_dcom_parent(image_path):
|
||||
"""Check if the process image is a known DCOM parent."""
|
||||
if not image_path:
|
||||
return False
|
||||
image_lower = image_path.lower()
|
||||
return any(parent in image_lower for parent in DCOM_PARENTS)
|
||||
|
||||
|
||||
def is_suspicious_child(image_path):
|
||||
"""Check if the process image is a suspicious child for DCOM context."""
|
||||
if not image_path:
|
||||
return False
|
||||
image_lower = image_path.lower()
|
||||
return any(child in image_lower for child in SUSPICIOUS_CHILDREN)
|
||||
|
||||
|
||||
def check_dcomllaunch_parent(command_line):
|
||||
"""Check if the parent command line indicates DcomLaunch service."""
|
||||
if not command_line:
|
||||
return False
|
||||
return "dcomlaunch" in command_line.lower()
|
||||
|
||||
|
||||
def detect_dcom_process_creation(events):
|
||||
"""
|
||||
Detect DCOM lateral movement via Sysmon Event ID 1 (Process Create).
|
||||
Looks for DCOM parent processes spawning suspicious children.
|
||||
"""
|
||||
findings = []
|
||||
|
||||
for event in events:
|
||||
if event.get("EventID") != 1:
|
||||
continue
|
||||
|
||||
parent_image = event.get("ParentImage", "")
|
||||
image = event.get("Image", "")
|
||||
parent_cmdline = event.get("ParentCommandLine", "")
|
||||
cmdline = event.get("CommandLine", "")
|
||||
user = event.get("User", "")
|
||||
time_created = event.get("TimeCreated", "")
|
||||
computer = event.get("Computer", "")
|
||||
|
||||
# Pattern 1: mmc.exe spawning suspicious child (MMC20.Application)
|
||||
if "mmc.exe" in parent_image.lower() and is_suspicious_child(image):
|
||||
findings.append({
|
||||
"timestamp": time_created,
|
||||
"computer": computer,
|
||||
"detection_type": "MMC20.Application DCOM Lateral Movement",
|
||||
"dcom_object": "MMC20.Application",
|
||||
"clsid": "{49B2791A-B1AE-4C90-9B8E-E860BA07F889}",
|
||||
"parent_image": parent_image,
|
||||
"parent_commandline": parent_cmdline,
|
||||
"child_image": image,
|
||||
"child_commandline": cmdline,
|
||||
"user": user,
|
||||
"severity": "HIGH",
|
||||
"mitre": "T1021.003",
|
||||
})
|
||||
|
||||
# Pattern 2: DcomLaunch svchost spawning dllhost or mmc
|
||||
if check_dcomllaunch_parent(parent_cmdline) and is_suspicious_child(image):
|
||||
findings.append({
|
||||
"timestamp": time_created,
|
||||
"computer": computer,
|
||||
"detection_type": "DcomLaunch Service Spawning Suspicious Process",
|
||||
"dcom_object": "Unknown (DcomLaunch)",
|
||||
"clsid": "N/A",
|
||||
"parent_image": parent_image,
|
||||
"parent_commandline": parent_cmdline,
|
||||
"child_image": image,
|
||||
"child_commandline": cmdline,
|
||||
"user": user,
|
||||
"severity": "HIGH",
|
||||
"mitre": "T1021.003",
|
||||
})
|
||||
|
||||
# Pattern 3: explorer.exe spawning cmd/powershell on servers
|
||||
# (ShellWindows/ShellBrowserWindow)
|
||||
if "explorer.exe" in parent_image.lower() and is_suspicious_child(image):
|
||||
# Check if this might be interactive (less suspicious) or DCOM (more suspicious)
|
||||
findings.append({
|
||||
"timestamp": time_created,
|
||||
"computer": computer,
|
||||
"detection_type": "ShellWindows/ShellBrowserWindow DCOM Lateral Movement (Requires Correlation)",
|
||||
"dcom_object": "ShellWindows or ShellBrowserWindow",
|
||||
"clsid": "{9BA05972-F6A8-11CF-A442-00A0C90A8F39} or {C08AFD90-F2A1-11D1-8455-00A0C91F3880}",
|
||||
"parent_image": parent_image,
|
||||
"parent_commandline": parent_cmdline,
|
||||
"child_image": image,
|
||||
"child_commandline": cmdline,
|
||||
"user": user,
|
||||
"severity": "MEDIUM",
|
||||
"mitre": "T1021.003",
|
||||
})
|
||||
|
||||
# Pattern 4: dllhost.exe spawning suspicious children
|
||||
if "dllhost.exe" in parent_image.lower() and is_suspicious_child(image):
|
||||
# Extract CLSID from dllhost command line if present
|
||||
detected_clsid = "Unknown"
|
||||
if "/Processid:" in parent_cmdline:
|
||||
clsid_start = parent_cmdline.find("/Processid:") + len("/Processid:")
|
||||
detected_clsid = parent_cmdline[clsid_start:].strip().strip("{}")
|
||||
detected_clsid = "{" + detected_clsid + "}"
|
||||
|
||||
dcom_name = DCOM_CLSIDS.get(detected_clsid.upper(), "Unknown DCOM Object")
|
||||
|
||||
findings.append({
|
||||
"timestamp": time_created,
|
||||
"computer": computer,
|
||||
"detection_type": "DCOM Object Execution via dllhost.exe",
|
||||
"dcom_object": dcom_name,
|
||||
"clsid": detected_clsid,
|
||||
"parent_image": parent_image,
|
||||
"parent_commandline": parent_cmdline,
|
||||
"child_image": image,
|
||||
"child_commandline": cmdline,
|
||||
"user": user,
|
||||
"severity": "HIGH",
|
||||
"mitre": "T1021.003",
|
||||
})
|
||||
|
||||
return findings
|
||||
|
||||
|
||||
def detect_dcom_network_connections(events):
|
||||
"""
|
||||
Detect DCOM-related network connections via Sysmon Event ID 3.
|
||||
Looks for inbound RPC connections (port 135) to DCOM processes.
|
||||
"""
|
||||
findings = []
|
||||
|
||||
for event in events:
|
||||
if event.get("EventID") != 3:
|
||||
continue
|
||||
|
||||
image = event.get("Image", "")
|
||||
dest_port = event.get("DestinationPort", "")
|
||||
source_ip = event.get("SourceIp", "")
|
||||
dest_ip = event.get("DestinationIp", "")
|
||||
initiated = event.get("Initiated", "")
|
||||
time_created = event.get("TimeCreated", "")
|
||||
computer = event.get("Computer", "")
|
||||
|
||||
# Inbound RPC connection (port 135) -- DCOM always starts here
|
||||
if dest_port == "135" and initiated.lower() == "false":
|
||||
findings.append({
|
||||
"timestamp": time_created,
|
||||
"computer": computer,
|
||||
"detection_type": "Inbound RPC Endpoint Mapper Connection",
|
||||
"source_ip": source_ip,
|
||||
"destination_ip": dest_ip,
|
||||
"destination_port": dest_port,
|
||||
"process_image": image,
|
||||
"severity": "MEDIUM",
|
||||
"mitre": "T1021.003",
|
||||
"note": "DCOM communication begins with RPC endpoint mapper query on port 135",
|
||||
})
|
||||
|
||||
# DCOM process making outbound connection on high port (dynamic RPC)
|
||||
if is_dcom_parent(image) and dest_port and int(dest_port) > 49151:
|
||||
findings.append({
|
||||
"timestamp": time_created,
|
||||
"computer": computer,
|
||||
"detection_type": "DCOM Process Dynamic RPC Connection",
|
||||
"source_ip": source_ip,
|
||||
"destination_ip": dest_ip,
|
||||
"destination_port": dest_port,
|
||||
"process_image": image,
|
||||
"severity": "LOW",
|
||||
"mitre": "T1021.003",
|
||||
"note": "DCOM process communicating on dynamic RPC port range",
|
||||
})
|
||||
|
||||
return findings
|
||||
|
||||
|
||||
def correlate_network_and_process(process_findings, network_findings, window_seconds=60):
|
||||
"""
|
||||
Correlate network connections with process creation events.
|
||||
A network connection to port 135 followed by DCOM process creation
|
||||
within the time window is a strong indicator of lateral movement.
|
||||
"""
|
||||
correlated = []
|
||||
|
||||
for proc in process_findings:
|
||||
proc_time = proc.get("timestamp", "")
|
||||
proc_computer = proc.get("computer", "")
|
||||
|
||||
if not proc_time:
|
||||
continue
|
||||
|
||||
try:
|
||||
proc_dt = datetime.fromisoformat(proc_time.replace("Z", "+00:00"))
|
||||
except (ValueError, TypeError):
|
||||
continue
|
||||
|
||||
for net in network_findings:
|
||||
net_time = net.get("timestamp", "")
|
||||
net_computer = net.get("computer", "")
|
||||
|
||||
if not net_time or net_computer != proc_computer:
|
||||
continue
|
||||
|
||||
try:
|
||||
net_dt = datetime.fromisoformat(net_time.replace("Z", "+00:00"))
|
||||
except (ValueError, TypeError):
|
||||
continue
|
||||
|
||||
time_diff = abs((proc_dt - net_dt).total_seconds())
|
||||
|
||||
if time_diff <= window_seconds and net_dt <= proc_dt:
|
||||
correlated.append({
|
||||
"correlation_type": "DCOM Lateral Movement Chain",
|
||||
"severity": "CRITICAL",
|
||||
"mitre": "T1021.003",
|
||||
"computer": proc_computer,
|
||||
"network_event": {
|
||||
"timestamp": net_time,
|
||||
"source_ip": net.get("source_ip"),
|
||||
"destination_port": net.get("destination_port"),
|
||||
},
|
||||
"process_event": {
|
||||
"timestamp": proc_time,
|
||||
"dcom_object": proc.get("dcom_object"),
|
||||
"parent_image": proc.get("parent_image"),
|
||||
"child_image": proc.get("child_image"),
|
||||
"child_commandline": proc.get("child_commandline"),
|
||||
"user": proc.get("user"),
|
||||
},
|
||||
"time_delta_seconds": round(time_diff, 2),
|
||||
})
|
||||
|
||||
return correlated
|
||||
|
||||
|
||||
def parse_evtx_file(filepath):
|
||||
"""Parse a .evtx file and return list of parsed events."""
|
||||
events = []
|
||||
try:
|
||||
with evtx.Evtx(filepath) as log:
|
||||
for record in log.records():
|
||||
try:
|
||||
event = parse_sysmon_event(record.xml())
|
||||
if event:
|
||||
events.append(event)
|
||||
except Exception:
|
||||
continue
|
||||
except Exception as e:
|
||||
print(f"[!] Error parsing {filepath}: {e}")
|
||||
return events
|
||||
|
||||
|
||||
def print_findings(findings, title):
|
||||
"""Print findings in a formatted table."""
|
||||
if not findings:
|
||||
print(f"\n[+] {title}: No findings")
|
||||
return
|
||||
|
||||
print(f"\n{'=' * 80}")
|
||||
print(f" {title} ({len(findings)} findings)")
|
||||
print(f"{'=' * 80}")
|
||||
|
||||
for i, finding in enumerate(findings, 1):
|
||||
print(f"\n [{i}] {finding.get('detection_type', 'Unknown')}")
|
||||
print(f" Severity: {finding.get('severity', 'N/A')}")
|
||||
print(f" MITRE: {finding.get('mitre', 'N/A')}")
|
||||
print(f" Time: {finding.get('timestamp', 'N/A')}")
|
||||
print(f" Computer: {finding.get('computer', 'N/A')}")
|
||||
|
||||
if "dcom_object" in finding:
|
||||
print(f" DCOM Object: {finding['dcom_object']}")
|
||||
print(f" CLSID: {finding.get('clsid', 'N/A')}")
|
||||
if "parent_image" in finding:
|
||||
print(f" Parent: {finding['parent_image']}")
|
||||
print(f" Child: {finding.get('child_image', 'N/A')}")
|
||||
print(f" Command: {finding.get('child_commandline', 'N/A')[:120]}")
|
||||
if "source_ip" in finding:
|
||||
print(f" Source IP: {finding['source_ip']}")
|
||||
print(f" Dest Port: {finding.get('destination_port', 'N/A')}")
|
||||
if "note" in finding:
|
||||
print(f" Note: {finding['note']}")
|
||||
|
||||
|
||||
def print_correlated(correlated):
|
||||
"""Print correlated findings."""
|
||||
if not correlated:
|
||||
print("\n[+] Correlated DCOM Chains: No findings")
|
||||
return
|
||||
|
||||
print(f"\n{'=' * 80}")
|
||||
print(f" CORRELATED DCOM LATERAL MOVEMENT CHAINS ({len(correlated)} findings)")
|
||||
print(f"{'=' * 80}")
|
||||
|
||||
for i, c in enumerate(correlated, 1):
|
||||
net = c["network_event"]
|
||||
proc = c["process_event"]
|
||||
print(f"\n [{i}] {c['correlation_type']}")
|
||||
print(f" Severity: {c['severity']}")
|
||||
print(f" Target: {c['computer']}")
|
||||
print(f" Source IP: {net['source_ip']} -> port {net['destination_port']}")
|
||||
print(f" Time Delta: {c['time_delta_seconds']}s")
|
||||
print(f" DCOM Object: {proc['dcom_object']}")
|
||||
print(f" Process Chain: {proc['parent_image']} -> {proc['child_image']}")
|
||||
print(f" Command: {proc.get('child_commandline', 'N/A')[:120]}")
|
||||
print(f" User: {proc.get('user', 'N/A')}")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Detect DCOM lateral movement from Sysmon and Security event logs"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--evtx", required=True,
|
||||
help="Path to Sysmon .evtx log file"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--security",
|
||||
help="Path to Windows Security .evtx log file (optional, for 4624 correlation)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--json", action="store_true",
|
||||
help="Output results in JSON format"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output", "-o",
|
||||
help="Output file path (default: stdout)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--correlation-window", type=int, default=60,
|
||||
help="Time window in seconds for correlating network and process events (default: 60)"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
if not os.path.exists(args.evtx):
|
||||
print(f"[!] File not found: {args.evtx}")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"[*] Parsing Sysmon events from: {args.evtx}")
|
||||
events = parse_evtx_file(args.evtx)
|
||||
print(f"[*] Parsed {len(events)} Sysmon events")
|
||||
|
||||
security_events = []
|
||||
if args.security:
|
||||
if os.path.exists(args.security):
|
||||
print(f"[*] Parsing Security events from: {args.security}")
|
||||
security_events = parse_evtx_file(args.security)
|
||||
print(f"[*] Parsed {len(security_events)} Security events")
|
||||
else:
|
||||
print(f"[!] Security log not found: {args.security}")
|
||||
|
||||
print("[*] Analyzing for DCOM lateral movement indicators...")
|
||||
|
||||
process_findings = detect_dcom_process_creation(events)
|
||||
network_findings = detect_dcom_network_connections(events)
|
||||
correlated = correlate_network_and_process(
|
||||
process_findings, network_findings, args.correlation_window
|
||||
)
|
||||
|
||||
all_results = {
|
||||
"scan_time": datetime.utcnow().isoformat() + "Z",
|
||||
"sysmon_log": args.evtx,
|
||||
"security_log": args.security or "Not provided",
|
||||
"total_events_parsed": len(events) + len(security_events),
|
||||
"process_creation_findings": process_findings,
|
||||
"network_connection_findings": network_findings,
|
||||
"correlated_chains": correlated,
|
||||
"summary": {
|
||||
"process_detections": len(process_findings),
|
||||
"network_detections": len(network_findings),
|
||||
"correlated_chains": len(correlated),
|
||||
"critical_findings": len([c for c in correlated]),
|
||||
"high_findings": len([f for f in process_findings if f.get("severity") == "HIGH"]),
|
||||
},
|
||||
}
|
||||
|
||||
if args.json:
|
||||
output = json.dumps(all_results, indent=2, default=str)
|
||||
if args.output:
|
||||
with open(args.output, "w") as f:
|
||||
f.write(output)
|
||||
print(f"[*] JSON results written to: {args.output}")
|
||||
else:
|
||||
print(output)
|
||||
else:
|
||||
print(f"\n[*] DCOM Lateral Movement Detection Report")
|
||||
print(f"[*] Scan Time: {all_results['scan_time']}")
|
||||
print(f"[*] Events Analyzed: {all_results['total_events_parsed']}")
|
||||
|
||||
print_findings(process_findings, "DCOM Process Creation Detections")
|
||||
print_findings(network_findings, "DCOM Network Connection Detections")
|
||||
print_correlated(correlated)
|
||||
|
||||
print(f"\n{'=' * 80}")
|
||||
print(f" SUMMARY")
|
||||
print(f"{'=' * 80}")
|
||||
s = all_results["summary"]
|
||||
print(f" Process Creation Detections: {s['process_detections']}")
|
||||
print(f" Network Connection Detections: {s['network_detections']}")
|
||||
print(f" Correlated Lateral Movement Chains: {s['correlated_chains']}")
|
||||
print(f" Critical Findings: {s['critical_findings']}")
|
||||
print(f" High Findings: {s['high_findings']}")
|
||||
|
||||
if args.output:
|
||||
with open(args.output, "w") as f:
|
||||
json.dump(all_results, f, indent=2, default=str)
|
||||
print(f"\n[*] Full results written to: {args.output}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user