Files
Anthropic-Cybersecurity-Skills/skills/detecting-wmi-persistence/scripts/agent.py
T

208 lines
8.7 KiB
Python

#!/usr/bin/env python3
"""WMI Persistence Detection Agent - hunts for malicious WMI event subscriptions via Sysmon and WMI queries."""
import json
import argparse
import logging
import subprocess
import re
import xml.etree.ElementTree as ET
from collections import defaultdict
from datetime import datetime
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger(__name__)
REPLICATION_GUIDS = {
"1131f6aa-9c07-11d1-f79f-00c04fc2dcd2": "DS-Replication-Get-Changes",
"1131f6ad-9c07-11d1-f79f-00c04fc2dcd2": "DS-Replication-Get-Changes-All",
}
SUSPICIOUS_CONSUMERS = ["CommandLineEventConsumer", "ActiveScriptEventConsumer"]
KNOWN_GOOD_FILTERS = [
"SCM Event Log Filter",
"BVTFilter",
"TSLogonFilter",
]
def query_sysmon_wmi_events(evtx_path=None, hours_back=72):
"""Query Sysmon Event IDs 19, 20, 21 for WMI persistence."""
events = []
for event_id in [19, 20, 21]:
cmd = [
"wevtutil", "qe", "Microsoft-Windows-Sysmon/Operational",
"/q:*[System[EventID={}]]".format(event_id),
"/f:xml", "/c:500",
]
if evtx_path:
cmd = ["wevtutil", "qe", evtx_path, "/lf:true",
"/q:*[System[EventID={}]]".format(event_id), "/f:xml", "/c:500"]
result = subprocess.run(cmd, capture_output=True, text=True)
for event_xml in re.findall(r"<Event.*?</Event>", result.stdout, re.DOTALL):
try:
root = ET.fromstring(event_xml)
ns = {"s": "http://schemas.microsoft.com/win/2004/08/events/event"}
data = {}
for el in root.findall(".//s:Data", ns):
data[el.get("Name", "")] = el.text or ""
events.append({
"event_id": event_id,
"timestamp": root.findtext(".//s:TimeCreated/@SystemTime", "", ns),
"computer": root.findtext(".//s:Computer", "", ns),
"operation": data.get("Operation", ""),
"event_type": data.get("EventType", ""),
"consumer_type": data.get("Type", ""),
"name": data.get("Name", ""),
"destination": data.get("Destination", ""),
"query": data.get("Query", ""),
"user": data.get("User", ""),
"raw_data": data,
})
except ET.ParseError:
continue
logger.info("Parsed %d Sysmon WMI events (IDs 19/20/21)", len(events))
return events
def enumerate_wmi_subscriptions():
"""Enumerate active WMI event subscriptions via PowerShell."""
subscriptions = {"filters": [], "consumers": [], "bindings": []}
ps_commands = {
"filters": "Get-WmiObject -Namespace root\\subscription -Class __EventFilter | Select Name, Query, QueryLanguage | ConvertTo-Json",
"consumers": "Get-WmiObject -Namespace root\\subscription -Class __EventConsumer | Select __CLASS, Name, CommandLineTemplate, ScriptText | ConvertTo-Json",
"bindings": "Get-WmiObject -Namespace root\\subscription -Class __FilterToConsumerBinding | Select Filter, Consumer | ConvertTo-Json",
}
for category, ps_cmd in ps_commands.items():
cmd = ["powershell", "-Command", ps_cmd]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.stdout.strip():
try:
data = json.loads(result.stdout)
if isinstance(data, dict):
data = [data]
subscriptions[category] = data
except json.JSONDecodeError:
pass
return subscriptions
def analyze_suspicious_subscriptions(subscriptions):
"""Identify suspicious WMI subscriptions."""
findings = []
for consumer in subscriptions.get("consumers", []):
consumer_class = consumer.get("__CLASS", "")
name = consumer.get("Name", "")
if consumer_class in SUSPICIOUS_CONSUMERS:
severity = "critical"
cmd_template = consumer.get("CommandLineTemplate", "")
script_text = consumer.get("ScriptText", "")
payload = cmd_template or script_text
if any(kw in payload.lower() for kw in ["powershell", "cmd.exe", "wscript", "cscript", "mshta", "certutil", "bitsadmin"]):
severity = "critical"
elif payload:
severity = "high"
findings.append({
"type": "suspicious_consumer",
"consumer_class": consumer_class,
"name": name,
"payload": payload[:500],
"severity": severity,
"mitre_technique": "T1546.003",
})
for filt in subscriptions.get("filters", []):
name = filt.get("Name", "")
query = filt.get("Query", "")
if name not in KNOWN_GOOD_FILTERS:
if any(kw in query.lower() for kw in ["win32_processstarttr", "__instancecreationevent", "win32_logonsession"]):
findings.append({
"type": "suspicious_filter",
"name": name,
"wql_query": query,
"severity": "high",
"mitre_technique": "T1546.003",
})
return findings
def analyze_sysmon_events(events):
"""Analyze Sysmon WMI events for suspicious patterns."""
findings = []
for event in events:
eid = event["event_id"]
if eid == 20 and event.get("consumer_type") in SUSPICIOUS_CONSUMERS:
destination = event.get("destination", "")
suspicious_cmds = ["powershell", "cmd.exe", "wscript", "mshta", "certutil", "regsvr32"]
if any(cmd in destination.lower() for cmd in suspicious_cmds):
findings.append({
"type": "sysmon_suspicious_consumer",
"event_id": eid,
"consumer_type": event["consumer_type"],
"destination": destination[:500],
"computer": event["computer"],
"timestamp": event["timestamp"],
"user": event["user"],
"severity": "critical",
})
if eid == 19:
query = event.get("query", "")
if "__instancecreationevent" in query.lower() or "win32_processstarttr" in query.lower():
findings.append({
"type": "sysmon_suspicious_filter",
"event_id": eid,
"wql_query": query,
"computer": event["computer"],
"timestamp": event["timestamp"],
"severity": "high",
})
return findings
def generate_report(sysmon_events, live_subscriptions, sysmon_findings, subscription_findings):
"""Generate comprehensive WMI persistence hunt report."""
all_findings = sysmon_findings + subscription_findings
report = {
"timestamp": datetime.utcnow().isoformat(),
"hunt_type": "WMI Event Subscription Persistence (T1546.003)",
"sysmon_events_analyzed": len(sysmon_events),
"live_subscriptions": {
"filters": len(live_subscriptions.get("filters", [])),
"consumers": len(live_subscriptions.get("consumers", [])),
"bindings": len(live_subscriptions.get("bindings", [])),
},
"total_findings": len(all_findings),
"critical_findings": sum(1 for f in all_findings if f.get("severity") == "critical"),
"high_findings": sum(1 for f in all_findings if f.get("severity") == "high"),
"findings": all_findings,
}
return report
def main():
parser = argparse.ArgumentParser(description="WMI Persistence Detection Agent")
parser.add_argument("--evtx", help="Path to exported Sysmon .evtx file (optional)")
parser.add_argument("--skip-live", action="store_true", help="Skip live WMI enumeration")
parser.add_argument("--output", default="wmi_persistence_report.json")
args = parser.parse_args()
sysmon_events = query_sysmon_wmi_events(args.evtx)
sysmon_findings = analyze_sysmon_events(sysmon_events)
live_subs = {}
sub_findings = []
if not args.skip_live:
live_subs = enumerate_wmi_subscriptions()
sub_findings = analyze_suspicious_subscriptions(live_subs)
report = generate_report(sysmon_events, live_subs, sysmon_findings, sub_findings)
with open(args.output, "w") as f:
json.dump(report, f, indent=2, default=str)
logger.info("WMI hunt: %d events, %d findings (%d critical)",
len(sysmon_events), report["total_findings"], report["critical_findings"])
print(json.dumps(report, indent=2, default=str))
if __name__ == "__main__":
main()