mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-06-12 14:14:56 +03:00
336 lines
12 KiB
Python
336 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Lateral Movement Detection Script
|
|
Analyzes Windows authentication logs to detect lateral movement patterns
|
|
including RDP, SMB, WinRM, PsExec, and WMI-based movement.
|
|
"""
|
|
|
|
import json
|
|
import csv
|
|
import argparse
|
|
import datetime
|
|
import re
|
|
from collections import defaultdict
|
|
from pathlib import Path
|
|
|
|
# Lateral movement logon types
|
|
LATERAL_LOGON_TYPES = {
|
|
"3": {"name": "Network", "techniques": ["T1021.002", "T1021.006", "T1047"], "risk_base": 20},
|
|
"10": {"name": "RemoteInteractive", "techniques": ["T1021.001"], "risk_base": 25},
|
|
}
|
|
|
|
# Suspicious account patterns
|
|
SYSTEM_ACCOUNTS = {"system", "anonymous logon", "anonymous", "local service", "network service", "dwm-1", "umfd-0"}
|
|
|
|
# Admin share indicators
|
|
ADMIN_SHARES = {"admin$", "c$", "ipc$", "d$", "e$"}
|
|
|
|
# PsExec and service-based indicators
|
|
SERVICE_LATERAL_PATTERNS = [
|
|
r"psexec", r"PSEXESVC", r"csexec", r"remcom",
|
|
r"cmd\.exe\s+/c", r"powershell.*-enc",
|
|
]
|
|
|
|
|
|
def parse_logs(input_path: str) -> list[dict]:
|
|
"""Parse JSON or CSV log files."""
|
|
path = Path(input_path)
|
|
if path.suffix == ".json":
|
|
with open(path, "r", encoding="utf-8") as f:
|
|
data = json.load(f)
|
|
return data if isinstance(data, list) else data.get("events", [])
|
|
elif path.suffix == ".csv":
|
|
with open(path, "r", encoding="utf-8-sig") as f:
|
|
return [dict(row) for row in csv.DictReader(f)]
|
|
return []
|
|
|
|
|
|
def normalize_event(event: dict) -> dict:
|
|
"""Normalize authentication event fields."""
|
|
field_map = {
|
|
"event_id": ["EventCode", "EventID", "event_id"],
|
|
"logon_type": ["Logon_Type", "LogonType", "logon_type"],
|
|
"account": ["Account_Name", "TargetUserName", "account_name", "user.name"],
|
|
"source_ip": ["Source_Network_Address", "IpAddress", "source_ip", "source.ip"],
|
|
"source_host": ["Workstation_Name", "WorkstationName", "source_host"],
|
|
"dest_host": ["Computer", "hostname", "DeviceName", "host.name"],
|
|
"logon_process": ["Logon_Process", "LogonProcessName", "logon_process"],
|
|
"auth_package": ["Authentication_Package", "AuthenticationPackageName", "auth_package"],
|
|
"share_name": ["Share_Name", "ShareName", "share_name"],
|
|
"service_name": ["Service_Name", "ServiceName", "service_name"],
|
|
"service_path": ["Service_File_Name", "ServiceFileName", "service_path"],
|
|
"process_name": ["Process_Name", "ProcessName", "process_name"],
|
|
"timestamp": ["_time", "timestamp", "Timestamp", "@timestamp", "UtcTime"],
|
|
}
|
|
normalized = {}
|
|
for target, sources in field_map.items():
|
|
for src in sources:
|
|
if src in event and event[src]:
|
|
normalized[target] = str(event[src]).strip()
|
|
break
|
|
if target not in normalized:
|
|
normalized[target] = ""
|
|
return normalized
|
|
|
|
|
|
def detect_network_logon(event: dict) -> dict | None:
|
|
"""Detect lateral movement via network logon events."""
|
|
event_id = event.get("event_id", "")
|
|
logon_type = event.get("logon_type", "")
|
|
|
|
if event_id != "4624" or logon_type not in LATERAL_LOGON_TYPES:
|
|
return None
|
|
|
|
account = event.get("account", "").lower()
|
|
if account in SYSTEM_ACCOUNTS or account.endswith("$"):
|
|
return None
|
|
|
|
source_ip = event.get("source_ip", "")
|
|
if not source_ip or source_ip in ("-", "::1", "127.0.0.1"):
|
|
return None
|
|
|
|
lt_info = LATERAL_LOGON_TYPES[logon_type]
|
|
risk = lt_info["risk_base"]
|
|
indicators = [f"Logon Type {logon_type} ({lt_info['name']})"]
|
|
|
|
auth_pkg = event.get("auth_package", "").lower()
|
|
if "ntlm" in auth_pkg:
|
|
risk += 10
|
|
indicators.append("NTLM authentication (potential Pass-the-Hash)")
|
|
if "negotiate" in auth_pkg and logon_type == "3":
|
|
indicators.append("Negotiate authentication package")
|
|
|
|
return {
|
|
"detection_type": "NETWORK_LOGON",
|
|
"technique": lt_info["techniques"][0],
|
|
"account": event.get("account", ""),
|
|
"source_ip": source_ip,
|
|
"source_host": event.get("source_host", ""),
|
|
"dest_host": event.get("dest_host", ""),
|
|
"logon_type": logon_type,
|
|
"auth_package": event.get("auth_package", ""),
|
|
"timestamp": event.get("timestamp", ""),
|
|
"risk_score": risk,
|
|
"indicators": indicators,
|
|
}
|
|
|
|
|
|
def detect_explicit_creds(event: dict) -> dict | None:
|
|
"""Detect explicit credential usage (Event 4648)."""
|
|
if event.get("event_id") != "4648":
|
|
return None
|
|
|
|
account = event.get("account", "").lower()
|
|
if account in SYSTEM_ACCOUNTS or account.endswith("$"):
|
|
return None
|
|
|
|
return {
|
|
"detection_type": "EXPLICIT_CREDENTIAL",
|
|
"technique": "T1021",
|
|
"account": event.get("account", ""),
|
|
"source_host": event.get("source_host", event.get("dest_host", "")),
|
|
"dest_host": event.get("dest_host", ""),
|
|
"process_name": event.get("process_name", ""),
|
|
"timestamp": event.get("timestamp", ""),
|
|
"risk_score": 35,
|
|
"indicators": ["Explicit credential logon (4648) - possible PsExec/RunAs"],
|
|
}
|
|
|
|
|
|
def detect_share_access(event: dict) -> dict | None:
|
|
"""Detect admin share access."""
|
|
if event.get("event_id") != "5140":
|
|
return None
|
|
|
|
share = event.get("share_name", "").lower()
|
|
share_name = share.split("\\")[-1] if "\\" in share else share
|
|
|
|
if share_name not in ADMIN_SHARES:
|
|
return None
|
|
|
|
account = event.get("account", "").lower()
|
|
if account in SYSTEM_ACCOUNTS or account.endswith("$"):
|
|
return None
|
|
|
|
risk = 40 if share_name in ("admin$", "c$") else 25
|
|
|
|
return {
|
|
"detection_type": "ADMIN_SHARE_ACCESS",
|
|
"technique": "T1021.002",
|
|
"account": event.get("account", ""),
|
|
"source_ip": event.get("source_ip", ""),
|
|
"dest_host": event.get("dest_host", ""),
|
|
"share": share,
|
|
"timestamp": event.get("timestamp", ""),
|
|
"risk_score": risk,
|
|
"indicators": [f"Admin share accessed: {share_name}"],
|
|
}
|
|
|
|
|
|
def detect_service_lateral(event: dict) -> dict | None:
|
|
"""Detect service-based lateral movement (PsExec)."""
|
|
if event.get("event_id") not in ("7045", "4697"):
|
|
return None
|
|
|
|
service_path = event.get("service_path", "")
|
|
for pattern in SERVICE_LATERAL_PATTERNS:
|
|
if re.search(pattern, service_path, re.IGNORECASE):
|
|
return {
|
|
"detection_type": "SERVICE_LATERAL",
|
|
"technique": "T1569.002",
|
|
"service_name": event.get("service_name", ""),
|
|
"service_path": service_path,
|
|
"dest_host": event.get("dest_host", ""),
|
|
"timestamp": event.get("timestamp", ""),
|
|
"risk_score": 60,
|
|
"indicators": [f"Suspicious service for lateral movement: {pattern}"],
|
|
}
|
|
return None
|
|
|
|
|
|
def build_movement_graph(findings: list[dict]) -> dict:
|
|
"""Build a graph of lateral movement paths."""
|
|
graph = defaultdict(lambda: defaultdict(list))
|
|
for finding in findings:
|
|
src = finding.get("source_ip") or finding.get("source_host", "unknown")
|
|
dst = finding.get("dest_host", "unknown")
|
|
if src and dst and src != dst:
|
|
graph[src][dst].append({
|
|
"account": finding.get("account", ""),
|
|
"technique": finding.get("technique", ""),
|
|
"timestamp": finding.get("timestamp", ""),
|
|
"type": finding.get("detection_type", ""),
|
|
})
|
|
return dict(graph)
|
|
|
|
|
|
def analyze_velocity(findings: list[dict], window_minutes: int = 10, threshold: int = 5) -> list[dict]:
|
|
"""Detect rapid multi-host access patterns."""
|
|
account_events = defaultdict(list)
|
|
for f in findings:
|
|
if f.get("account") and f.get("timestamp"):
|
|
account_events[f["account"]].append(f)
|
|
|
|
velocity_alerts = []
|
|
for account, events in account_events.items():
|
|
events.sort(key=lambda x: x.get("timestamp", ""))
|
|
unique_dests = set()
|
|
window_start = 0
|
|
|
|
for i, event in enumerate(events):
|
|
unique_dests.add(event.get("dest_host", ""))
|
|
if len(unique_dests) >= threshold:
|
|
velocity_alerts.append({
|
|
"detection_type": "VELOCITY_ANOMALY",
|
|
"account": account,
|
|
"unique_destinations": len(unique_dests),
|
|
"destinations": list(unique_dests),
|
|
"risk_score": 80,
|
|
"risk_level": "CRITICAL",
|
|
"indicators": [f"Account accessed {len(unique_dests)} hosts rapidly"],
|
|
})
|
|
break
|
|
|
|
return velocity_alerts
|
|
|
|
|
|
def run_hunt(input_path: str, output_dir: str) -> None:
|
|
"""Execute lateral movement hunt."""
|
|
print(f"[*] Lateral Movement Hunt - {datetime.datetime.now().isoformat()}")
|
|
|
|
events = parse_logs(input_path)
|
|
print(f"[*] Loaded {len(events)} events")
|
|
|
|
findings = []
|
|
stats = defaultdict(int)
|
|
|
|
detectors = [
|
|
detect_network_logon,
|
|
detect_explicit_creds,
|
|
detect_share_access,
|
|
detect_service_lateral,
|
|
]
|
|
|
|
for raw_event in events:
|
|
event = normalize_event(raw_event)
|
|
for detector in detectors:
|
|
result = detector(event)
|
|
if result:
|
|
risk = result["risk_score"]
|
|
result["risk_level"] = (
|
|
"CRITICAL" if risk >= 70 else "HIGH" if risk >= 50
|
|
else "MEDIUM" if risk >= 30 else "LOW"
|
|
)
|
|
findings.append(result)
|
|
stats[result["detection_type"]] += 1
|
|
|
|
# Velocity analysis
|
|
velocity_alerts = analyze_velocity(findings)
|
|
findings.extend(velocity_alerts)
|
|
stats["VELOCITY_ANOMALY"] = len(velocity_alerts)
|
|
|
|
# Build movement graph
|
|
graph = build_movement_graph(findings)
|
|
|
|
# Write output
|
|
output_path = Path(output_dir)
|
|
output_path.mkdir(parents=True, exist_ok=True)
|
|
|
|
with open(output_path / "lateral_movement_findings.json", "w", encoding="utf-8") as f:
|
|
json.dump({
|
|
"hunt_id": f"TH-LATMOV-{datetime.date.today().isoformat()}",
|
|
"total_events": len(events),
|
|
"total_findings": len(findings),
|
|
"statistics": dict(stats),
|
|
"movement_graph": {src: dict(dsts) for src, dsts in graph.items()},
|
|
"findings": findings,
|
|
}, f, indent=2)
|
|
|
|
with open(output_path / "hunt_report.md", "w", encoding="utf-8") as f:
|
|
f.write(f"# Lateral Movement Hunt Report\n\n")
|
|
f.write(f"**Date**: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
|
|
f.write(f"**Events**: {len(events)} | **Findings**: {len(findings)}\n\n")
|
|
f.write("## Movement Graph\n\n")
|
|
for src, dests in graph.items():
|
|
for dst, connections in dests.items():
|
|
f.write(f"- `{src}` -> `{dst}` ({len(connections)} connections)\n")
|
|
f.write("\n## Velocity Anomalies\n\n")
|
|
for alert in velocity_alerts:
|
|
f.write(f"- **{alert['account']}**: {alert['unique_destinations']} hosts in short window\n")
|
|
|
|
print(f"[+] {len(findings)} findings, {len(graph)} source nodes in movement graph")
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Lateral Movement Detection")
|
|
subparsers = parser.add_subparsers(dest="command")
|
|
|
|
hunt_p = subparsers.add_parser("hunt")
|
|
hunt_p.add_argument("--input", "-i", required=True)
|
|
hunt_p.add_argument("--output", "-o", default="./latmov_output")
|
|
|
|
subparsers.add_parser("queries", help="Print Splunk SPL queries")
|
|
|
|
args = parser.parse_args()
|
|
|
|
if args.command == "hunt":
|
|
run_hunt(args.input, args.output)
|
|
elif args.command == "queries":
|
|
print("=== Splunk Lateral Movement Queries ===\n")
|
|
queries = {
|
|
"Network Logons": 'index=wineventlog EventCode=4624 Logon_Type=3\n| where NOT match(Account_Name, "(?i)(SYSTEM|ANONYMOUS|\\\\$)")\n| stats count dc(Computer) by Account_Name Source_Network_Address\n| where count > 3',
|
|
"RDP Sessions": 'index=wineventlog EventCode=4624 Logon_Type=10\n| stats count by Account_Name Source_Network_Address Computer',
|
|
"Admin Shares": 'index=wineventlog EventCode=5140 Share_Name IN ("*ADMIN$","*C$")\n| stats count by Account_Name Source_Address Computer Share_Name',
|
|
"PsExec Services": 'index=wineventlog EventCode=7045\n| where match(Service_File_Name, "(?i)(psexec|PSEXESVC)")\n| table _time Computer Service_Name Service_File_Name',
|
|
}
|
|
for name, query in queries.items():
|
|
print(f"--- {name} ---")
|
|
print(query)
|
|
print()
|
|
else:
|
|
parser.print_help()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|