mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-06-10 13:14:55 +03:00
Add 5 new cybersecurity skills with full implementations
- implementing-vulnerability-management-with-greenbone: python-gvm GMP API, scan task creation, XML report parsing - detecting-email-account-compromise: Microsoft Graph inbox rules, impossible travel detection, OAuth grant analysis - performing-threat-intelligence-sharing-with-misp: PyMISP event creation, attribute management, sharing validation - analyzing-cobaltstrike-malleable-c2-profiles: dissect.cobaltstrike C2Profile parsing, Suricata rule generation - hunting-for-registry-run-key-persistence: Sysmon Event 13 analysis, T1547.001 detection, Sigma rule generation
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
name: analyzing-network-packets-with-scapy
|
||||
description: Craft, send, sniff, and dissect network packets using Scapy for protocol analysis, network reconnaissance, and traffic anomaly detection in authorized security testing
|
||||
domain: cybersecurity
|
||||
subdomain: network-security
|
||||
tags:
|
||||
- scapy
|
||||
- packet-analysis
|
||||
- network-forensics
|
||||
- protocol-dissection
|
||||
- pcap
|
||||
- traffic-analysis
|
||||
version: "1.0"
|
||||
author: mahipal
|
||||
license: Apache-2.0
|
||||
---
|
||||
|
||||
# Analyzing Network Packets with Scapy
|
||||
|
||||
## Overview
|
||||
|
||||
Scapy is a Python packet manipulation library that enables crafting, sending, sniffing, and dissecting network packets at granular protocol layers. This skill covers using Scapy for security-relevant tasks including TCP/UDP/ICMP packet crafting, pcap file analysis, protocol field extraction, SYN scan implementation, DNS query analysis, and detecting anomalous traffic patterns such as unusually fragmented packets or malformed headers.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Python 3.8+ with `scapy` library installed (`pip install scapy`)
|
||||
- Root/administrator privileges for raw socket operations (sniffing, sending)
|
||||
- Npcap (Windows) or libpcap (Linux) for packet capture
|
||||
- Authorization to perform packet operations on target network
|
||||
|
||||
## Steps
|
||||
|
||||
1. Read and parse pcap/pcapng files with `rdpcap()` for offline analysis
|
||||
2. Extract protocol layers (IP, TCP, UDP, DNS, HTTP) and field values
|
||||
3. Compute traffic statistics: top talkers, protocol distribution, port frequency
|
||||
4. Detect SYN flood patterns by analyzing TCP flag ratios
|
||||
5. Identify DNS exfiltration indicators via query length and entropy analysis
|
||||
6. Craft custom probe packets for authorized network testing
|
||||
7. Export findings as structured JSON report
|
||||
|
||||
## Expected Output
|
||||
|
||||
JSON report containing packet statistics, protocol distribution, top source/destination IPs, detected anomalies (SYN floods, DNS tunneling indicators, fragmentation attacks), and per-flow summaries.
|
||||
@@ -0,0 +1,90 @@
|
||||
# Scapy Network Packet Analysis API Reference
|
||||
|
||||
## Core Scapy Functions
|
||||
|
||||
### Reading Packets
|
||||
```python
|
||||
from scapy.all import rdpcap, sniff, wrpcap
|
||||
|
||||
# Read pcap file
|
||||
packets = rdpcap("capture.pcap")
|
||||
|
||||
# Live sniff with BPF filter (requires root)
|
||||
packets = sniff(filter="tcp port 80", count=100, iface="eth0")
|
||||
|
||||
# Write packets to pcap
|
||||
wrpcap("output.pcap", packets)
|
||||
```
|
||||
|
||||
### Packet Layer Access
|
||||
```python
|
||||
from scapy.all import IP, TCP, UDP, DNS, DNSQR, ICMP
|
||||
|
||||
pkt = packets[0]
|
||||
pkt.haslayer(IP) # Check if layer exists
|
||||
pkt[IP].src # Source IP
|
||||
pkt[IP].dst # Destination IP
|
||||
pkt[TCP].sport # Source port
|
||||
pkt[TCP].dport # Destination port
|
||||
pkt[TCP].flags # TCP flags: S, SA, A, FA, R, PA
|
||||
pkt[DNS].qd.qname # DNS query name
|
||||
pkt[ICMP].type # ICMP type (8=echo request, 0=echo reply)
|
||||
```
|
||||
|
||||
### Packet Crafting
|
||||
```python
|
||||
from scapy.all import IP, TCP, sr1, send
|
||||
|
||||
# SYN probe (authorized testing only)
|
||||
syn = IP(dst="192.168.1.1") / TCP(dport=80, flags="S")
|
||||
response = sr1(syn, timeout=2, verbose=0)
|
||||
|
||||
# ICMP ping
|
||||
ping = IP(dst="192.168.1.1") / ICMP()
|
||||
send(ping, verbose=0)
|
||||
|
||||
# Custom DNS query
|
||||
dns = IP(dst="8.8.8.8") / UDP(dport=53) / DNS(rd=1, qd=DNSQR(qname="example.com"))
|
||||
```
|
||||
|
||||
## Protocol Fields Reference
|
||||
|
||||
### TCP Flags
|
||||
| Flag | Value | Meaning |
|
||||
|------|-------|---------|
|
||||
| S | 0x02 | SYN |
|
||||
| SA | 0x12 | SYN-ACK |
|
||||
| A | 0x10 | ACK |
|
||||
| F | 0x01 | FIN |
|
||||
| R | 0x04 | RST |
|
||||
| P | 0x08 | PSH |
|
||||
|
||||
### ICMP Types
|
||||
| Type | Meaning |
|
||||
|------|---------|
|
||||
| 0 | Echo Reply |
|
||||
| 3 | Destination Unreachable |
|
||||
| 8 | Echo Request |
|
||||
| 11 | Time Exceeded |
|
||||
|
||||
## BPF Filter Syntax
|
||||
```
|
||||
tcp port 443 # TCP traffic on port 443
|
||||
host 10.0.0.1 # All traffic to/from IP
|
||||
src net 192.168.0.0/24 # Source from subnet
|
||||
udp and port 53 # DNS traffic
|
||||
tcp[tcpflags] & tcp-syn != 0 # SYN packets only
|
||||
```
|
||||
|
||||
## CLI Usage
|
||||
|
||||
```bash
|
||||
# Analyze pcap file for anomalies
|
||||
python agent.py --pcap capture.pcap --output report.json
|
||||
|
||||
# Custom thresholds
|
||||
python agent.py --pcap traffic.pcapng --syn-threshold 50 --dns-length 30
|
||||
|
||||
# Port scan detection sensitivity
|
||||
python agent.py --pcap scan.pcap --scan-threshold 10
|
||||
```
|
||||
@@ -0,0 +1,188 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Network packet analysis agent using Scapy for pcap parsing and anomaly detection."""
|
||||
|
||||
import json
|
||||
import math
|
||||
import argparse
|
||||
from collections import defaultdict, Counter
|
||||
from datetime import datetime
|
||||
|
||||
from scapy.all import rdpcap, IP, TCP, UDP, DNS, DNSQR, ICMP, Raw
|
||||
|
||||
|
||||
def load_pcap(filepath):
|
||||
"""Load packets from a pcap/pcapng file."""
|
||||
packets = rdpcap(filepath)
|
||||
print(f"[+] Loaded {len(packets)} packets from {filepath}")
|
||||
return packets
|
||||
|
||||
|
||||
def extract_packet_info(packets):
|
||||
"""Extract structured info from each packet with IP layer."""
|
||||
records = []
|
||||
for pkt in packets:
|
||||
if not pkt.haslayer(IP):
|
||||
continue
|
||||
info = {
|
||||
"src_ip": pkt[IP].src,
|
||||
"dst_ip": pkt[IP].dst,
|
||||
"proto": pkt[IP].proto,
|
||||
"ttl": pkt[IP].ttl,
|
||||
"length": len(pkt),
|
||||
"flags": str(pkt[IP].flags),
|
||||
"timestamp": float(pkt.time),
|
||||
}
|
||||
if pkt.haslayer(TCP):
|
||||
info["src_port"] = pkt[TCP].sport
|
||||
info["dst_port"] = pkt[TCP].dport
|
||||
info["tcp_flags"] = str(pkt[TCP].flags)
|
||||
info["protocol"] = "TCP"
|
||||
elif pkt.haslayer(UDP):
|
||||
info["src_port"] = pkt[UDP].sport
|
||||
info["dst_port"] = pkt[UDP].dport
|
||||
info["protocol"] = "UDP"
|
||||
elif pkt.haslayer(ICMP):
|
||||
info["icmp_type"] = pkt[ICMP].type
|
||||
info["icmp_code"] = pkt[ICMP].code
|
||||
info["protocol"] = "ICMP"
|
||||
else:
|
||||
info["protocol"] = str(pkt[IP].proto)
|
||||
if pkt.haslayer(DNS) and pkt.haslayer(DNSQR):
|
||||
info["dns_query"] = pkt[DNSQR].qname.decode("utf-8", errors="ignore").rstrip(".")
|
||||
info["dns_type"] = pkt[DNSQR].qtype
|
||||
records.append(info)
|
||||
return records
|
||||
|
||||
|
||||
def compute_traffic_stats(records):
|
||||
"""Compute overall traffic statistics."""
|
||||
src_ips = Counter(r["src_ip"] for r in records)
|
||||
dst_ips = Counter(r["dst_ip"] for r in records)
|
||||
protocols = Counter(r["protocol"] for r in records)
|
||||
dst_ports = Counter(r.get("dst_port", 0) for r in records if r.get("dst_port"))
|
||||
total_bytes = sum(r["length"] for r in records)
|
||||
return {
|
||||
"total_packets": len(records),
|
||||
"total_bytes": total_bytes,
|
||||
"unique_src_ips": len(src_ips),
|
||||
"unique_dst_ips": len(dst_ips),
|
||||
"top_src_ips": src_ips.most_common(10),
|
||||
"top_dst_ips": dst_ips.most_common(10),
|
||||
"protocol_distribution": dict(protocols),
|
||||
"top_dst_ports": dst_ports.most_common(10),
|
||||
}
|
||||
|
||||
|
||||
def detect_syn_flood(records, threshold=100):
|
||||
"""Detect SYN flood by counting SYN-only packets per destination IP."""
|
||||
syn_counts = defaultdict(int)
|
||||
synack_counts = defaultdict(int)
|
||||
for r in records:
|
||||
if r.get("tcp_flags") == "S":
|
||||
syn_counts[r["dst_ip"]] += 1
|
||||
elif r.get("tcp_flags") == "SA":
|
||||
synack_counts[r["dst_ip"]] += 1
|
||||
alerts = []
|
||||
for ip, count in syn_counts.items():
|
||||
ack_count = synack_counts.get(ip, 0)
|
||||
ratio = ack_count / count if count > 0 else 1.0
|
||||
if count >= threshold and ratio < 0.3:
|
||||
alerts.append({
|
||||
"detection": "SYN Flood",
|
||||
"target_ip": ip,
|
||||
"syn_count": count,
|
||||
"synack_count": ack_count,
|
||||
"synack_ratio": round(ratio, 4),
|
||||
"severity": "critical",
|
||||
})
|
||||
return alerts
|
||||
|
||||
|
||||
def calculate_entropy(data):
|
||||
"""Calculate Shannon entropy of a string."""
|
||||
if not data:
|
||||
return 0.0
|
||||
freq = Counter(data)
|
||||
length = len(data)
|
||||
return -sum((c / length) * math.log2(c / length) for c in freq.values())
|
||||
|
||||
|
||||
def detect_dns_tunneling(records, length_threshold=50, entropy_threshold=3.5):
|
||||
"""Detect DNS tunneling via long/high-entropy query names."""
|
||||
alerts = []
|
||||
for r in records:
|
||||
query = r.get("dns_query", "")
|
||||
if not query:
|
||||
continue
|
||||
subdomain = query.split(".")[0] if "." in query else query
|
||||
if len(subdomain) >= length_threshold or calculate_entropy(subdomain) >= entropy_threshold:
|
||||
alerts.append({
|
||||
"detection": "DNS Tunneling Indicator",
|
||||
"query": query,
|
||||
"subdomain_length": len(subdomain),
|
||||
"entropy": round(calculate_entropy(subdomain), 4),
|
||||
"src_ip": r["src_ip"],
|
||||
"severity": "high",
|
||||
})
|
||||
return alerts
|
||||
|
||||
|
||||
def detect_port_scan(records, threshold=20):
|
||||
"""Detect port scanning by counting unique destination ports per source IP."""
|
||||
src_ports = defaultdict(set)
|
||||
for r in records:
|
||||
if r.get("tcp_flags") == "S" and r.get("dst_port"):
|
||||
src_ports[r["src_ip"]].add(r["dst_port"])
|
||||
alerts = []
|
||||
for ip, ports in src_ports.items():
|
||||
if len(ports) >= threshold:
|
||||
alerts.append({
|
||||
"detection": "Port Scan",
|
||||
"source_ip": ip,
|
||||
"unique_ports_probed": len(ports),
|
||||
"sample_ports": sorted(list(ports))[:20],
|
||||
"severity": "high",
|
||||
})
|
||||
return alerts
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Network Packet Analysis Agent (Scapy)")
|
||||
parser.add_argument("--pcap", required=True, help="Path to pcap/pcapng file")
|
||||
parser.add_argument("--syn-threshold", type=int, default=100, help="SYN flood detection threshold")
|
||||
parser.add_argument("--dns-length", type=int, default=50, help="DNS tunneling subdomain length threshold")
|
||||
parser.add_argument("--scan-threshold", type=int, default=20, help="Port scan unique ports threshold")
|
||||
parser.add_argument("--output", default="packet_analysis_report.json", help="Output report path")
|
||||
args = parser.parse_args()
|
||||
|
||||
packets = load_pcap(args.pcap)
|
||||
records = extract_packet_info(packets)
|
||||
print(f"[+] Extracted {len(records)} IP-layer records")
|
||||
|
||||
stats = compute_traffic_stats(records)
|
||||
syn_alerts = detect_syn_flood(records, args.syn_threshold)
|
||||
dns_alerts = detect_dns_tunneling(records, args.dns_length)
|
||||
scan_alerts = detect_port_scan(records, args.scan_threshold)
|
||||
|
||||
report = {
|
||||
"analysis_time": datetime.utcnow().isoformat() + "Z",
|
||||
"pcap_file": args.pcap,
|
||||
"traffic_stats": stats,
|
||||
"anomalies": {
|
||||
"syn_flood": syn_alerts,
|
||||
"dns_tunneling": dns_alerts,
|
||||
"port_scan": scan_alerts,
|
||||
},
|
||||
"total_anomalies": len(syn_alerts) + len(dns_alerts) + len(scan_alerts),
|
||||
}
|
||||
|
||||
with open(args.output, "w") as f:
|
||||
json.dump(report, f, indent=2, default=str)
|
||||
print(f"[+] SYN flood alerts: {len(syn_alerts)}")
|
||||
print(f"[+] DNS tunneling indicators: {len(dns_alerts)}")
|
||||
print(f"[+] Port scan detections: {len(scan_alerts)}")
|
||||
print(f"[+] Report saved to {args.output}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
name: detecting-credential-dumping-techniques
|
||||
description: Detect LSASS credential dumping, SAM database extraction, and NTDS.dit theft using Sysmon Event ID 10, Windows Security logs, and SIEM correlation rules
|
||||
domain: cybersecurity
|
||||
subdomain: threat-detection
|
||||
tags:
|
||||
- credential-dumping
|
||||
- lsass
|
||||
- mimikatz
|
||||
- sysmon
|
||||
- active-directory
|
||||
- windows-security
|
||||
- defense-evasion
|
||||
version: "1.0"
|
||||
author: mahipal
|
||||
license: Apache-2.0
|
||||
---
|
||||
|
||||
# Detecting Credential Dumping Techniques
|
||||
|
||||
## Overview
|
||||
|
||||
Credential dumping (MITRE ATT&CK T1003) is a post-exploitation technique where adversaries extract authentication credentials from OS memory, registry hives, or domain controller databases. This skill covers detection of LSASS memory access via Sysmon Event ID 10 (ProcessAccess), SAM registry hive export via reg.exe, NTDS.dit extraction via ntdsutil/vssadmin, and comsvcs.dll MiniDump abuse. Detection rules analyze GrantedAccess bitmasks, suspicious calling processes, and known tool signatures.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Sysmon v14+ deployed with ProcessAccess logging (Event ID 10) for lsass.exe
|
||||
- Windows Security audit policy enabling process creation (Event ID 4688) with command line logging
|
||||
- Splunk or Elastic SIEM ingesting Sysmon and Windows Security logs
|
||||
- Python 3.8+ for log analysis
|
||||
|
||||
## Steps
|
||||
|
||||
1. Configure Sysmon to log ProcessAccess events targeting lsass.exe
|
||||
2. Forward Sysmon Event ID 10 and Windows Event ID 4688 to SIEM
|
||||
3. Create detection rules for known GrantedAccess patterns (0x1010, 0x1FFFFF)
|
||||
4. Detect comsvcs.dll MiniDump and procdump.exe targeting LSASS PID
|
||||
5. Alert on reg.exe SAM/SECURITY/SYSTEM hive export commands
|
||||
6. Detect ntdsutil/vssadmin shadow copy creation for NTDS.dit theft
|
||||
7. Correlate detections with user/host context for risk scoring
|
||||
|
||||
## Expected Output
|
||||
|
||||
JSON report containing detected credential dumping indicators with technique classification, severity ratings, process details, MITRE ATT&CK mapping, and Splunk/Elastic detection queries.
|
||||
@@ -0,0 +1,98 @@
|
||||
# Credential Dumping Detection API Reference
|
||||
|
||||
## Sysmon Event ID 10 - ProcessAccess
|
||||
|
||||
### Key Fields
|
||||
```
|
||||
SourceImage - Process accessing LSASS
|
||||
SourceProcessId - PID of accessing process
|
||||
TargetImage - Should be C:\Windows\System32\lsass.exe
|
||||
GrantedAccess - Access rights bitmask
|
||||
CallTrace - DLL call stack of the access
|
||||
```
|
||||
|
||||
### Suspicious GrantedAccess Values
|
||||
| Value | Meaning | Tool Association |
|
||||
|-------|---------|-----------------|
|
||||
| 0x1010 | VM_READ + QUERY_LIMITED | Mimikatz |
|
||||
| 0x1410 | VM_READ + QUERY_INFO | ProcDump |
|
||||
| 0x1FFFFF | PROCESS_ALL_ACCESS | Various dumpers |
|
||||
| 0x1438 | VM_READ + QUERY + DUP_HANDLE | Cobalt Strike |
|
||||
| 0x40 | DUP_HANDLE only | Handle duplication |
|
||||
|
||||
## Sysmon Event ID 1 - Process Creation
|
||||
|
||||
### Command Line Patterns for Credential Theft
|
||||
```
|
||||
# SAM hive export
|
||||
reg save hklm\sam C:\temp\sam.hiv
|
||||
reg save hklm\security C:\temp\security.hiv
|
||||
reg save hklm\system C:\temp\system.hiv
|
||||
|
||||
# comsvcs.dll LSASS dump
|
||||
rundll32.exe C:\Windows\System32\comsvcs.dll, MiniDump <lsass_pid> dump.bin full
|
||||
|
||||
# NTDS.dit extraction
|
||||
ntdsutil "activate instance ntds" ifm "create full C:\temp"
|
||||
vssadmin create shadow /for=C:
|
||||
```
|
||||
|
||||
## Splunk SPL Queries
|
||||
|
||||
### LSASS Access Detection
|
||||
```spl
|
||||
index=sysmon EventCode=10 TargetImage="*\\lsass.exe"
|
||||
GrantedAccess IN ("0x1010","0x1FFFFF","0x1410","0x1438")
|
||||
SourceImage!="*\\csrss.exe" SourceImage!="*\\svchost.exe"
|
||||
| stats count by SourceImage, GrantedAccess, Computer, User
|
||||
| sort -count
|
||||
```
|
||||
|
||||
### comsvcs.dll MiniDump Detection
|
||||
```spl
|
||||
index=sysmon EventCode=1
|
||||
(CommandLine="*comsvcs*MiniDump*" OR CommandLine="*comsvcs*#24*")
|
||||
| table _time, Computer, User, ParentImage, CommandLine
|
||||
```
|
||||
|
||||
### SAM/SECURITY Hive Export
|
||||
```spl
|
||||
index=sysmon EventCode=1 Image="*\\reg.exe"
|
||||
(CommandLine="*save*hklm\\sam*" OR CommandLine="*save*hklm\\security*")
|
||||
| table _time, Computer, User, CommandLine
|
||||
```
|
||||
|
||||
## Elastic / KQL Queries
|
||||
|
||||
### LSASS Access in Elastic
|
||||
```kql
|
||||
event.code: "10" AND
|
||||
winlog.event_data.TargetImage: *lsass.exe AND
|
||||
winlog.event_data.GrantedAccess: ("0x1010" OR "0x1FFFFF")
|
||||
```
|
||||
|
||||
### Process Creation with Credential Theft Commands
|
||||
```kql
|
||||
event.code: "1" AND
|
||||
(process.command_line: *comsvcs*MiniDump* OR
|
||||
process.command_line: *reg*save*hklm\\sam*)
|
||||
```
|
||||
|
||||
## MITRE ATT&CK Mapping
|
||||
|
||||
| Sub-technique | ID | Detection Method |
|
||||
|---|---|---|
|
||||
| LSASS Memory | T1003.001 | Sysmon EID 10 GrantedAccess |
|
||||
| Security Account Manager | T1003.002 | reg.exe save commands |
|
||||
| NTDS | T1003.003 | ntdsutil / vssadmin commands |
|
||||
| DCSync | T1003.006 | Event ID 4662 with replication GUIDs |
|
||||
|
||||
## CLI Usage
|
||||
|
||||
```bash
|
||||
# Analyze Sysmon XML export
|
||||
python agent.py --sysmon-xml sysmon_events.xml --output cred_report.json
|
||||
|
||||
# Print Splunk detection queries
|
||||
python agent.py --show-splunk
|
||||
```
|
||||
@@ -0,0 +1,228 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Detect credential dumping techniques via Sysmon/Windows event log analysis."""
|
||||
|
||||
import json
|
||||
import re
|
||||
import argparse
|
||||
import xml.etree.ElementTree as ET
|
||||
from collections import defaultdict
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
LSASS_GRANTED_ACCESS_SUSPICIOUS = {
|
||||
"0x1010": "PROCESS_VM_READ | PROCESS_QUERY_LIMITED_INFORMATION (Mimikatz-style)",
|
||||
"0x1410": "PROCESS_VM_READ | PROCESS_QUERY_INFORMATION (procdump-style)",
|
||||
"0x1FFFFF": "PROCESS_ALL_ACCESS (full access to LSASS)",
|
||||
"0x1438": "PROCESS_VM_READ | PROCESS_QUERY_INFORMATION | PROCESS_DUP_HANDLE",
|
||||
"0x40": "PROCESS_DUP_HANDLE (handle duplication for indirect access)",
|
||||
}
|
||||
|
||||
SUSPICIOUS_CALLERS = [
|
||||
"mimikatz", "procdump", "rundll32.exe", "taskmgr.exe",
|
||||
"powershell.exe", "cmd.exe", "wmic.exe", "cscript.exe", "wscript.exe",
|
||||
]
|
||||
|
||||
SAM_EXPORT_PATTERNS = [
|
||||
r"reg\s+save\s+hklm\\sam",
|
||||
r"reg\s+save\s+hklm\\security",
|
||||
r"reg\s+save\s+hklm\\system",
|
||||
r"esentutl.*ntds\.dit",
|
||||
r"ntdsutil.*\"activate instance ntds\"",
|
||||
r"vssadmin\s+create\s+shadow",
|
||||
r"copy\s+\\\\.*\\c\$.*ntds\.dit",
|
||||
r"secretsdump",
|
||||
]
|
||||
|
||||
COMSVCS_PATTERNS = [
|
||||
r"comsvcs\.dll.*MiniDump",
|
||||
r"comsvcs\.dll.*#24",
|
||||
r"rundll32.*comsvcs",
|
||||
]
|
||||
|
||||
|
||||
def parse_sysmon_xml(xml_path):
|
||||
"""Parse Sysmon event log XML export for Event ID 10 (ProcessAccess)."""
|
||||
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)
|
||||
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_lsass_access(events):
|
||||
"""Detect suspicious ProcessAccess (Event ID 10) targeting lsass.exe."""
|
||||
alerts = []
|
||||
for ev in events:
|
||||
if ev["event_id"] != 10:
|
||||
continue
|
||||
target = ev.get("TargetImage", "").lower()
|
||||
if "lsass.exe" not in target:
|
||||
continue
|
||||
granted = ev.get("GrantedAccess", "")
|
||||
source_image = ev.get("SourceImage", "").lower()
|
||||
source_name = source_image.split("\\")[-1] if source_image else ""
|
||||
severity = "medium"
|
||||
reasons = []
|
||||
if granted in LSASS_GRANTED_ACCESS_SUSPICIOUS:
|
||||
reasons.append(f"Suspicious GrantedAccess {granted}: {LSASS_GRANTED_ACCESS_SUSPICIOUS[granted]}")
|
||||
severity = "critical" if granted == "0x1FFFFF" else "high"
|
||||
if any(s in source_name for s in SUSPICIOUS_CALLERS):
|
||||
reasons.append(f"Suspicious calling process: {source_name}")
|
||||
severity = "critical"
|
||||
if not reasons:
|
||||
continue
|
||||
alerts.append({
|
||||
"detection": "LSASS Memory Access",
|
||||
"mitre_technique": "T1003.001",
|
||||
"timestamp": ev["timestamp"],
|
||||
"source_process": ev.get("SourceImage", ""),
|
||||
"source_pid": ev.get("SourceProcessId", ""),
|
||||
"target_process": ev.get("TargetImage", ""),
|
||||
"granted_access": granted,
|
||||
"call_trace": ev.get("CallTrace", "")[:200],
|
||||
"severity": severity,
|
||||
"reasons": reasons,
|
||||
"user": ev.get("SourceUser", ev.get("User", "")),
|
||||
"host": ev.get("Computer", ""),
|
||||
})
|
||||
return alerts
|
||||
|
||||
|
||||
def detect_credential_commands(events):
|
||||
"""Detect SAM/NTDS.dit export and comsvcs.dll dump commands from Event ID 1 or 4688."""
|
||||
alerts = []
|
||||
for ev in events:
|
||||
if ev["event_id"] not in (1, 4688):
|
||||
continue
|
||||
cmdline = ev.get("CommandLine", ev.get("ProcessCommandLine", "")).lower()
|
||||
if not cmdline:
|
||||
continue
|
||||
for pattern in SAM_EXPORT_PATTERNS:
|
||||
if re.search(pattern, cmdline, re.IGNORECASE):
|
||||
technique = "T1003.002" if "sam" in cmdline or "security" in cmdline else "T1003.003"
|
||||
alerts.append({
|
||||
"detection": "Registry Hive / NTDS.dit Export",
|
||||
"mitre_technique": technique,
|
||||
"timestamp": ev["timestamp"],
|
||||
"command_line": ev.get("CommandLine", ev.get("ProcessCommandLine", "")),
|
||||
"process": ev.get("Image", ev.get("NewProcessName", "")),
|
||||
"user": ev.get("User", ev.get("SubjectUserName", "")),
|
||||
"severity": "critical",
|
||||
})
|
||||
break
|
||||
for pattern in COMSVCS_PATTERNS:
|
||||
if re.search(pattern, cmdline, re.IGNORECASE):
|
||||
alerts.append({
|
||||
"detection": "LSASS Dump via comsvcs.dll",
|
||||
"mitre_technique": "T1003.001",
|
||||
"timestamp": ev["timestamp"],
|
||||
"command_line": ev.get("CommandLine", ev.get("ProcessCommandLine", "")),
|
||||
"process": ev.get("Image", ev.get("NewProcessName", "")),
|
||||
"user": ev.get("User", ev.get("SubjectUserName", "")),
|
||||
"severity": "critical",
|
||||
})
|
||||
break
|
||||
return alerts
|
||||
|
||||
|
||||
def detect_ntdll_access(events):
|
||||
"""Detect suspicious ntdll.dll access patterns in CallTrace (Mimikatz signature)."""
|
||||
alerts = []
|
||||
for ev in events:
|
||||
if ev["event_id"] != 10:
|
||||
continue
|
||||
call_trace = ev.get("CallTrace", "")
|
||||
if "ntdll.dll" in call_trace.lower() and "UNKNOWN" in call_trace:
|
||||
target = ev.get("TargetImage", "").lower()
|
||||
if "lsass.exe" in target:
|
||||
alerts.append({
|
||||
"detection": "NTDLL Suspicious CallTrace (Mimikatz Signature)",
|
||||
"mitre_technique": "T1003.001",
|
||||
"timestamp": ev["timestamp"],
|
||||
"source_process": ev.get("SourceImage", ""),
|
||||
"call_trace_snippet": call_trace[:300],
|
||||
"severity": "critical",
|
||||
})
|
||||
return alerts
|
||||
|
||||
|
||||
def generate_splunk_queries():
|
||||
"""Return SPL detection queries for credential dumping."""
|
||||
return {
|
||||
"lsass_access": (
|
||||
'index=sysmon EventCode=10 TargetImage="*\\\\lsass.exe" '
|
||||
'GrantedAccess IN ("0x1010","0x1FFFFF","0x1410","0x1438") '
|
||||
'| stats count by SourceImage, GrantedAccess, Computer'
|
||||
),
|
||||
"comsvcs_dump": (
|
||||
'index=sysmon EventCode=1 CommandLine="*comsvcs*MiniDump*" '
|
||||
'OR CommandLine="*comsvcs*#24*" | table _time, Computer, User, CommandLine'
|
||||
),
|
||||
"sam_export": (
|
||||
'index=sysmon EventCode=1 (CommandLine="*reg*save*hklm\\\\sam*" '
|
||||
'OR CommandLine="*reg*save*hklm\\\\security*") '
|
||||
'| table _time, Computer, User, CommandLine'
|
||||
),
|
||||
"ntds_extraction": (
|
||||
'index=sysmon EventCode=1 (CommandLine="*ntdsutil*" '
|
||||
'OR CommandLine="*vssadmin*create*shadow*") '
|
||||
'| table _time, Computer, User, CommandLine'
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Credential Dumping Detection Agent")
|
||||
parser.add_argument("--sysmon-xml", help="Path to Sysmon event log XML export")
|
||||
parser.add_argument("--output", default="credential_dump_report.json", help="Output report path")
|
||||
parser.add_argument("--show-splunk", action="store_true", help="Print Splunk detection 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.sysmon_xml:
|
||||
print("[!] Provide --sysmon-xml path or use --show-splunk for detection queries")
|
||||
return
|
||||
|
||||
events = parse_sysmon_xml(args.sysmon_xml)
|
||||
print(f"[+] Parsed {len(events)} Sysmon/Security events")
|
||||
|
||||
lsass_alerts = detect_lsass_access(events)
|
||||
cmd_alerts = detect_credential_commands(events)
|
||||
ntdll_alerts = detect_ntdll_access(events)
|
||||
|
||||
report = {
|
||||
"analysis_time": datetime.utcnow().isoformat() + "Z",
|
||||
"total_events": len(events),
|
||||
"detections": {
|
||||
"lsass_memory_access": lsass_alerts,
|
||||
"credential_export_commands": cmd_alerts,
|
||||
"ntdll_suspicious_calltrace": ntdll_alerts,
|
||||
},
|
||||
"total_alerts": len(lsass_alerts) + len(cmd_alerts) + len(ntdll_alerts),
|
||||
"mitre_techniques": ["T1003.001", "T1003.002", "T1003.003"],
|
||||
"splunk_queries": generate_splunk_queries(),
|
||||
}
|
||||
|
||||
with open(args.output, "w") as f:
|
||||
json.dump(report, f, indent=2)
|
||||
print(f"[+] LSASS access alerts: {len(lsass_alerts)}")
|
||||
print(f"[+] Credential export commands: {len(cmd_alerts)}")
|
||||
print(f"[+] NTDLL suspicious traces: {len(ntdll_alerts)}")
|
||||
print(f"[+] Report saved to {args.output}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
name: detecting-pass-the-ticket-attacks
|
||||
description: Detect Kerberos Pass-the-Ticket (PtT) attacks by analyzing Windows Event IDs 4768, 4769, and 4771 for anomalous ticket usage patterns in Splunk and Elastic SIEM
|
||||
domain: cybersecurity
|
||||
subdomain: threat-detection
|
||||
tags:
|
||||
- kerberos
|
||||
- pass-the-ticket
|
||||
- active-directory
|
||||
- splunk
|
||||
- elastic
|
||||
- credential-theft
|
||||
- windows-security
|
||||
version: "1.0"
|
||||
author: mahipal
|
||||
license: Apache-2.0
|
||||
---
|
||||
|
||||
# Detecting Pass-the-Ticket Attacks
|
||||
|
||||
## Overview
|
||||
|
||||
Pass-the-Ticket (PtT) is a credential theft technique (MITRE ATT&CK T1550.003) where adversaries steal Kerberos tickets (TGT or TGS) from one system and replay them on another to authenticate without knowing the user's password. This skill teaches detection of PtT attacks by correlating Windows Security Event IDs 4768 (TGT request), 4769 (TGS request), and 4771 (pre-authentication failure) for anomalies such as ticket reuse across different hosts, RC4 encryption downgrades, and unusual service ticket request volumes.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Windows Domain Controller with advanced audit policy enabled (Audit Kerberos Authentication Service, Audit Kerberos Service Ticket Operations)
|
||||
- Splunk or Elastic SIEM ingesting Windows Security event logs
|
||||
- Sysmon deployed on endpoints for supplementary process telemetry
|
||||
- Python 3.8+ with `requests` library
|
||||
|
||||
## Steps
|
||||
|
||||
1. Enable Kerberos audit logging on Domain Controllers via Group Policy
|
||||
2. Forward Event IDs 4768, 4769, and 4771 to SIEM platform
|
||||
3. Deploy detection rules for RC4 encryption downgrade (TicketEncryptionType 0x17)
|
||||
4. Create correlation rule for ticket reuse across multiple source IPs
|
||||
5. Build baseline of normal TGS request volume per user/host
|
||||
6. Alert on standard deviation anomalies in ticket request patterns
|
||||
7. Investigate flagged events with enrichment from Active Directory
|
||||
|
||||
## Expected Output
|
||||
|
||||
JSON report containing detected PtT indicators including anomalous ticket requests, RC4 downgrades, cross-host ticket reuse events, and risk-scored users with MITRE ATT&CK technique mapping.
|
||||
@@ -0,0 +1,104 @@
|
||||
# Pass-the-Ticket Detection API Reference
|
||||
|
||||
## Windows Security Event IDs
|
||||
|
||||
### Event ID 4768 - TGT Requested
|
||||
```
|
||||
Key Fields:
|
||||
TargetUserName - Account requesting TGT
|
||||
TargetDomainName - Domain of account
|
||||
IpAddress - Source IP of request
|
||||
TicketEncryptionType - 0x12 (AES256), 0x17 (RC4-HMAC)
|
||||
PreAuthType - 15 (PA-ENC-TIMESTAMP)
|
||||
```
|
||||
|
||||
### Event ID 4769 - TGS Requested
|
||||
```
|
||||
Key Fields:
|
||||
TargetUserName - Account using the ticket
|
||||
ServiceName - SPN of requested service
|
||||
IpAddress - Source IP
|
||||
TicketEncryptionType - 0x17 indicates RC4 downgrade
|
||||
TicketOptions - Kerberos ticket flags
|
||||
```
|
||||
|
||||
### Event ID 4771 - Kerberos Pre-Authentication Failed
|
||||
```
|
||||
Key Fields:
|
||||
TargetUserName - Account that failed
|
||||
IpAddress - Source of failure
|
||||
Status - 0x18 (wrong password), 0x12 (expired)
|
||||
```
|
||||
|
||||
## Splunk SPL Queries
|
||||
|
||||
### RC4 Encryption Downgrade Detection
|
||||
```spl
|
||||
index=wineventlog sourcetype="WinEventLog:Security" EventCode=4769
|
||||
TicketEncryptionType=0x17
|
||||
| stats count by TargetUserName, IpAddress, ServiceName
|
||||
| where count > 3
|
||||
```
|
||||
|
||||
### Cross-Host Ticket Reuse
|
||||
```spl
|
||||
index=wineventlog EventCode=4769
|
||||
| stats dc(IpAddress) as ip_count, values(IpAddress) as ips
|
||||
by TargetUserName
|
||||
| where ip_count > 1
|
||||
| sort -ip_count
|
||||
```
|
||||
|
||||
### TGS Volume Anomaly
|
||||
```spl
|
||||
index=wineventlog EventCode=4769
|
||||
| bin _time span=1h
|
||||
| stats count by TargetUserName, _time
|
||||
| eventstats avg(count) as avg_count, stdev(count) as sd by TargetUserName
|
||||
| where count > avg_count + (3 * sd)
|
||||
```
|
||||
|
||||
## Elastic / KQL Queries
|
||||
|
||||
### RC4 Downgrade in Elastic
|
||||
```kql
|
||||
event.code: "4769" AND winlog.event_data.TicketEncryptionType: "0x17"
|
||||
```
|
||||
|
||||
### Cross-Host Reuse in Elastic
|
||||
```json
|
||||
POST security-*/_search
|
||||
{
|
||||
"size": 0,
|
||||
"query": { "term": { "event.code": "4769" } },
|
||||
"aggs": {
|
||||
"by_user": {
|
||||
"terms": { "field": "winlog.event_data.TargetUserName" },
|
||||
"aggs": {
|
||||
"unique_ips": { "cardinality": { "field": "source.ip" } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## MITRE ATT&CK Mapping
|
||||
|
||||
| Technique | ID | Detection |
|
||||
|---|---|---|
|
||||
| Use Alternate Authentication Material: Pass the Ticket | T1550.003 | RC4 downgrade, cross-host reuse |
|
||||
| Steal or Forge Kerberos Tickets: Kerberoasting | T1558.003 | High TGS volume for SPNs |
|
||||
| Brute Force: Password Spraying | T1110.003 | Pre-auth failure spikes |
|
||||
|
||||
## CLI Usage
|
||||
|
||||
```bash
|
||||
# Parse exported event log XML and detect PtT indicators
|
||||
python agent.py --evtx-xml security_events.xml --output report.json
|
||||
|
||||
# Show Splunk detection queries
|
||||
python agent.py --show-splunk
|
||||
|
||||
# Custom thresholds
|
||||
python agent.py --evtx-xml events.xml --tgs-threshold 30 --preauth-threshold 5
|
||||
```
|
||||
@@ -0,0 +1,203 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Detect Kerberos Pass-the-Ticket attacks via Windows Event ID 4768/4769/4771 analysis."""
|
||||
|
||||
import json
|
||||
import argparse
|
||||
import xml.etree.ElementTree as ET
|
||||
from collections import defaultdict
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def parse_evtx_xml(xml_path):
|
||||
"""Parse exported Windows Security event log XML for Kerberos events."""
|
||||
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, 4771):
|
||||
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,
|
||||
"target_user": fields.get("TargetUserName", ""),
|
||||
"target_domain": fields.get("TargetDomainName", ""),
|
||||
"ip_address": fields.get("IpAddress", ""),
|
||||
"service_name": fields.get("ServiceName", ""),
|
||||
"ticket_encryption_type": fields.get("TicketEncryptionType", ""),
|
||||
"status": fields.get("Status", ""),
|
||||
"pre_auth_type": fields.get("PreAuthType", ""),
|
||||
})
|
||||
return events
|
||||
|
||||
|
||||
def detect_rc4_downgrade(events):
|
||||
"""Detect RC4 encryption downgrade (TicketEncryptionType 0x17) in TGS requests."""
|
||||
alerts = []
|
||||
for ev in events:
|
||||
enc_type = ev["ticket_encryption_type"]
|
||||
if enc_type in ("0x17", "23"):
|
||||
alerts.append({
|
||||
"detection": "RC4 Encryption Downgrade",
|
||||
"mitre_technique": "T1550.003",
|
||||
"event_id": ev["event_id"],
|
||||
"timestamp": ev["timestamp"],
|
||||
"user": ev["target_user"],
|
||||
"domain": ev["target_domain"],
|
||||
"service": ev["service_name"],
|
||||
"ip_address": ev["ip_address"],
|
||||
"encryption_type": enc_type,
|
||||
"severity": "high",
|
||||
"description": "RC4 (0x17) ticket encryption detected; may indicate Pass-the-Ticket or Kerberoasting",
|
||||
})
|
||||
return alerts
|
||||
|
||||
|
||||
def detect_cross_host_ticket_reuse(events):
|
||||
"""Detect same user TGS requests from multiple source IPs within short window."""
|
||||
user_ips = defaultdict(set)
|
||||
user_events = defaultdict(list)
|
||||
for ev in events:
|
||||
if ev["event_id"] == 4769 and ev["target_user"] and ev["ip_address"]:
|
||||
key = f"{ev['target_user']}@{ev['target_domain']}"
|
||||
user_ips[key].add(ev["ip_address"])
|
||||
user_events[key].append(ev)
|
||||
alerts = []
|
||||
for user, ips in user_ips.items():
|
||||
if len(ips) >= 2:
|
||||
sample = user_events[user][:5]
|
||||
alerts.append({
|
||||
"detection": "Cross-Host Ticket Reuse",
|
||||
"mitre_technique": "T1550.003",
|
||||
"user": user,
|
||||
"source_ips": list(ips),
|
||||
"ip_count": len(ips),
|
||||
"request_count": len(user_events[user]),
|
||||
"severity": "critical",
|
||||
"sample_timestamps": [e["timestamp"] for e in sample],
|
||||
"description": "Same user ticket used from multiple IPs, indicating stolen ticket replay",
|
||||
})
|
||||
return alerts
|
||||
|
||||
|
||||
def detect_anomalous_tgs_volume(events, threshold=50):
|
||||
"""Detect users requesting abnormally high number of TGS tickets."""
|
||||
user_tgs = defaultdict(int)
|
||||
for ev in events:
|
||||
if ev["event_id"] == 4769 and ev["target_user"]:
|
||||
user_tgs[f"{ev['target_user']}@{ev['target_domain']}"] += 1
|
||||
alerts = []
|
||||
for user, count in user_tgs.items():
|
||||
if count >= threshold:
|
||||
alerts.append({
|
||||
"detection": "Anomalous TGS Volume",
|
||||
"mitre_technique": "T1550.003",
|
||||
"user": user,
|
||||
"tgs_request_count": count,
|
||||
"threshold": threshold,
|
||||
"severity": "high",
|
||||
"description": f"User requested {count} service tickets (threshold: {threshold})",
|
||||
})
|
||||
return alerts
|
||||
|
||||
|
||||
def detect_preauth_failures(events, threshold=10):
|
||||
"""Detect excessive Kerberos pre-authentication failures (Event ID 4771)."""
|
||||
user_failures = defaultdict(int)
|
||||
for ev in events:
|
||||
if ev["event_id"] == 4771:
|
||||
user_failures[f"{ev['target_user']}@{ev['target_domain']}"] += 1
|
||||
alerts = []
|
||||
for user, count in user_failures.items():
|
||||
if count >= threshold:
|
||||
alerts.append({
|
||||
"detection": "Excessive Pre-Auth Failures",
|
||||
"mitre_technique": "T1110.003",
|
||||
"user": user,
|
||||
"failure_count": count,
|
||||
"severity": "medium",
|
||||
"description": f"{count} Kerberos pre-authentication failures detected",
|
||||
})
|
||||
return alerts
|
||||
|
||||
|
||||
def generate_splunk_queries():
|
||||
"""Return SPL queries for Splunk-based PtT detection."""
|
||||
return {
|
||||
"rc4_downgrade": (
|
||||
'index=wineventlog sourcetype="WinEventLog:Security" EventCode=4769 '
|
||||
'TicketEncryptionType=0x17 | stats count by TargetUserName, IpAddress, ServiceName'
|
||||
),
|
||||
"cross_host_reuse": (
|
||||
'index=wineventlog EventCode=4769 | stats dc(IpAddress) as ip_count, '
|
||||
'values(IpAddress) as source_ips by TargetUserName | where ip_count > 1'
|
||||
),
|
||||
"tgs_volume_anomaly": (
|
||||
'index=wineventlog EventCode=4769 | stats count by TargetUserName '
|
||||
'| where count > 50 | sort -count'
|
||||
),
|
||||
"preauth_failures": (
|
||||
'index=wineventlog EventCode=4771 | stats count by TargetUserName, IpAddress '
|
||||
'| where count > 10'
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Pass-the-Ticket Attack Detector")
|
||||
parser.add_argument("--evtx-xml", help="Path to exported Security event log XML")
|
||||
parser.add_argument("--tgs-threshold", type=int, default=50, help="TGS volume alert threshold")
|
||||
parser.add_argument("--preauth-threshold", type=int, default=10, help="Pre-auth failure threshold")
|
||||
parser.add_argument("--output", default="ptt_detection_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:
|
||||
queries = generate_splunk_queries()
|
||||
for name, spl in queries.items():
|
||||
print(f"\n--- {name} ---\n{spl}")
|
||||
return
|
||||
|
||||
if not args.evtx_xml:
|
||||
print("[!] Provide --evtx-xml path to exported Windows Security event log XML")
|
||||
print("[*] Or use --show-splunk to get Splunk detection queries")
|
||||
return
|
||||
|
||||
events = parse_evtx_xml(args.evtx_xml)
|
||||
print(f"[+] Parsed {len(events)} Kerberos events (4768/4769/4771)")
|
||||
|
||||
rc4_alerts = detect_rc4_downgrade(events)
|
||||
reuse_alerts = detect_cross_host_ticket_reuse(events)
|
||||
volume_alerts = detect_anomalous_tgs_volume(events, args.tgs_threshold)
|
||||
preauth_alerts = detect_preauth_failures(events, args.preauth_threshold)
|
||||
|
||||
report = {
|
||||
"analysis_time": datetime.utcnow().isoformat() + "Z",
|
||||
"total_kerberos_events": len(events),
|
||||
"detections": {
|
||||
"rc4_downgrade": rc4_alerts,
|
||||
"cross_host_ticket_reuse": reuse_alerts,
|
||||
"anomalous_tgs_volume": volume_alerts,
|
||||
"preauth_failures": preauth_alerts,
|
||||
},
|
||||
"total_alerts": len(rc4_alerts) + len(reuse_alerts) + len(volume_alerts) + len(preauth_alerts),
|
||||
"mitre_techniques": ["T1550.003", "T1558.003", "T1110.003"],
|
||||
"splunk_queries": generate_splunk_queries(),
|
||||
}
|
||||
|
||||
with open(args.output, "w") as f:
|
||||
json.dump(report, f, indent=2)
|
||||
print(f"[+] Alerts: RC4={len(rc4_alerts)}, Reuse={len(reuse_alerts)}, "
|
||||
f"Volume={len(volume_alerts)}, PreAuth={len(preauth_alerts)}")
|
||||
print(f"[+] Report saved to {args.output}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Mahipal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
name: implementing-siem-use-case-tuning
|
||||
description: Tune SIEM detection rules to reduce false positives by analyzing alert volumes, creating whitelists, adjusting thresholds, and measuring detection efficacy metrics in Splunk and Elastic
|
||||
domain: cybersecurity
|
||||
subdomain: security-operations
|
||||
tags:
|
||||
- siem
|
||||
- detection-engineering
|
||||
- false-positive-reduction
|
||||
- splunk
|
||||
- elastic
|
||||
- alert-tuning
|
||||
- soc
|
||||
version: "1.0"
|
||||
author: mahipal
|
||||
license: Apache-2.0
|
||||
---
|
||||
|
||||
# Implementing SIEM Use Case Tuning
|
||||
|
||||
## Overview
|
||||
|
||||
SIEM use case tuning reduces alert fatigue by systematically analyzing detection rules for false positive rates, adjusting thresholds based on environmental baselines, creating context-aware whitelists, and measuring detection efficacy through precision/recall metrics. This skill covers tuning workflows for Splunk correlation searches and Elastic detection rules, including statistical baselining, exclusion list management, and alert-to-incident conversion tracking.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Splunk Enterprise/Cloud with ES or Elastic SIEM with detection rules enabled
|
||||
- Historical alert data (minimum 30 days) for baseline analysis
|
||||
- Python 3.8+ with `requests` library
|
||||
- SIEM admin credentials or API tokens
|
||||
|
||||
## Steps
|
||||
|
||||
1. Export current alert volumes per detection rule from SIEM
|
||||
2. Calculate false positive rate per rule using analyst disposition data
|
||||
3. Identify top noise-generating rules by volume and FP rate
|
||||
4. Build environmental baselines for thresholds (e.g., login counts, process spawns)
|
||||
5. Create whitelist entries for known-good entities (service accounts, scanners)
|
||||
6. Adjust rule thresholds using statistical analysis (mean + N standard deviations)
|
||||
7. Measure tuning impact via before/after precision and alert-to-incident ratio
|
||||
|
||||
## Expected Output
|
||||
|
||||
JSON report with per-rule tuning recommendations including current FP rate, suggested threshold adjustments, whitelist entries, and projected alert reduction percentages.
|
||||
@@ -0,0 +1,82 @@
|
||||
# SIEM Use Case Tuning API Reference
|
||||
|
||||
## Splunk Notable Event Export
|
||||
|
||||
### Export Notables via SPL
|
||||
```spl
|
||||
| inputlookup notable_events
|
||||
| search status_label IN ("New", "In Progress", "Resolved")
|
||||
| table rule_name, _time, status_label, src, dest, user, urgency
|
||||
| rename status_label as disposition, _time as timestamp
|
||||
| outputlookup alert_export.csv
|
||||
```
|
||||
|
||||
### Splunk ES Correlation Search Tuning
|
||||
```spl
|
||||
# Measure FP rate per correlation search over 30 days
|
||||
| inputlookup notable_events where earliest=-30d
|
||||
| eval is_fp=if(status_label="Resolved" AND disposition="False Positive", 1, 0)
|
||||
| stats count as total, sum(is_fp) as fp_count by rule_name
|
||||
| eval fp_rate=round(fp_count/total, 4)
|
||||
| sort -fp_rate
|
||||
```
|
||||
|
||||
### Update Correlation Search Threshold
|
||||
```
|
||||
POST /servicesNS/nobody/SplunkEnterpriseSecuritySuite/saved/searches/{search_name}
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
|
||||
search=<updated_spl_with_new_threshold>
|
||||
```
|
||||
|
||||
## Elastic Detection Rule Tuning
|
||||
|
||||
### List Detection Rules
|
||||
```
|
||||
GET /_security/detection_engine/rules/_find?per_page=100
|
||||
Authorization: ApiKey <base64_api_key>
|
||||
```
|
||||
|
||||
### Add Exception to Rule
|
||||
```json
|
||||
POST /_security/detection_engine/rules/exceptions
|
||||
{
|
||||
"rule_id": "rule-uuid",
|
||||
"name": "Whitelist scanner IPs",
|
||||
"entries": [
|
||||
{
|
||||
"field": "source.ip",
|
||||
"operator": "is_one_of",
|
||||
"value": ["10.0.1.50", "10.0.1.51"],
|
||||
"type": "match_any"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Query Rule Execution Stats (Kibana)
|
||||
```kql
|
||||
event.kind: "signal" AND kibana.alert.rule.name: "Brute Force Detection"
|
||||
| stats count by kibana.alert.workflow_status
|
||||
```
|
||||
|
||||
## Alert Tuning Metrics
|
||||
|
||||
| Metric | Formula | Target |
|
||||
|---|---|---|
|
||||
| False Positive Rate | FP / (FP + TP) | < 30% |
|
||||
| Precision | TP / (TP + FP) | > 70% |
|
||||
| Alert-to-Incident Ratio | Incidents / Total Alerts | > 20% |
|
||||
| Mean Time to Triage | avg(triage_end - alert_time) | < 15 min |
|
||||
|
||||
## CLI Usage
|
||||
|
||||
```bash
|
||||
# Analyze alert CSV export
|
||||
python agent.py --alert-csv notable_export.csv --output tuning.json
|
||||
|
||||
# Adjust FP threshold for whitelist candidates
|
||||
python agent.py --alert-csv alerts.csv --fp-threshold 0.9 --top-rules 10
|
||||
|
||||
# CSV format: rule_name,timestamp,disposition,source,user,severity
|
||||
```
|
||||
@@ -0,0 +1,187 @@
|
||||
#!/usr/bin/env python3
|
||||
"""SIEM use case tuning agent - analyzes alert data to reduce false positives and optimize detection rules."""
|
||||
|
||||
import json
|
||||
import csv
|
||||
import math
|
||||
import argparse
|
||||
from collections import defaultdict
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def load_alert_data(filepath):
|
||||
"""Load alert/notable event export (CSV with columns: rule_name, timestamp, disposition, source, user)."""
|
||||
alerts = []
|
||||
with open(filepath, newline="", encoding="utf-8") as f:
|
||||
reader = csv.DictReader(f)
|
||||
for row in reader:
|
||||
alerts.append({
|
||||
"rule_name": row.get("rule_name", row.get("search_name", "")),
|
||||
"timestamp": row.get("timestamp", row.get("_time", "")),
|
||||
"disposition": row.get("disposition", row.get("status", "unknown")),
|
||||
"source": row.get("source", row.get("src", "")),
|
||||
"user": row.get("user", row.get("dest_user", "")),
|
||||
"severity": row.get("severity", "medium"),
|
||||
})
|
||||
return alerts
|
||||
|
||||
|
||||
def calculate_rule_metrics(alerts):
|
||||
"""Calculate per-rule alert volume, FP rate, and disposition breakdown."""
|
||||
rule_stats = defaultdict(lambda: {"total": 0, "true_positive": 0, "false_positive": 0,
|
||||
"pending": 0, "sources": set(), "users": set()})
|
||||
for alert in alerts:
|
||||
rule = alert["rule_name"]
|
||||
rule_stats[rule]["total"] += 1
|
||||
disp = alert["disposition"].lower()
|
||||
if disp in ("true_positive", "tp", "confirmed", "escalated"):
|
||||
rule_stats[rule]["true_positive"] += 1
|
||||
elif disp in ("false_positive", "fp", "benign", "closed_fp"):
|
||||
rule_stats[rule]["false_positive"] += 1
|
||||
else:
|
||||
rule_stats[rule]["pending"] += 1
|
||||
if alert["source"]:
|
||||
rule_stats[rule]["sources"].add(alert["source"])
|
||||
if alert["user"]:
|
||||
rule_stats[rule]["users"].add(alert["user"])
|
||||
|
||||
metrics = []
|
||||
for rule, stats in rule_stats.items():
|
||||
reviewed = stats["true_positive"] + stats["false_positive"]
|
||||
fp_rate = stats["false_positive"] / reviewed if reviewed > 0 else 0.0
|
||||
precision = stats["true_positive"] / reviewed if reviewed > 0 else 0.0
|
||||
metrics.append({
|
||||
"rule_name": rule,
|
||||
"total_alerts": stats["total"],
|
||||
"true_positives": stats["true_positive"],
|
||||
"false_positives": stats["false_positive"],
|
||||
"pending": stats["pending"],
|
||||
"fp_rate": round(fp_rate, 4),
|
||||
"precision": round(precision, 4),
|
||||
"unique_sources": len(stats["sources"]),
|
||||
"unique_users": len(stats["users"]),
|
||||
"top_sources": list(stats["sources"])[:10],
|
||||
})
|
||||
return sorted(metrics, key=lambda x: x["fp_rate"], reverse=True)
|
||||
|
||||
|
||||
def identify_whitelist_candidates(alerts, fp_threshold=0.8):
|
||||
"""Identify source/user pairs that consistently trigger FPs for a given rule."""
|
||||
rule_source_stats = defaultdict(lambda: defaultdict(lambda: {"tp": 0, "fp": 0}))
|
||||
for alert in alerts:
|
||||
disp = alert["disposition"].lower()
|
||||
key = alert["source"] or alert["user"]
|
||||
if not key:
|
||||
continue
|
||||
if disp in ("false_positive", "fp", "benign", "closed_fp"):
|
||||
rule_source_stats[alert["rule_name"]][key]["fp"] += 1
|
||||
elif disp in ("true_positive", "tp", "confirmed", "escalated"):
|
||||
rule_source_stats[alert["rule_name"]][key]["tp"] += 1
|
||||
|
||||
candidates = []
|
||||
for rule, sources in rule_source_stats.items():
|
||||
for source, counts in sources.items():
|
||||
total = counts["tp"] + counts["fp"]
|
||||
if total >= 3 and counts["fp"] / total >= fp_threshold:
|
||||
candidates.append({
|
||||
"rule_name": rule,
|
||||
"entity": source,
|
||||
"fp_count": counts["fp"],
|
||||
"tp_count": counts["tp"],
|
||||
"fp_ratio": round(counts["fp"] / total, 4),
|
||||
"recommendation": "Add to whitelist" if counts["tp"] == 0 else "Review before whitelisting",
|
||||
})
|
||||
return sorted(candidates, key=lambda x: x["fp_count"], reverse=True)
|
||||
|
||||
|
||||
def compute_threshold_recommendation(alerts, rule_name, field="total"):
|
||||
"""Compute statistical threshold for a rule based on hourly alert distribution."""
|
||||
hourly_counts = defaultdict(int)
|
||||
for alert in alerts:
|
||||
if alert["rule_name"] != rule_name:
|
||||
continue
|
||||
try:
|
||||
dt = datetime.fromisoformat(alert["timestamp"].replace("Z", "+00:00"))
|
||||
hourly_counts[dt.strftime("%Y-%m-%d %H")] += 1
|
||||
except (ValueError, AttributeError):
|
||||
continue
|
||||
if not hourly_counts:
|
||||
return None
|
||||
values = list(hourly_counts.values())
|
||||
mean = sum(values) / len(values)
|
||||
variance = sum((x - mean) ** 2 for x in values) / len(values)
|
||||
stdev = math.sqrt(variance)
|
||||
return {
|
||||
"rule_name": rule_name,
|
||||
"hourly_mean": round(mean, 2),
|
||||
"hourly_stdev": round(stdev, 2),
|
||||
"suggested_threshold_2sd": round(mean + 2 * stdev, 0),
|
||||
"suggested_threshold_3sd": round(mean + 3 * stdev, 0),
|
||||
"sample_hours": len(hourly_counts),
|
||||
}
|
||||
|
||||
|
||||
def generate_tuning_report(metrics, whitelist, thresholds):
|
||||
"""Generate comprehensive tuning report with recommendations."""
|
||||
high_fp_rules = [m for m in metrics if m["fp_rate"] > 0.7]
|
||||
medium_fp_rules = [m for m in metrics if 0.3 < m["fp_rate"] <= 0.7]
|
||||
total_alerts = sum(m["total_alerts"] for m in metrics)
|
||||
total_fp = sum(m["false_positives"] for m in metrics)
|
||||
projected_reduction = sum(w["fp_count"] for w in whitelist)
|
||||
|
||||
return {
|
||||
"analysis_time": datetime.utcnow().isoformat() + "Z",
|
||||
"summary": {
|
||||
"total_rules_analyzed": len(metrics),
|
||||
"total_alerts": total_alerts,
|
||||
"total_false_positives": total_fp,
|
||||
"overall_fp_rate": round(total_fp / total_alerts, 4) if total_alerts else 0,
|
||||
"high_fp_rules": len(high_fp_rules),
|
||||
"whitelist_candidates": len(whitelist),
|
||||
"projected_alert_reduction": projected_reduction,
|
||||
},
|
||||
"high_fp_rules": high_fp_rules,
|
||||
"medium_fp_rules": medium_fp_rules,
|
||||
"whitelist_recommendations": whitelist[:20],
|
||||
"threshold_recommendations": thresholds,
|
||||
"actions": [
|
||||
{"priority": "high", "action": f"Disable or rewrite {len(high_fp_rules)} rules with FP rate > 70%"},
|
||||
{"priority": "medium", "action": f"Add {len(whitelist)} whitelist entries to reduce {projected_reduction} FP alerts"},
|
||||
{"priority": "low", "action": f"Review {len(medium_fp_rules)} rules with FP rate 30-70%"},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="SIEM Use Case Tuning Agent")
|
||||
parser.add_argument("--alert-csv", required=True, help="CSV export of SIEM alerts with disposition data")
|
||||
parser.add_argument("--fp-threshold", type=float, default=0.8, help="FP ratio threshold for whitelist candidates")
|
||||
parser.add_argument("--top-rules", type=int, default=5, help="Number of top rules to compute thresholds for")
|
||||
parser.add_argument("--output", default="tuning_report.json", help="Output report path")
|
||||
args = parser.parse_args()
|
||||
|
||||
alerts = load_alert_data(args.alert_csv)
|
||||
print(f"[+] Loaded {len(alerts)} alerts from {args.alert_csv}")
|
||||
|
||||
metrics = calculate_rule_metrics(alerts)
|
||||
print(f"[+] Analyzed {len(metrics)} unique detection rules")
|
||||
|
||||
whitelist = identify_whitelist_candidates(alerts, args.fp_threshold)
|
||||
print(f"[+] Found {len(whitelist)} whitelist candidates (FP ratio >= {args.fp_threshold})")
|
||||
|
||||
thresholds = []
|
||||
for m in metrics[:args.top_rules]:
|
||||
t = compute_threshold_recommendation(alerts, m["rule_name"])
|
||||
if t:
|
||||
thresholds.append(t)
|
||||
|
||||
report = generate_tuning_report(metrics, whitelist, thresholds)
|
||||
with open(args.output, "w") as f:
|
||||
json.dump(report, f, indent=2)
|
||||
print(f"[+] Tuning report saved to {args.output}")
|
||||
print(f"[+] Overall FP rate: {report['summary']['overall_fp_rate']:.1%}")
|
||||
print(f"[+] Projected alert reduction from whitelisting: {report['summary']['projected_alert_reduction']}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user