#!/usr/bin/env python3 """Detect PowerShell Empire framework artifacts in Windows event logs.""" import argparse import base64 import json import re import subprocess import sys from datetime import datetime, timezone EMPIRE_LAUNCHER_PATTERN = re.compile( r"powershell\s+-noP\s+-sta\s+-w\s+1\s+-enc\s+", re.IGNORECASE ) EMPIRE_STAGER_PATTERNS = [ re.compile(r"System\.Net\.WebClient", re.IGNORECASE), re.compile(r"\.DownloadString\(", re.IGNORECASE), re.compile(r"\.DownloadData\(", re.IGNORECASE), re.compile(r"FromBase64String", re.IGNORECASE), re.compile(r"IEX\s*\(", re.IGNORECASE), re.compile(r"Invoke-Expression", re.IGNORECASE), re.compile(r"New-Object\s+System\.Net\.WebClient", re.IGNORECASE), re.compile(r"\[System\.Convert\]::FromBase64String", re.IGNORECASE), ] EMPIRE_MODULE_SIGNATURES = [ "Invoke-Mimikatz", "Invoke-Kerberoast", "Invoke-TokenManipulation", "Invoke-PSInject", "Invoke-DCOM", "Invoke-RunAs", "Invoke-PSRemoting", "Invoke-SessionGopher", "Invoke-ReflectivePEInjection", "Install-SSP", "New-GPOImmediateTask", "Get-Keystrokes", "Get-Screenshot", "Get-ClipboardContents", "Invoke-Portscan", "Invoke-SMBExec", "Invoke-WMIExec", ] EMPIRE_DEFAULT_URIS = [ "/login/process.php", "/admin/get.php", "/admin/news.php", "/news.php", "/login/process.jsp", ] EMPIRE_DEFAULT_USER_AGENTS = [ "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko", "Mozilla/5.0 (compatible, MSIE 11, Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko", ] def query_event_log(event_id, log_name="Microsoft-Windows-PowerShell/Operational", max_events=1000): """Query Windows Event Log for specific event ID using wevtutil.""" cmd = [ "wevtutil", "qe", log_name, "/q:*[System[(EventID={})]]".format(event_id), "/c:{}".format(max_events), "/f:xml", "/rd:true" ] try: proc = subprocess.run(cmd, capture_output=True, text=True, timeout=60) return proc.stdout except FileNotFoundError: return "" except subprocess.TimeoutExpired: return "" def parse_script_block_events(xml_data): """Parse Event ID 4104 Script Block Logging events from XML.""" events = [] if not xml_data: return events blocks = re.split(r"([^<]+)", block) script_match = re.search(r"([^<]+)", block) sid_match = re.search(r"= 2: findings.append({ "indicator": "empire_stager_patterns", "severity": "High", "description": f"Multiple Empire stager patterns detected: {', '.join(matched_stagers[:5])}", "matched_count": len(matched_stagers), "mitre": "T1059.001" }) # Check for known Empire module signatures for module in EMPIRE_MODULE_SIGNATURES: if module.lower() in script_text.lower(): findings.append({ "indicator": "empire_module", "severity": "Critical", "module_name": module, "description": f"Empire post-exploitation module detected: {module}", "mitre": "T1059.001" }) # Check for Empire default URIs for uri in EMPIRE_DEFAULT_URIS: if uri in script_text: findings.append({ "indicator": "empire_staging_uri", "severity": "High", "uri": uri, "description": f"Default Empire staging URI detected: {uri}", "mitre": "T1071.001" }) # Check for Empire user agents for ua in EMPIRE_DEFAULT_USER_AGENTS: if ua in script_text: findings.append({ "indicator": "empire_default_useragent", "severity": "Medium", "user_agent": ua, "description": "Default Empire HTTP listener user agent detected", "mitre": "T1071.001" }) # Check for encoded command patterns b64_blocks = re.findall(r"[A-Za-z0-9+/]{100,}={0,2}", script_text) for b64 in b64_blocks[:5]: decoded = decode_base64_payload(b64) if decoded and any(p.search(decoded) for p in EMPIRE_STAGER_PATTERNS): findings.append({ "indicator": "encoded_empire_payload", "severity": "Critical", "description": "Base64 encoded payload contains Empire stager patterns", "decoded_preview": decoded[:300], "mitre": "T1027" }) return findings def scan_event_logs(max_events=1000): """Scan PowerShell event logs for Empire artifacts.""" results = { "scan_time": datetime.now(timezone.utc).isoformat(), "events_analyzed": 0, "suspicious_events": [], "summary": { "total_findings": 0, "critical": 0, "high": 0, "medium": 0 } } # Query Event ID 4104 (Script Block Logging) xml_data = query_event_log(4104, "Microsoft-Windows-PowerShell/Operational", max_events) events = parse_script_block_events(xml_data) results["events_analyzed"] = len(events) for event in events: findings = analyze_script_block(event["script_block"]) if findings: results["suspicious_events"].append({ "timestamp": event["timestamp"], "computer": event["computer"], "user_sid": event["user_sid"], "findings": findings, "script_preview": event["script_block"][:200] }) for f in findings: results["summary"]["total_findings"] += 1 sev = f.get("severity", "").lower() if sev in results["summary"]: results["summary"][sev] += 1 # Also check Event ID 4103 (Module Logging) xml_4103 = query_event_log(4103, "Microsoft-Windows-PowerShell/Operational", max_events) events_4103 = parse_script_block_events(xml_4103) for event in events_4103: for module in EMPIRE_MODULE_SIGNATURES: if module.lower() in event.get("script_block", "").lower(): results["suspicious_events"].append({ "timestamp": event["timestamp"], "computer": event["computer"], "event_id": 4103, "findings": [{ "indicator": "empire_module_in_module_log", "severity": "Critical", "module_name": module, "mitre": "T1059.001" }] }) results["summary"]["total_findings"] += 1 results["summary"]["critical"] += 1 return results def analyze_script_file(filepath): """Analyze a PowerShell script file or exported log for Empire artifacts.""" with open(filepath, "r", encoding="utf-8", errors="replace") as f: content = f.read() findings = analyze_script_block(content) return { "file": filepath, "scan_time": datetime.now(timezone.utc).isoformat(), "findings": findings, "finding_count": len(findings) } def main(): parser = argparse.ArgumentParser( description="Detect PowerShell Empire artifacts in event logs and scripts" ) subparsers = parser.add_subparsers(dest="command", help="Analysis mode") scan_parser = subparsers.add_parser("scan-logs", help="Scan Windows event logs for Empire IOCs") scan_parser.add_argument("--max-events", type=int, default=1000, help="Max events to query (default: 1000)") file_parser = subparsers.add_parser("analyze-file", help="Analyze a PowerShell script or log file") file_parser.add_argument("file", help="Path to script or log file") decode_parser = subparsers.add_parser("decode", help="Decode Base64 encoded PowerShell payload") decode_parser.add_argument("payload", help="Base64 encoded string") args = parser.parse_args() if args.command == "scan-logs": result = scan_event_logs(args.max_events) elif args.command == "analyze-file": result = analyze_script_file(args.file) elif args.command == "decode": decoded = decode_base64_payload(args.payload) result = { "encoded": args.payload[:100] + "..." if len(args.payload) > 100 else args.payload, "decoded": decoded, "empire_indicators": analyze_script_block(decoded) if decoded else [] } else: parser.print_help() sys.exit(0) print(json.dumps(result, indent=2, default=str)) if __name__ == "__main__": main()