Files
Anthropic-Cybersecurity-Skills/skills/detecting-golden-ticket-forgery/scripts/agent.py
T

202 lines
8.3 KiB
Python

#!/usr/bin/env python3
"""Detect Kerberos Golden Ticket forgery via Windows Security event log analysis."""
import json
import argparse
import xml.etree.ElementTree as ET
from collections import defaultdict
from datetime import datetime
def parse_security_events(xml_path):
"""Parse exported Windows Security event log XML for Kerberos events 4768/4769."""
tree = ET.parse(xml_path)
root = tree.getroot()
ns = {"e": "http://schemas.microsoft.com/win/2004/08/events/event"}
events = []
for event_el in root.findall(".//e:Event", ns):
sys_el = event_el.find("e:System", ns)
event_id = int(sys_el.find("e:EventID", ns).text)
if event_id not in (4768, 4769):
continue
time_created = sys_el.find("e:TimeCreated", ns).attrib.get("SystemTime", "")
data_el = event_el.find("e:EventData", ns)
fields = {}
for d in data_el.findall("e:Data", ns):
fields[d.attrib.get("Name", "")] = d.text or ""
events.append({"event_id": event_id, "timestamp": time_created, **fields})
return events
def detect_rc4_in_aes_environment(events):
"""Detect RC4 encryption (0x17) in TGS requests where AES should be enforced."""
alerts = []
for ev in events:
if ev["event_id"] != 4769:
continue
enc_type = ev.get("TicketEncryptionType", "")
if enc_type in ("0x17", "23"):
alerts.append({
"detection": "RC4 Encryption in TGS Request",
"mitre_technique": "T1558.001",
"timestamp": ev["timestamp"],
"user": ev.get("TargetUserName", ""),
"domain": ev.get("TargetDomainName", ""),
"service": ev.get("ServiceName", ""),
"ip_address": ev.get("IpAddress", ""),
"encryption_type": enc_type,
"severity": "critical",
"description": "RC4 (0x17) encryption detected in TGS request; Golden Ticket indicator in AES-enforced environments",
})
return alerts
def detect_orphaned_tgs(events):
"""Detect TGS requests (4769) without preceding TGT request (4768) from same user."""
tgt_users = set()
for ev in events:
if ev["event_id"] == 4768:
tgt_users.add(f"{ev.get('TargetUserName', '')}@{ev.get('TargetDomainName', '')}")
alerts = []
tgs_without_tgt = defaultdict(list)
for ev in events:
if ev["event_id"] != 4769:
continue
user_key = f"{ev.get('TargetUserName', '')}@{ev.get('TargetDomainName', '')}"
if user_key not in tgt_users and ev.get("TargetUserName", ""):
tgs_without_tgt[user_key].append(ev)
for user, tgs_events in tgs_without_tgt.items():
alerts.append({
"detection": "Orphaned TGS Request (No Preceding TGT)",
"mitre_technique": "T1558.001",
"user": user,
"tgs_count": len(tgs_events),
"services": list({e.get("ServiceName", "") for e in tgs_events}),
"source_ips": list({e.get("IpAddress", "") for e in tgs_events}),
"first_seen": tgs_events[0]["timestamp"],
"severity": "critical",
"description": "TGS requests without corresponding TGT; forged ticket likely",
})
return alerts
def detect_abnormal_ticket_lifetime(events, max_lifetime_hours=10):
"""Detect tickets with lifetime exceeding domain policy (default MaxTicketAge=10h)."""
user_tgt_times = defaultdict(list)
for ev in events:
if ev["event_id"] == 4768 and ev.get("TargetUserName"):
try:
ts = datetime.fromisoformat(ev["timestamp"].replace("Z", "+00:00"))
user_tgt_times[ev["TargetUserName"]].append(ts)
except (ValueError, AttributeError):
continue
alerts = []
for user, times in user_tgt_times.items():
if len(times) < 2:
continue
times.sort()
for i in range(1, len(times)):
gap_hours = (times[i] - times[i - 1]).total_seconds() / 3600
if gap_hours > max_lifetime_hours * 2:
alerts.append({
"detection": "Abnormal TGT Renewal Gap",
"mitre_technique": "T1558.001",
"user": user,
"gap_hours": round(gap_hours, 2),
"max_expected_hours": max_lifetime_hours,
"severity": "high",
"description": f"TGT renewal gap of {gap_hours:.1f}h exceeds 2x MaxTicketAge ({max_lifetime_hours}h)",
})
return alerts
def detect_krbtgt_service_anomaly(events):
"""Detect TGS requests targeting the krbtgt service (unusual and suspicious)."""
alerts = []
for ev in events:
if ev["event_id"] == 4769 and ev.get("ServiceName", "").lower().startswith("krbtgt"):
alerts.append({
"detection": "TGS Request Targeting krbtgt Service",
"mitre_technique": "T1558.001",
"timestamp": ev["timestamp"],
"user": ev.get("TargetUserName", ""),
"service": ev.get("ServiceName", ""),
"ip_address": ev.get("IpAddress", ""),
"severity": "critical",
"description": "Direct TGS request for krbtgt service is highly anomalous",
})
return alerts
def generate_splunk_queries():
"""Return Splunk SPL queries for Golden Ticket detection."""
return {
"rc4_downgrade": (
'index=wineventlog sourcetype="WinEventLog:Security" EventCode=4769 '
'TicketEncryptionType=0x17 ServiceName!="krbtgt" '
'| stats count by TargetUserName, IpAddress, ServiceName'
),
"orphaned_tgs": (
'index=wineventlog EventCode=4769 '
'| join type=left TargetUserName [search index=wineventlog EventCode=4768 '
'| rename TargetUserName as tgt_user | dedup tgt_user | fields tgt_user] '
'| where isnull(tgt_user) | stats count by TargetUserName, IpAddress'
),
"krbtgt_tgs": (
'index=wineventlog EventCode=4769 ServiceName="krbtgt*" '
'| table _time, TargetUserName, IpAddress, ServiceName, TicketEncryptionType'
),
}
def main():
parser = argparse.ArgumentParser(description="Golden Ticket Forgery Detector")
parser.add_argument("--evtx-xml", help="Path to exported Security event log XML")
parser.add_argument("--max-ticket-hours", type=int, default=10, help="MaxTicketAge in hours (default: 10)")
parser.add_argument("--output", default="golden_ticket_report.json", help="Output report path")
parser.add_argument("--show-splunk", action="store_true", help="Print Splunk SPL queries")
args = parser.parse_args()
if args.show_splunk:
for name, spl in generate_splunk_queries().items():
print(f"\n--- {name} ---\n{spl}")
return
if not args.evtx_xml:
print("[!] Provide --evtx-xml path or use --show-splunk for detection queries")
return
events = parse_security_events(args.evtx_xml)
print(f"[+] Parsed {len(events)} Kerberos events (4768/4769)")
rc4_alerts = detect_rc4_in_aes_environment(events)
orphan_alerts = detect_orphaned_tgs(events)
lifetime_alerts = detect_abnormal_ticket_lifetime(events, args.max_ticket_hours)
krbtgt_alerts = detect_krbtgt_service_anomaly(events)
report = {
"analysis_time": datetime.utcnow().isoformat() + "Z",
"total_events": len(events),
"detections": {
"rc4_encryption_downgrade": rc4_alerts,
"orphaned_tgs_requests": orphan_alerts,
"abnormal_ticket_lifetime": lifetime_alerts,
"krbtgt_service_anomaly": krbtgt_alerts,
},
"total_alerts": len(rc4_alerts) + len(orphan_alerts) + len(lifetime_alerts) + len(krbtgt_alerts),
"mitre_techniques": ["T1558.001"],
"splunk_queries": generate_splunk_queries(),
}
with open(args.output, "w") as f:
json.dump(report, f, indent=2)
print(f"[+] RC4 downgrades: {len(rc4_alerts)}")
print(f"[+] Orphaned TGS: {len(orphan_alerts)}")
print(f"[+] Lifetime anomalies: {len(lifetime_alerts)}")
print(f"[+] krbtgt anomalies: {len(krbtgt_alerts)}")
print(f"[+] Report saved to {args.output}")
if __name__ == "__main__":
main()