Skip to content

Bloodhound CE support #52

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
TheToddLuci0 opened this issue Apr 28, 2025 · 1 comment
Open

Bloodhound CE support #52

TheToddLuci0 opened this issue Apr 28, 2025 · 1 comment

Comments

@TheToddLuci0
Copy link
Contributor

I've been working on rewriting some of the queries in Max's brain to work with the new database structure, and it's looking like it's going to require a rewrite of most of the queries. At this point, I'm trying to figure out how best to submit the changes here.

Couple of options I'm seeing:

  1. Drop support for OG bloodhound, only support CE
  2. Add some manner of specifying (or detecting?) which version of BH the database is based on
    • This has the side effect of making max even bigger and harder to maintain, since it's basically two code bases in one
  3. Maintain a separate branch in the same repo (ie main for legacy and main-ce for bloodhound-ce support
  4. Hard fork to a separate repo for CE support
  5. Complete rewrite to try and make better use of the new database schemas / features (rather than just replacing the existing queries with as much as a like-for-like as possible)
  6. Try to implement some kind of "make BH-CE compatible with Max.py" function, and keep the rest of the existing code base.

Looking around, doesn't seem to be a super clear "standard" approach. Posing this issue here to see what the feel is @knavesec

Technical details

Ok, so for some (I'm sure quite smart and not annoying at all) reason, BH no longer uses simple things like .high_value or .owned as clear attributes on a given node. In other words, doing queries like MATCH (u:User {owned:True}) no longer works. Instead, those attributes are shoved into a string delimited single field called system_tags. Basically, it maps like this:
n.high_value = True -> n.system_tags = "admin_tier_0"
n.owned = True -> n.system_tags = "owned"
n.owned = True, n.high_value = True -> n.system_tags = "admin_tier_0 owned"

This, of course, makes queries a lot harder. Using the built-in quires in BH-CE as a reference, it looks like n.system_tags isn't even guaranteed to exist, and since it's a string attribute, not a boolean attribute, just calling CONTAINS isn't enough. Therefore, the basic structure of a simple query changes. (Note the addition of COALESCE to ensure that there's an empty string if n.system_tags doesn't exist.

Old:

MATCH (u:User {owned=True}) RETURN u

New:

MATCH (u:User) WHERE COALESCE(u.system_tags, '') CONTAINS 'owned' RETURN u

This, of course, makes queries gross to write, and gets gross fast.

A more complex example, paths from owned to HVTs

Old:

MATCH shortestPath((n {owned:True})-[*1..]->(m {highvalue:True})) RETURN DISTINCT n.name

New (This is directly from the BH examples):

MATCH p=shortestPath((s)-[:Owns|GenericAll|GenericWrite|WriteOwner|WriteDacl|MemberOf|ForceChangePassword|AllExtendedRights|AddMember|HasSession|GPLink|AllowedToDelegate|CoerceToTGT|AllowedToAct|AdminTo|CanPSRemote|CanRDP|ExecuteDCOM|HasSIDHistory|AddSelf|DCSync|ReadLAPSPassword|ReadGMSAPassword|DumpSMSAPassword|SQLAdmin|AddAllowedToAct|WriteSPN|AddKeyCredentialLink|SyncLAPSPassword|WriteAccountRestrictions|WriteGPLink|GoldenCert|ADCSESC1|ADCSESC3|ADCSESC4|ADCSESC6a|ADCSESC6b|ADCSESC9a|ADCSESC9b|ADCSESC10a|ADCSESC10b|ADCSESC13|SyncedToEntraUser|CoerceAndRelayNTLMToSMB|CoerceAndRelayNTLMToADCS|WriteOwnerLimitedRights|OwnsLimitedRights|CoerceAndRelayNTLMToLDAP|CoerceAndRelayNTLMToLDAPS|Contains|DCFor|TrustedBy*1..]->(t))
WHERE COALESCE(t.system_tags, '') CONTAINS 'admin_tier_0' 
AND s<>t
AND COALESCE(s.system_tags, '') CONTAINS 'owned'
RETURN DISTINCT s.name

This gets even grosser when we want to start modifying things. For example, to mark something as owned (as with --mark-owned), we can no longer just do

MATCH (u.User {username=JOHN.DOE@EXAMPLE.COM}) SET u.owned = True

Instead, we have to do something like

MATCH (u:User {username=JOHN.DOE@EXAMPLE.COM})
SET u.system_tags=(
    CASE WHEN COALESCE(n.system_tags, "") CONTAINS "owned" 
    THEN n.system_tags 
    ELSE trim(COALESCE(n.system_tags, "") + " owned" ) 
    END
)

To break that down:

  1. Match the user
  2. Use a CASE to check if the user is already owned
  3. If so, leave as is
  4. Otherwise:
  5. COALESCE with an empty string so we have a non-null
  6. Append owned with a space, in case something already exists
    - This prevents `u.system_tags = "admin_tier_0owned"
  7. TRIM the string, in case it doesn't already have some tag
    - This prevents u.system_tags = " owned"

The point

The point of these examples is that it's not quite as simple as pointing max at a new DB. I'm still working on getting all the queries rewritten and optimized, but it feels like we'll have to make some change here to keep max useful, since bloodhound legacy isn't getting updates anymore.

Open to ideas, thoughts, whatever. Max is a good boy, I don't want to put him down because someone changed the DB schema

@tigre-bleu
Copy link
Contributor

Just to give a personal feedback, I almost stopped using bloodhound legacy because CE version is mostly on-par with the legacy version feature wise as well as being more adapted to multi-project (using https://github.com/Tanguy-Boisset/bloodhound-automation).

I would say that dropping support for legacy bloodhound is fine. Maintaining the two branches seems like a lot of (mostly useless) effort.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants