red teaming

Mapping Attack Paths with BloodHound CE

Collect Active Directory data with SharpHound and Entra ID data with AzureHound, ingest into BloodHound Community Edition, and analyze on-prem, cloud, and hybrid attack paths with built-in queries and custom Cypher.

active-directoryattack-pathsazurehoundbloodhound-cecypherentra-idhybrid-identitysharphound
Install this skill
npx skills add mukul975/Anthropic-Cybersecurity-Skills
Framework mappings

Legal Notice: This skill is for authorized security testing, red-team engagements, and educational purposes only. Active Directory and Entra ID reconnaissance reveals privilege-escalation chains that lead to full domain/tenant compromise. Use only against environments you own or have explicit written authorization (rules of engagement) to test. Unauthorized use violates the Computer Fraud and Abuse Act and equivalent laws.

Overview

BloodHound Community Edition (CE) is SpecterOps's graph-based attack-path-management platform. It models security principals (users, computers, groups, OUs, GPOs, Entra ID users/groups/apps/roles) as nodes and the permissions, group memberships, sessions, trusts, and ACLs between them as edges. By framing Active Directory and Entra ID as a directed graph, BloodHound turns the question "can this low-privileged account reach Domain Admins / Global Administrator?" into a shortest-path query that finds escalation chains a human reviewer would miss.

BloodHound CE replaced the legacy Neo4j-only BloodHound with a containerized stack: a Go API server, a PostgreSQL relational store, and a Neo4j graph database, all behind a modern web UI and REST API. Data is gathered by two collectors maintained by SpecterOps:

  • SharpHound (the CE/.NET collector, run on or against a domain-joined Windows host) gathers on-prem AD: objects, group membership, ACLs, sessions, local-admin rights, trusts, GPOs, certificate services (ADCS), and LAPS readability. It emits a ZIP of JSON.
  • AzureHound (a cross-platform Go binary) gathers Entra ID and Azure Resource Manager data via the Microsoft Graph and Azure REST APIs: users, groups, app registrations, service principals, directory roles, subscription/role assignments, and key vaults. It emits a single JSON file.

As of recent CE releases, when both an AD domain and its synced Entra ID tenant are ingested, BloodHound automatically renders Hybrid Attack Paths — chains that cross the on-prem/cloud boundary (for example, an on-prem user synced to a cloud account that holds a privileged Entra role). Mapped to MITRE ATT&CK, the core activity is T1069 – Permission Groups Discovery, supported by T1087 (Account Discovery), T1482 (Domain Trust Discovery), and T1018 (Remote System Discovery).

When to Use

  • During an authorized internal penetration test or red-team engagement after obtaining any domain foothold or Entra credentials
  • When you need to prioritize escalation routes from owned principals to Tier-0 assets (Domain Admins, Global Administrator)
  • When assessing hybrid identity risk where on-prem AD is synced to Entra ID
  • During purple-team exercises to validate that detections fire on collection and on path-execution steps
  • When producing attack-path evidence and remediation guidance for a defensive client

Prerequisites

  • Authorized scope covering AD/Entra reconnaissance
  • A foothold: any valid domain user (for SharpHound) and/or valid Entra credentials or a token (for AzureHound)
  • Docker + Docker Compose on the analysis workstation
  • The collectors:
    # Deploy BloodHound CE (pulls Postgres, Neo4j, and the BloodHound API)
    curl -L https://ghst.ly/getbhce -o docker-compose.yml
    docker compose pull
    docker compose up -d
    # Reveal the randomly generated initial admin password
    docker compose logs bloodhound | grep -i "Initial Password"
     
    # AzureHound (download the release binary for your OS from the GitHub releases page)
    #   https://github.com/SpecterOps/AzureHound/releases
    chmod +x ./azurehound
     
    # SharpHound CE collector: download SharpHound.zip from the BloodHound CE
    # web UI (Administration -> Download Collectors), transfer to a domain-joined host.

