NTDS.dit parser with full record support and BloodHound export
NTDS.dit parser that supports: parsing and decrypting secret records (user hashes, supplemental credentials). Remaining work includes: Implement parsing for all record types (groups, ACLs, domain objects, etc.). Add BloodHound-compatible JSON export for seamless integration.
Internally we have this already, but it needs a real big polish before it's suitable for open-source!
Edit: I only saw #1347 after this issue. Perhaps we can contribute some code of ours too!
That would be great! I’ll be happy to review and work together on polishing it for open-source.
If you have some test data, I’d really appreciate it. Right now, I’m only testing with my own DC, but I can’t share those files since they contain domain secrets. I’d be happy to test on any sample data you could provide.
Really cool stuff @B0TAxy! As @Schamper mentioned, internally we already have an NTDS.dit parser, but a major refactor and update of that code is what I (and previously, some other colleagues that definitely deserve credit for that) have been working on for a while now. Our idea is to opensource (parts of) that parser as well, but this is still an ongoing discussion.
Regarding the TODO's you mention; implementing efficient parsing of Group/ACL/ACE/User/Computer etc objects is exactly what our code is aiming at, so this will be complementary to your code. I did not yet look at parsing & decrypting secrets, so really nice that you already figured that out! Our goal is to be able to query the NTDS.dit using LDAP search filters and and returning proper Python classes/objects representing the different AD objects. A sneak peak of what's currently working (based on some testdata that is not public):
In [1]: import gzip
...: from dissect.esedb.ntds import NTDS
In [2]: fh = gzip.GzipFile("tests/_data/ntds.dit.gz", "rb")
...: ntds = NTDS(fh)
# Interact with NTDS.dit file using NTDS class/API
In [3]: users = list(ntds.users())
In [4]: len(users)
Out[4]: 9
In [5]: users[0]
Out[5]: User(name=Administrator, sAMAccountName=Administrator, is_machine_account=False)
In [6]: groups = ntds.groups()
In [7]: da = next(x for x in groups if x.name == "Domain Admins")
In [8]: da.members()
Out[8]: <generator object Group.members at 0xffff7c80f1c0>
In [9] next(da.members())
Out[9]: User(name=Administrator, sAMAccountName=Administrator, is_machine_account=False)
# Direct interaction via LDAP Search Filters
In [10]: results = ntds.query("(&(objectClass=user)(sAMAccountName=Administrator))")
In [11]: next(results)
Out[11]: User(name=Administrator, sAMAccountName=Administrator, is_machine_account=False)
I'm currently working on creating some test data to perform a test on the functionality, as well as the performance. I'm planning to run the https://github.com/davidprowe/BadBlood script in a dummy domain for this purpose. I'd say, lets stay in touch via this issue. Once I have some test data available, I'll probably upload it in the dissect.database repo, but I'll give a ping here.
Hi @joost-j ,
Thanks for the detailed update! I’d be glad to collaborate on this. Really appreciate you working on the test data — I’ll wait for your ping once it’s available.
I’m currently implementing group extraction as well as security descriptor parsing, so if you have any snippets or examples, I’d be happy to take a look and integrate them.
Looking forward to working together on this!
Quick idea (just brainstorming): once LDAP search filters are in place, we could run a lightweight fake‑LDAP endpoint that answers SharpHound’s queries from an extracted NTDS.dit (so SharpHound runs unchanged but targets local offline data).
That way, we wouldn’t need to add BloodHound‑compatible JSON output.
Would love to hear what you think.
Quick update, I'm still finalizing all the code and tests. I've added some test data to the branch just now that I use for local testing: https://github.com/fox-it/dissect.database/commit/369c0761806e6fbcda06e57a24eafe770e50bbc6.
I set up a dummy Domain Controller, created a domain DISSECT.local and ran the BadBlood tool with a small number of groups/users/computers on the domain controller. Some information about what should be in there:
PS C:\Users\Administrator.DC01> (Get-ADUser -Filter *).count
15
PS C:\Users\Administrator.DC01> (Get-ADComputer -Filter *).count
15
PS C:\Users\Administrator.DC01> (Get-ADGroup -Filter *).count
54
To validate your implementation, you could try and see if you can obtain the secrets of these users:
Password of henk.devries: Winter2025!
Password of beau.terham: Zomer2027!@
Password history of beau.terham: Zomer2025!@ & Zomer2026!@
They should be easy to crack :smile: Planning to upload the rest of my code soon!
Hi @joost-j , sorry for the wait — I’ve been a bit busy lately. Thanks for the update and the extra test data! I’ll take a look at it soon and get back to you once I’ve gone through everything.
Hi @joost-j — I looked at it after updating my code per your MR comment.
I can confirm I successfully extracted the hashes from the data you added, but in the test dataset you pushed there isn’t a single record that contains lmPwdHistory or ntPwdHistory, so I couldn’t verify extraction of those attributes against that dataset. In private ntds.dit testing the extraction of lmPwdHistory / ntPwdHistory did appear to work as expected, though, so I think the logic is fine — it’s just the branch test data that’s missing those fields.
Thanks again for the suggestion and the test data .
Attached are two screenshots: one from my plugin displaying the extracted hash, and one from CyberChef showing the hashed password. Both screenshots show identical values, which confirms that the extraction succeeded.