mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-08-28 12:19:41 +03:00
feat: add 5 new cybersecurity skills - Azure storage audit, supply chain simulation, Azure PIM, Empire artifacts, NTLM relay
This commit is contained in:
@@ -0,0 +1,342 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Detect NTLM relay attacks via Windows Event 4624 analysis, IP-hostname correlation, and SMB signing audit."""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, timezone
|
||||
|
||||
|
||||
def query_security_log(event_id, max_events=2000):
|
||||
"""Query Windows Security event log for specific event ID using wevtutil."""
|
||||
cmd = [
|
||||
"wevtutil", "qe", "Security",
|
||||
"/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=120)
|
||||
return proc.stdout
|
||||
except (FileNotFoundError, subprocess.TimeoutExpired):
|
||||
return ""
|
||||
|
||||
|
||||
def parse_4624_events(xml_data):
|
||||
"""Parse Event 4624 logon events from XML output."""
|
||||
events = []
|
||||
if not xml_data:
|
||||
return events
|
||||
|
||||
blocks = re.split(r"<Event\s+xmlns=", xml_data)
|
||||
for block in blocks[1:]:
|
||||
time_match = re.search(r"<TimeCreated\s+SystemTime='([^']+)'", block)
|
||||
computer_match = re.search(r"<Computer>([^<]+)</Computer>", block)
|
||||
|
||||
data_fields = {}
|
||||
for m in re.finditer(r"<Data Name='(\w+)'>([^<]*)</Data>", block):
|
||||
data_fields[m.group(1)] = m.group(2)
|
||||
|
||||
logon_type = data_fields.get("LogonType", "")
|
||||
if logon_type != "3":
|
||||
continue
|
||||
|
||||
auth_package = data_fields.get("AuthenticationPackageName", "")
|
||||
if "NTLM" not in auth_package.upper():
|
||||
continue
|
||||
|
||||
events.append({
|
||||
"timestamp": time_match.group(1) if time_match else "",
|
||||
"computer": computer_match.group(1) if computer_match else "",
|
||||
"target_username": data_fields.get("TargetUserName", ""),
|
||||
"target_domain": data_fields.get("TargetDomainName", ""),
|
||||
"logon_type": logon_type,
|
||||
"auth_package": auth_package,
|
||||
"lm_package": data_fields.get("LmPackageName", ""),
|
||||
"workstation_name": data_fields.get("WorkstationName", ""),
|
||||
"source_ip": data_fields.get("IpAddress", ""),
|
||||
"source_port": data_fields.get("IpPort", ""),
|
||||
"logon_process": data_fields.get("LogonProcessName", ""),
|
||||
"target_sid": data_fields.get("TargetUserSid", ""),
|
||||
"logon_guid": data_fields.get("LogonGuid", ""),
|
||||
"impersonation_level": data_fields.get("ImpersonationLevel", "")
|
||||
})
|
||||
|
||||
return events
|
||||
|
||||
|
||||
def parse_5145_events(xml_data):
|
||||
"""Parse Event 5145 network share access events for named pipe monitoring."""
|
||||
events = []
|
||||
if not xml_data:
|
||||
return events
|
||||
|
||||
suspicious_pipes = [
|
||||
"spoolss", "netdfs", "lsarpc", "lsass", "netlogon", "samr",
|
||||
"efsrpc", "fssagentrpc", "eventlog", "winreg", "srvsvc",
|
||||
"dnsserver", "dhcpserver", "winspipe"
|
||||
]
|
||||
|
||||
blocks = re.split(r"<Event\s+xmlns=", xml_data)
|
||||
for block in blocks[1:]:
|
||||
time_match = re.search(r"<TimeCreated\s+SystemTime='([^']+)'", block)
|
||||
data_fields = {}
|
||||
for m in re.finditer(r"<Data Name='(\w+)'>([^<]*)</Data>", block):
|
||||
data_fields[m.group(1)] = m.group(2)
|
||||
|
||||
share_name = data_fields.get("ShareName", "").lower()
|
||||
relative_target = data_fields.get("RelativeTargetName", "").lower()
|
||||
|
||||
if any(pipe in relative_target for pipe in suspicious_pipes):
|
||||
events.append({
|
||||
"timestamp": time_match.group(1) if time_match else "",
|
||||
"subject_username": data_fields.get("SubjectUserName", ""),
|
||||
"subject_domain": data_fields.get("SubjectDomainName", ""),
|
||||
"source_ip": data_fields.get("IpAddress", ""),
|
||||
"share_name": share_name,
|
||||
"pipe_name": relative_target
|
||||
})
|
||||
|
||||
return events
|
||||
|
||||
|
||||
def detect_ip_hostname_mismatch(events):
|
||||
"""Detect when WorkstationName IP doesn't match source IpAddress."""
|
||||
findings = []
|
||||
hostname_ip_map = defaultdict(set)
|
||||
|
||||
# Build hostname-to-IP baseline
|
||||
for event in events:
|
||||
hostname = event.get("workstation_name", "").upper()
|
||||
ip = event.get("source_ip", "")
|
||||
if hostname and ip and ip != "-" and ip != "::1" and ip != "127.0.0.1":
|
||||
hostname_ip_map[hostname].add(ip)
|
||||
|
||||
# Detect hostnames authenticating from multiple IPs
|
||||
for hostname, ips in hostname_ip_map.items():
|
||||
if len(ips) > 2:
|
||||
findings.append({
|
||||
"check": "multiple_source_ips",
|
||||
"severity": "High",
|
||||
"hostname": hostname,
|
||||
"source_ips": list(ips),
|
||||
"ip_count": len(ips),
|
||||
"description": f"Workstation '{hostname}' authenticating via NTLM from {len(ips)} different IPs — possible relay",
|
||||
"mitre": "T1557.001"
|
||||
})
|
||||
|
||||
return findings
|
||||
|
||||
|
||||
def detect_machine_account_relay(events):
|
||||
"""Detect machine accounts authenticating from unexpected IPs."""
|
||||
findings = []
|
||||
machine_events = [e for e in events if e["target_username"].endswith("$")]
|
||||
machine_ip_map = defaultdict(set)
|
||||
|
||||
for event in machine_events:
|
||||
machine_ip_map[event["target_username"]].add(event["source_ip"])
|
||||
|
||||
for machine, ips in machine_ip_map.items():
|
||||
if len(ips) > 1:
|
||||
findings.append({
|
||||
"check": "machine_account_multi_ip",
|
||||
"severity": "Critical",
|
||||
"machine_account": machine,
|
||||
"source_ips": list(ips),
|
||||
"description": f"Machine account '{machine}' authenticated from {len(ips)} different IPs",
|
||||
"mitre": "T1557.001"
|
||||
})
|
||||
|
||||
return findings
|
||||
|
||||
|
||||
def detect_rapid_authentication(events, window_seconds=5, threshold=5):
|
||||
"""Detect rapid multi-host authentication from a single account."""
|
||||
findings = []
|
||||
user_events = defaultdict(list)
|
||||
|
||||
for event in events:
|
||||
user_key = f"{event['target_domain']}\\{event['target_username']}"
|
||||
user_events[user_key].append(event)
|
||||
|
||||
for user, user_evts in user_events.items():
|
||||
if user.endswith("$"):
|
||||
continue
|
||||
sorted_evts = sorted(user_evts, key=lambda x: x.get("timestamp", ""))
|
||||
unique_targets = set()
|
||||
window_start = 0
|
||||
|
||||
for i, evt in enumerate(sorted_evts):
|
||||
unique_targets.add(evt.get("computer", ""))
|
||||
if len(unique_targets) >= threshold:
|
||||
findings.append({
|
||||
"check": "rapid_multi_host_auth",
|
||||
"severity": "Critical",
|
||||
"username": user,
|
||||
"target_count": len(unique_targets),
|
||||
"targets": list(unique_targets)[:10],
|
||||
"description": f"User '{user}' authenticated to {len(unique_targets)} hosts in rapid succession",
|
||||
"mitre": "T1557.001"
|
||||
})
|
||||
break
|
||||
|
||||
return findings
|
||||
|
||||
|
||||
def detect_null_sid_logons(events):
|
||||
"""Detect logon events with NULL SID and missing LogonGUID — relay indicator."""
|
||||
findings = []
|
||||
null_events = []
|
||||
|
||||
for event in events:
|
||||
sid = event.get("target_sid", "")
|
||||
guid = event.get("logon_guid", "")
|
||||
if (sid == "S-1-0-0" or sid == "NULL SID" or not sid) and (not guid or guid == "{00000000-0000-0000-0000-000000000000}"):
|
||||
null_events.append(event)
|
||||
|
||||
if null_events:
|
||||
findings.append({
|
||||
"check": "null_sid_logon",
|
||||
"severity": "High",
|
||||
"event_count": len(null_events),
|
||||
"description": f"{len(null_events)} NTLM logon events with NULL SID and empty LogonGUID detected",
|
||||
"sample_events": null_events[:5],
|
||||
"mitre": "T1557.001"
|
||||
})
|
||||
|
||||
return findings
|
||||
|
||||
|
||||
def check_smb_signing(hosts=None):
|
||||
"""Check SMB signing status on specified hosts using PowerShell."""
|
||||
if not hosts:
|
||||
# Get domain computers from AD
|
||||
cmd = [
|
||||
"powershell", "-NoProfile", "-Command",
|
||||
"Get-ADComputer -Filter * -Property DNSHostName | "
|
||||
"Select-Object -ExpandProperty DNSHostName | "
|
||||
"ConvertTo-Json"
|
||||
]
|
||||
else:
|
||||
cmd = ["powershell", "-NoProfile", "-Command",
|
||||
f"'{','.join(hosts)}' -split ',' | ConvertTo-Json"]
|
||||
|
||||
try:
|
||||
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
||||
if proc.returncode != 0:
|
||||
return [{"error": "Failed to enumerate hosts", "stderr": proc.stderr.strip()}]
|
||||
|
||||
host_list = json.loads(proc.stdout) if proc.stdout.strip() else []
|
||||
if isinstance(host_list, str):
|
||||
host_list = [host_list]
|
||||
except Exception as e:
|
||||
return [{"error": str(e)}]
|
||||
|
||||
results = []
|
||||
for host in host_list[:50]:
|
||||
check_cmd = [
|
||||
"powershell", "-NoProfile", "-Command",
|
||||
f"try {{ $smb = Get-SmbServerConfiguration -CimSession '{host}' "
|
||||
f"-ErrorAction Stop; "
|
||||
f"@{{ Host='{host}'; RequireSecuritySignature=$smb.RequireSecuritySignature; "
|
||||
f"EnableSecuritySignature=$smb.EnableSecuritySignature }} | ConvertTo-Json "
|
||||
f"}} catch {{ @{{ Host='{host}'; Error=$_.Exception.Message }} | ConvertTo-Json }}"
|
||||
]
|
||||
try:
|
||||
proc = subprocess.run(check_cmd, capture_output=True, text=True, timeout=10)
|
||||
if proc.stdout.strip():
|
||||
result = json.loads(proc.stdout)
|
||||
if not result.get("RequireSecuritySignature", True):
|
||||
result["severity"] = "High"
|
||||
result["message"] = f"SMB signing not required on {host} — vulnerable to relay"
|
||||
results.append(result)
|
||||
except Exception:
|
||||
results.append({"host": host, "error": "Connection failed"})
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def run_hunt(args):
|
||||
"""Run full NTLM relay detection hunt."""
|
||||
results = {
|
||||
"scan_time": datetime.now(timezone.utc).isoformat(),
|
||||
"events_analyzed": 0,
|
||||
"findings": [],
|
||||
"summary": {"total_findings": 0, "critical": 0, "high": 0, "medium": 0}
|
||||
}
|
||||
|
||||
# Parse Event 4624 NTLM logon events
|
||||
xml_4624 = query_security_log(4624, args.max_events)
|
||||
events = parse_4624_events(xml_4624)
|
||||
results["events_analyzed"] = len(events)
|
||||
|
||||
# Run all detection checks
|
||||
results["findings"].extend(detect_ip_hostname_mismatch(events))
|
||||
results["findings"].extend(detect_machine_account_relay(events))
|
||||
results["findings"].extend(detect_rapid_authentication(events))
|
||||
results["findings"].extend(detect_null_sid_logons(events))
|
||||
|
||||
# Parse Event 5145 for named pipe access
|
||||
if args.check_pipes:
|
||||
xml_5145 = query_security_log(5145, args.max_events)
|
||||
pipe_events = parse_5145_events(xml_5145)
|
||||
if pipe_events:
|
||||
results["findings"].append({
|
||||
"check": "suspicious_pipe_access",
|
||||
"severity": "Medium",
|
||||
"event_count": len(pipe_events),
|
||||
"description": f"{len(pipe_events)} accesses to sensitive named pipes detected",
|
||||
"sample_events": pipe_events[:5],
|
||||
"mitre": "T1557.001"
|
||||
})
|
||||
|
||||
# Check SMB signing if requested
|
||||
if args.check_smb_signing:
|
||||
smb_results = check_smb_signing(args.hosts.split(",") if args.hosts else None)
|
||||
unsigned = [r for r in smb_results if r.get("severity") == "High"]
|
||||
if unsigned:
|
||||
results["findings"].append({
|
||||
"check": "smb_signing_disabled",
|
||||
"severity": "High",
|
||||
"hosts_without_signing": len(unsigned),
|
||||
"details": unsigned[:10],
|
||||
"description": f"{len(unsigned)} hosts do not require SMB signing",
|
||||
"mitre": "T1557.001"
|
||||
})
|
||||
|
||||
# Calculate summary
|
||||
for f in results["findings"]:
|
||||
results["summary"]["total_findings"] += 1
|
||||
sev = f.get("severity", "").lower()
|
||||
if sev in results["summary"]:
|
||||
results["summary"][sev] += 1
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Hunt for NTLM relay attacks in Windows event logs")
|
||||
parser.add_argument("--max-events", type=int, default=2000, help="Max events to query (default: 2000)")
|
||||
parser.add_argument("--check-pipes", action="store_true", help="Also check Event 5145 for named pipe access")
|
||||
parser.add_argument("--check-smb-signing", action="store_true", help="Audit SMB signing on domain hosts")
|
||||
parser.add_argument("--hosts", help="Comma-separated list of hosts to check SMB signing")
|
||||
parser.add_argument("--output", "-o", default="-", help="Output file (default: stdout)")
|
||||
args = parser.parse_args()
|
||||
|
||||
results = run_hunt(args)
|
||||
output = json.dumps(results, indent=2, default=str)
|
||||
|
||||
if args.output == "-":
|
||||
print(output)
|
||||
else:
|
||||
with open(args.output, "w") as f:
|
||||
f.write(output)
|
||||
print(f"Hunt report written to {args.output}", file=sys.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user