Objectives

  • Deploy BloodHound CE and authenticate to the web UI and API
  • Collect AD data with SharpHound and Entra data with AzureHound
  • Ingest both datasets and mark compromised principals as Owned
  • Run built-in analysis (Shortest Path to Domain Admins, Kerberoastable accounts, ADCS abuse)
  • Author custom Cypher queries for bespoke and hybrid attack paths
  • Prioritize and document escalation routes for the engagement report

MITRE ATT&CK Mapping

ID Technique Application in this skill
T1069 Permission Groups Discovery Enumerating AD/Entra group memberships and the permissions between principals to build the attack graph
T1087 Account Discovery SharpHound/AzureHound enumeration of users, computers, and service principals
T1482 Domain Trust Discovery Collecting and analyzing domain/forest trust relationships as graph edges
T1018 Remote System Discovery Enumerating domain computers and their relationships (sessions, local admin)

Workflow

Step 1: Deploy BloodHound CE and log in

Bring up the stack, retrieve the generated password, and reset it on first login.

docker compose up -d
docker compose logs bloodhound 2>&1 | grep -i "Initial Password"
# Browse to http://localhost:8080  (default user: admin)

Step 2: Collect on-prem AD data with SharpHound

On a domain-joined Windows host (or via runas/token of a domain user), run the CE collector. -c All gathers every collection method; --outputdirectory controls where the ZIP lands.

# Full collection
.\SharpHound.exe -c All --outputdirectory C:\Temp
 
# Stealthier LDAP-only collection (no host touch for sessions/local-admin)
.\SharpHound.exe -c DCOnly --outputdirectory C:\Temp
 
# Looped session collection to map logged-on Tier-0 users over time
.\SharpHound.exe -c Session --loop --loopduration 02:00:00 --outputdirectory C:\Temp

From Linux, the Python collector (bloodhound-ce-python) is an alternative when you only have credentials and network access:

pip install bloodhound-ce
bloodhound-ce-python -u 'jdoe' -p 'Passw0rd!' -d corp.local -ns 10.0.0.10 -c All --zip

Step 3: Collect Entra ID / Azure data with AzureHound

Run AzureHound list with credentials, a JWT, or a refresh token, writing a single JSON file.

# Username/password (where allowed by CA/MFA policy)
./azurehound list -u "user@corp.com" -p "$PASSWORD" -t "corp.com" -o entra.json
 
# Using a previously acquired JWT (e.g., from roadtx / device-code phishing)
./azurehound list --jwt "$JWT" -o entra.json
 
# Using a refresh token
./azurehound list --refresh-token "$REFRESH_TOKEN" -t "<tenant-id>" -o entra.json

Step 4: Ingest data into BloodHound CE

Upload the SharpHound ZIP and the AzureHound JSON through Administration -> File Ingest, or POST to the API.

# API ingest (after obtaining a JWT from /api/v2/login)
TOKEN=$(curl -s http://localhost:8080/api/v2/login \
  -H 'Content-Type: application/json' \
  -d '{"login_method":"secret","username":"admin","secret":"<password>"}' \
  | python -c "import sys,json;print(json.load(sys.stdin)['data']['session_token'])")
 
# Start a file-upload job, then PUT the collector ZIP/JSON to it
JOB=$(curl -s -X POST http://localhost:8080/api/v2/file-upload/start \
  -H "Authorization: Bearer $TOKEN" | python -c "import sys,json;print(json.load(sys.stdin)['data']['id'])")
curl -s -X PUT "http://localhost:8080/api/v2/file-upload/$JOB" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/zip' \
  --data-binary @C:/Temp/BloodHound.zip
curl -s -X POST "http://localhost:8080/api/v2/file-upload/$JOB/end" \
  -H "Authorization: Bearer $TOKEN"

Step 5: Mark owned principals

In the UI, search for each compromised account/computer, open the node, and toggle Mark as Owned. Owned principals seed pathfinding queries ({owned:true}).

Step 6: Run built-in analysis queries

Open the Cypher tab or use the pre-built queries in the search panel:

  • Shortest Path to Domain Admins
  • Kerberoastable accounts with a path to high value
  • AS-REP roastable users
  • Principals with DCSync rights
  • ADCS misconfigurations (ESC1-ESC8 edges in CE)
  • Computers with unconstrained delegation

Step 7: Author custom Cypher for bespoke paths

Use Cypher for paths the built-ins do not cover, including hybrid AD-to-Entra chains.

// Shortest path from any owned principal to Domain Admins
MATCH p=shortestPath((n {owned:true})-[*1..]->(g:Group))
WHERE g.objectid ENDS WITH "-512"
RETURN p
 
// Kerberoastable users (SPN set) with a path to Domain Admins
MATCH (u:User {hasspn:true})
MATCH p=shortestPath((u)-[*1..]->(g:Group))
WHERE g.objectid ENDS WITH "-512"
RETURN p
 
// Principals who can DCSync the domain
MATCH (n)-[:MemberOf*0..]->()-[:DCSync|GetChanges|GetChangesAll*1..]->(d:Domain)
RETURN n.name, d.name
 
// ACL abuse from owned principals (GenericAll/WriteDacl/WriteOwner/ForceChangePassword)
MATCH p=(n {owned:true})-[:GenericAll|GenericWrite|WriteDacl|WriteOwner|ForceChangePassword*1..]->(t)
RETURN p
 
// Hybrid: on-prem user synced to an Entra account holding a privileged directory role
MATCH p=(u:User)-[:SyncedToEntraUser]->(:AZUser)-[:AZHasRole|AZGlobalAdmin*1..]->(r)
RETURN p

Step 8: Prioritize and document

Rank paths by hop count, stealth (avoid noisy edges like HasSession requiring host touch), and tooling availability. Record each path with the principals, edges, required actions, and a remediation note (e.g., remove the abusable ACL, tier the account).

Tools and Resources

Tool Purpose Source
BloodHound CE Graph attack-path platform (Docker stack) https://github.com/SpecterOps/BloodHound
SharpHound (CE) On-prem AD collector https://github.com/SpecterOps/SharpHound
AzureHound Entra ID / Azure RM collector https://github.com/SpecterOps/AzureHound
bloodhound-ce-python Linux Python AD collector https://github.com/dirkjanm/BloodHound.py
CE Quickstart Official deployment docs https://bloodhound.specterops.io/get-started/quickstart/community-edition-quickstart
BloodHound Cypher refs Edge/query documentation https://bloodhound.specterops.io/

Key Edge / Path Types

Edge / Path Meaning Escalation example
MemberOf Group membership Nested group reaches Domain Admins
GenericAll / WriteDacl Full/ACL control over an object Reset a privileged user's password
ForceChangePassword Can reset another principal's password Take over a Tier-0 account
HasSession A user is logged into a computer Steal a DA token from a compromised host
ADCSESC1..ESC8 Certificate services misconfig Enroll a cert as a privileged principal
SyncedToEntraUser / AZGlobalAdmin Hybrid identity edges On-prem foothold -> cloud Global Admin

Validation Criteria

  • BloodHound CE deployed and reachable; default admin password rotated
  • SharpHound data collected from all in-scope domains
  • AzureHound JSON collected for the in-scope tenant (if hybrid/cloud in scope)
  • Both datasets ingested successfully (no failed upload jobs)
  • Compromised principals marked as Owned
  • Shortest paths to Domain Admins / Global Administrator identified
  • ADCS and ACL-abuse paths enumerated
  • At least one custom Cypher query (including a hybrid query where applicable) executed
  • Paths prioritized by hops/stealth and documented with remediation
Source materials

References and resources

Everything below is rendered for inspection. Script files are read-only and never run.

References 2

api-reference.md2.2 KB

BloodHound CE Collectors & API Reference

