Credential importer + “Owned” tagging for BloodHound Community Edition (CE).
Inspired by knavesec/Max
-
auth — log in to BloodHound CE and cache a JWT (used by other commands for API).
-
patch — read a potfile (cracked creds) + NTLM file, parse, stats, and update graph nodes in Neo4j.
- Always sets:
Patchhound_has_hash(bool)Patchhound_has_pass(bool) – true when a cracked password is known
- With
-t/--tempalso writes (temporary values):Patchhound_nt– NTLM hashPatchhound_pass– plaintext password
These two are expected to be ephemeral; BloodHound CE does not persist custom node props across container restarts by default.
- With
-o/--ownedafter patching, discover eligible SIDs via Neo4j and then append “Owned” selectors via the BloodHound v2 HTTP API.- Lookups (who to own) happen only in Neo4j; the API is used only to append the Owned selectors.
- Always sets:
-
search (optional helper) — lightweight BHCE search.
Colors can be disabled with --no-color.
Verbose output via -v, I love verbose, can't miss an import.
- Minimal graph writes by default: only
Patchhound_has_hashandPatchhound_has_pass. - Temporary writes with
-t: addPatchhound_ntandPatchhound_pass. - “Owned” via API with
-o:- Query Neo4j for
:Usernodes wherePatchhound_has_pass = trueand a non-empty SID (objectid). - Also note which of those SIDs have a linked
:AZUserwith the same on‑prem SID. - Append those SIDs to an asset group using
PUT /api/v2/asset-groups/{id}/selectors.
- Query Neo4j for
- Better identity matching (no fallbacks; union of all paths):
:User {name}(e.g.,DOM\sam):User.samaccountname:User.userprincipalname/userPrincipalName:AZUser.userprincipalname/userPrincipalName:Computer.samaccountname- Case-insensitive comparisons for SAM/UPN; UPN can be synthesized from
dom.com\SAM → [email protected]when UPN token isn’t present.
- Cleaner output:
- Non-verbose: just the green checks
JWT valid,Potfile Check,NTLM Check,Neo4j auth OK, plus “Waiting…” banners before progress bars. - Verbose: pretty, line-per-stat blocks;
$HEX[...]decodes printed asnthash:HEXHASHCAT:password; excluded lines (with reasons).
- Non-verbose: just the green checks
python3 -m venv .venv && source .venv/bin/activate
pip install neo4j requestsDefaults live in src/conn.py:
DEFAULT_URIDEFAULT_USERDEFAULT_PASS(BH CE default isbloodhoundcommunityedition)
Adjust there or wire flags as you prefer.
python3 PatchHound.py --help
python3 PatchHound.py [--no-color] [subcommand] -hpython3 PatchHound.py auth -u http://localhost:8080/ -U admin -p 'Exclude -p for PassPrompt' [-v]- Writes a session file at:
${TMPDIR}/patchhound.session.json
python3 PatchHound.py patch -c crack.potfile -n ntds.txt [-t] [-o] [-v]What it reads
-c/--clearspotfile — expected format:<32hex_ntlm>:<password>$HEX[...]values are decoded; in verbose you’ll see lines like:nthash:$HEX[hex...]:decoded-password- Any line that doesn’t match the expected schema is excluded and listed (verbose) with a reason.
-n/--ntlmhash file — consumes lines containing one or more 32-hex NTLMs and an account token:- prefers
DOMAIN\SAM; captures explicit UPN tokens where present; synthesizes UPN asSAM@fqdnwhen possible. - Non-conforming lines are excluded with reasons (verbose).
- prefers
How matching works (Neo4j)
- For each candidate row we try all of these simultaneously:
(:User {name})(:User).samaccountname(:User).userprincipalname | userPrincipalName(:AZUser).userprincipalname | userPrincipalName(:Computer).samaccountname
- Comparisons for SAM/UPN are case-insensitive (
toUpperon both sides). - Nodes are de-duplicated and updated idempotently.
What gets written
- Always:
Patchhound_has_hash = truePatchhound_has_pass = (pwd != null)
- With
-t/--temp:Patchhound_nt = <ntlm>Patchhound_pass = <password>
Output
- Non-verbose:
[+] JWT valid [+] Potfile Check [+] NTLM Check [+] Neo4j auth OK [+] Waiting for Neo4j Applying [████████████████████████████] 14598/14598 (100%) [+] Updated nodes: 1234 - Verbose also prints pretty stats, decoded HEX, and excluded input lines.
-
After patching (or even when nothing is applied),
-owill:- Query Neo4j for users where
Patchhound_has_pass = trueand a non-empty SID (u.objectid). - Count how many of those SIDs have a matching
:AZUseron‑prem SID (several property names supported). - Append those SIDs to the configured asset group via BHCE v2 API.
- Query Neo4j for users where
-
Non-verbose:
[+] Waiting for Neo4j and API Owned API [████████████████████████████] 1876/1876 (100%) [+] Owned API: attempted 1876 selector adds -
Verbose final summary (printed after all logic):
[*] Owned summary: users_with_password_true : 1876 with_sid : 1876 distinct_sids_sent : 1876 sids_with_azuser_link : 0 asset_group_id : 2 example_request: PUT http://localhost:8080/api/v2/asset-groups/2/selectors payload: [{"selector_name":"Manual","sid":"S-1-5-21-...","action":"add"}]
-v, --verbose— verbose output (stats, excluded lines, HEX decodes, summaries)--no-color— disable colored/ASCII outputpatch:-c, --clears— path to potfile (required)-n, --ntlm— path to NTLM hash file (recommended)-t, --temp— writePatchhound_ntandPatchhound_pass-o, --owned— append “Owned” selectors via API based on Neo4j discovery
auth:-u, --url-U, --username-p, --password
- BloodHound CE typically does not persist arbitrary custom node properties across container restarts; the temporary fields (
Patchhound_nt,Patchhound_pass) are intended as ephemeral conveniences. - UPNs can be lower or upper; comparisons are case-insensitive. For logs, you might see uppercase normalization.
:AZUseroften lackssamAccountName; when hybrid, an on-prem SID may be available under several different property names. The matcher checks the common ones.
Neo4j driver not installed→pip install neo4jJWT invalid→ re-runauth- No updates applied:
- Potfile has no valid lines (format must be
32hex:password) - NTLM file had no
(acct, hash)pairs
- Potfile has no valid lines (format must be
- Owned append failed:
- Check API base URL and JWT in the session file
- Verify your account can mutate asset groups; AAD proxy / reverse proxy headers can interfere






