mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-06-14 06:54:57 +03:00
202 lines
8.3 KiB
Python
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()
|