Files

218 lines
7.7 KiB
Python

#!/usr/bin/env python3
"""Sysmon scheduled task detection agent for hunting malicious persistence."""
import json
import sys
import argparse
import re
import base64
import xml.etree.ElementTree as ET
from datetime import datetime
from collections import defaultdict
SUSPICIOUS_PATHS = [
r"\\users\\public\\", r"\\programdata\\", r"\\windows\\temp\\",
r"\\appdata\\local\\temp\\", r"\\downloads\\", r"\\desktop\\",
r"c:\\temp\\", r"\\recycle",
]
SUSPICIOUS_COMMANDS = [
r"powershell.*-enc", r"powershell.*-e\s+", r"powershell.*downloadstring",
r"powershell.*iex", r"powershell.*invoke-expression",
r"cmd.*/c\s+", r"mshta\s+", r"certutil.*-urlcache",
r"bitsadmin.*/transfer", r"regsvr32.*/s.*/u",
r"rundll32.*javascript", r"wscript.*\.vbs",
]
def parse_evtx_xml(xml_path):
"""Parse exported Windows Event Log XML for Sysmon and Security events."""
events = []
try:
tree = ET.parse(xml_path)
root = tree.getroot()
ns = {"e": "http://schemas.microsoft.com/win/2004/08/events/event"}
for event_el in root.findall(".//e:Event", ns):
system = event_el.find("e:System", ns)
event_data = event_el.find("e:EventData", ns)
if system is None:
continue
event_id = int(system.findtext("e:EventID", "0", ns))
data = {}
if event_data is not None:
for d in event_data.findall("e:Data", ns):
name = d.get("Name", "")
data[name] = d.text or ""
events.append({
"event_id": event_id,
"timestamp": system.findtext("e:TimeCreated/@SystemTime", "", ns)
or system.find("e:TimeCreated", ns).get("SystemTime", "") if system.find("e:TimeCreated", ns) is not None else "",
"computer": system.findtext("e:Computer", "", ns),
"data": data,
})
except ET.ParseError as e:
return [{"error": f"XML parse error: {e}"}]
return events
def detect_schtasks_creation(events):
"""Detect suspicious schtasks.exe process creation (Sysmon Event 1)."""
findings = []
for evt in events:
if evt["event_id"] != 1:
continue
image = evt["data"].get("Image", "").lower()
cmdline = evt["data"].get("CommandLine", "")
parent = evt["data"].get("ParentImage", "")
if "schtasks" not in image and "at.exe" not in image:
continue
if "/create" not in cmdline.lower() and "/change" not in cmdline.lower():
continue
severity = "MEDIUM"
reasons = []
for pattern in SUSPICIOUS_PATHS:
if re.search(pattern, cmdline, re.IGNORECASE):
severity = "HIGH"
reasons.append(f"Task executes from suspicious path: {pattern}")
for pattern in SUSPICIOUS_COMMANDS:
if re.search(pattern, cmdline, re.IGNORECASE):
severity = "CRITICAL"
reasons.append(f"Suspicious command pattern: {pattern}")
if "/s " in cmdline.lower() or "/s\t" in cmdline:
severity = "CRITICAL"
reasons.append("Remote task creation detected (lateral movement)")
if "-enc" in cmdline.lower() or "-e " in cmdline.lower():
encoded = re.search(r'-[eE](?:nc)?\s+([A-Za-z0-9+/=]{20,})', cmdline)
if encoded:
try:
decoded = base64.b64decode(encoded.group(1)).decode("utf-16-le", errors="replace")
reasons.append(f"Decoded command: {decoded[:150]}")
except Exception:
pass
if not reasons:
reasons.append("Scheduled task creation detected")
findings.append({
"timestamp": evt["timestamp"],
"computer": evt["computer"],
"image": image,
"command_line": cmdline[:300],
"parent_process": parent,
"user": evt["data"].get("User", ""),
"severity": severity,
"reasons": reasons,
"mitre": "T1053.005",
})
return findings
def detect_task_file_creation(events):
"""Detect task XML file creation in System32\\Tasks (Sysmon Event 11)."""
findings = []
for evt in events:
if evt["event_id"] != 11:
continue
target = evt["data"].get("TargetFilename", "")
if "\\windows\\system32\\tasks\\" not in target.lower():
continue
process = evt["data"].get("Image", "")
findings.append({
"timestamp": evt["timestamp"],
"task_file": target,
"created_by": process,
"severity": "MEDIUM",
"detail": "New scheduled task XML file created",
})
return findings
def detect_event_4698(events):
"""Detect Security Event 4698 — scheduled task registered."""
findings = []
for evt in events:
if evt["event_id"] != 4698:
continue
task_name = evt["data"].get("TaskName", "")
task_content = evt["data"].get("TaskContent", "")
user = evt["data"].get("SubjectUserName", "")
severity = "MEDIUM"
reasons = []
for pattern in SUSPICIOUS_COMMANDS:
if re.search(pattern, task_content, re.IGNORECASE):
severity = "CRITICAL"
reasons.append(f"Task content contains: {pattern}")
findings.append({
"timestamp": evt["timestamp"],
"task_name": task_name,
"registered_by": user,
"severity": severity,
"reasons": reasons or ["New task registered"],
"task_content_preview": task_content[:200],
})
return findings
def run_audit(args):
"""Execute scheduled task detection audit."""
print(f"\n{'='*60}")
print(f" MALICIOUS SCHEDULED TASK DETECTION")
print(f" Generated: {datetime.utcnow().isoformat()} UTC")
print(f"{'='*60}\n")
report = {}
if args.evtx_xml:
events = parse_evtx_xml(args.evtx_xml)
report["total_events"] = len(events)
print(f"Parsed {len(events)} events from {args.evtx_xml}\n")
schtask_findings = detect_schtasks_creation(events)
report["schtasks_findings"] = schtask_findings
print(f"--- SCHTASKS CREATION (Event 1) — {len(schtask_findings)} findings ---")
for f in schtask_findings[:15]:
print(f" [{f['severity']}] {f['computer']}: {f['command_line'][:80]}")
for r in f["reasons"]:
print(f" -> {r[:100]}")
file_findings = detect_task_file_creation(events)
report["task_file_findings"] = file_findings
print(f"\n--- TASK FILE CREATION (Event 11) — {len(file_findings)} findings ---")
for f in file_findings[:10]:
print(f" [{f['severity']}] {f['task_file']}")
reg_findings = detect_event_4698(events)
report["event_4698_findings"] = reg_findings
print(f"\n--- TASK REGISTRATION (Event 4698) — {len(reg_findings)} findings ---")
for f in reg_findings[:10]:
print(f" [{f['severity']}] {f['task_name']} by {f['registered_by']}")
return report
def main():
parser = argparse.ArgumentParser(description="Sysmon Scheduled Task Detection Agent")
parser.add_argument("--evtx-xml", required=True,
help="Exported event log XML file to analyze")
parser.add_argument("--output", help="Save report to JSON file")
args = parser.parse_args()
report = run_audit(args)
if args.output:
with open(args.output, "w") as f:
json.dump(report, f, indent=2, default=str)
print(f"\n[+] Report saved to {args.output}")
if __name__ == "__main__":
main()