SharpHound (CE / .NET collector)

Flag Purpose
-c, --collectionmethods <m> Methods: All, DCOnly, Session, LocalAdmin, ACL, Trusts, Group, GPOLocalGroup, Container, CertServices
--outputdirectory <dir> Directory to write the output ZIP
-d, --domain <fqdn> Target domain
--loop Repeat session collection in a loop
--loopduration HH:MM:SS How long to loop
--zipfilename <name> Name of output ZIP
--ldapusername / --ldappassword Alternate LDAP credentials
--stealth Reduced-footprint collection

bloodhound-ce-python (Linux)

Flag Purpose
-u <user> Username
-p <pass> Password
-d <domain> Domain FQDN
-ns <ip> Nameserver (DC IP)
-c All Collection methods
--zip Compress output into a ZIP
-k Use Kerberos authentication

AzureHound (Entra ID / Azure RM)

Flag Purpose
list Subcommand: collect all supported data
-u <user> Username
-p <pass> Password
-t <tenant> Tenant domain or ID
--jwt <token> Authenticate with an acquired JWT
--refresh-token <rt> Authenticate with a refresh token
-o <file> Output JSON file

BloodHound CE REST API (selected endpoints)

Method Endpoint Purpose
POST /api/v2/login Obtain a session JWT (login_method: secret)
POST /api/v2/file-upload/start Begin a file-ingest job
PUT /api/v2/file-upload/{id} Upload collector ZIP/JSON to the job
POST /api/v2/file-upload/{id}/end Finalize and trigger ingestion
POST /api/v2/graphs/cypher Run a Cypher query, return graph data
GET /api/v2/domains List ingested domains
GET /api/v2/pathfinding Pathfinding between two nodes

Common Cypher snippets

// Owned -> Domain Admins (RID 512)
MATCH p=shortestPath((n {owned:true})-[*1..]->(g:Group)) WHERE g.objectid ENDS WITH "-512" RETURN p
// Unconstrained delegation computers
MATCH (c:Computer {unconstraineddelegation:true}) RETURN c.name
// Entra Global Admins
MATCH p=(n)-[:AZGlobalAdmin*1..]->(:AZTenant) RETURN p
standards.md1.2 KB

Standards and Framework Mapping

NIST CSF 2.0

ID Name Rationale
ID.AM-03 Organizational communication and data flows are mapped BloodHound graphs the identity/permission "data flows" between principals across AD and Entra, mapping how privilege propagates through the environment.

MITRE ATT&CK

ID Name Rationale
T1069 Permission Groups Discovery Core technique: SharpHound/AzureHound enumerate group memberships and inter-principal permissions to build the attack graph.
T1087 Account Discovery Collectors enumerate user, computer, and service-principal accounts.
T1482 Domain Trust Discovery Domain/forest trusts are collected and rendered as graph edges.
T1018 Remote System Discovery Domain computers and their session/local-admin relationships are enumerated.

Supporting References

Scripts 1

agent.py4.7 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
"""
agent.py - BloodHound CE ingestion + Cypher helper.

Authenticates to a BloodHound Community Edition server's REST API, uploads a
SharpHound ZIP or AzureHound JSON file via the file-upload job workflow, and runs
Cypher queries (e.g. shortest path from owned principals to Domain Admins).

AUTHORIZED USE ONLY. BloodHound reveals privilege-escalation paths to full domain
/ tenant compromise. Use only against environments you are authorized to assess.

References:
  - BloodHound CE   https://github.com/SpecterOps/BloodHound
  - CE API docs     https://bloodhound.specterops.io/
"""
import argparse
import json
import os
import sys
import time
import urllib.request
import urllib.error

DA_SHORTEST_PATH = (
    'MATCH p=shortestPath((n {owned:true})-[*1..]->(g:Group)) '
    'WHERE g.objectid ENDS WITH "-512" RETURN p'
)


def _req(method, url, token=None, body=None, content_type="application/json", raw=False):
    headers = {}
    if token:
        headers["Authorization"] = f"Bearer {token}"
    data = None
    if body is not None:
        if raw:
            data = body
            headers["Content-Type"] = content_type
        else:
            data = json.dumps(body).encode()
            headers["Content-Type"] = "application/json"
    req = urllib.request.Request(url, data=data, headers=headers, method=method)
    try:
        with urllib.request.urlopen(req, timeout=60) as resp:
            text = resp.read().decode()
            return json.loads(text) if text.strip() else {}
    except urllib.error.HTTPError as e:
        msg = e.read().decode(errors="replace")
        raise SystemExit(f"[!] API {method} {url} -> HTTP {e.code}: {msg}")
    except urllib.error.URLError as e:
        raise SystemExit(f"[!] cannot reach {url}: {e.reason}")


def login(base, username, secret):
    r = _req("POST", f"{base}/api/v2/login", body={
        "login_method": "secret", "username": username, "secret": secret})
    token = r.get("data", {}).get("session_token")
    if not token:
        raise SystemExit("[!] login failed: no session_token returned")
    print("[+] authenticated to BloodHound CE")
    return token


def upload(base, token, path):
    if not os.path.isfile(path):
        raise SystemExit(f"[!] file not found: {path}")
    job = _req("POST", f"{base}/api/v2/file-upload/start", token=token)
    job_id = job.get("data", {}).get("id")
    if job_id is None:
        raise SystemExit("[!] could not start upload job")
    ctype = "application/zip" if path.lower().endswith(".zip") else "application/json"
    with open(path, "rb") as fh:
        _req("PUT", f"{base}/api/v2/file-upload/{job_id}", token=token,
             body=fh.read(), content_type=ctype, raw=True)
    _req("POST", f"{base}/api/v2/file-upload/{job_id}/end", token=token)
    print(f"[+] uploaded {os.path.basename(path)} (job {job_id}); ingestion queued")
    return job_id


def run_cypher(base, token, query):
    r = _req("POST", f"{base}/api/v2/graphs/cypher", token=token,
             body={"query": query, "include_properties": True})
    nodes = r.get("data", {}).get("nodes", {})
    edges = r.get("data", {}).get("edges", [])
    print(f"[+] query returned {len(nodes)} nodes / {len(edges)} edges")
    for nid, node in list(nodes.items())[:50]:
        label = node.get("label") or node.get("objectId") or nid
        print(f"    - {node.get('kind','?')}: {label}")
    return r


def main():
    p = argparse.ArgumentParser(description="BloodHound CE ingest + Cypher helper.")
    p.add_argument("--url", default="http://localhost:8080", help="BloodHound CE base URL")
    p.add_argument("--user", default="admin", help="Admin username")
    p.add_argument("--secret", default=os.environ.get("BHE_SECRET"),
                   help="Admin password (or set BHE_SECRET)")
    p.add_argument("--upload", action="append", default=[],
                   help="Collector file to ingest (ZIP or JSON); repeatable")
    p.add_argument("--cypher", help="Cypher query to run")
    p.add_argument("--da-path", action="store_true",
                   help="Run the built-in owned->Domain Admins shortest-path query")
    args = p.parse_args()

    if not args.secret:
        raise SystemExit("[!] provide --secret or set BHE_SECRET")

    base = args.url.rstrip("/")
    token = login(base, args.user, args.secret)

    for f in args.upload:
        upload(base, token, f)
    if args.upload:
        print("[i] waiting 10s for ingestion to begin...")
        time.sleep(10)

    if args.da_path:
        run_cypher(base, token, DA_SHORTEST_PATH)
    if args.cypher:
        run_cypher(base, token, args.cypher)
    if not (args.upload or args.cypher or args.da_path):
        print("[i] nothing to do; pass --upload, --cypher, or --da-path")
    return 0


if __name__ == "__main__":
    sys.exit(main())
Keep exploring