BloodHound Deep Dive Part 5: Kerberoasting and AS-REP Roasting Through the Graph

Impacket Deep Dive Part 3 and Part 4 found svc-legacy-scan and svc-backup by running GetADUsers.py against the whole domain and reading the userAccountControl flags by eye. That works on a four-account lab. It does not work on a domain with four thousand accounts, most of which have no interesting flags set at all. BloodHound’s object properties exist to make that search a single query instead of a manual review.

hasspn and dontreqpreauth

When SharpHound’s ObjectProps collection method runs, it reads each user’s Kerberos-relevant userAccountControl bits and, critically, whether the account has a Service Principal Name registered, and writes both as boolean properties directly on the User node: hasspn for Kerberoastable accounts, dontreqpreauth for AS-REP roastable ones. Both are real BloodHound schema properties, not conveniences invented for this series, and the queries against them are exactly this direct:

neo4j> MATCH (u:User {hasspn:true}) WHERE NOT u.name STARTS WITH 'KRBTGT' RETURN u.name AS kerberoastable;
kerberoastable
"SVC-BACKUP@CONTOSO.LOCAL"

neo4j> MATCH (u:User {dontreqpreauth:true}) RETURN u.name AS asrep_roastable;
asrep_roastable
"SVC-LEGACY-SCAN@CONTOSO.LOCAL"

Both results, run against the same seeded Neo4j instance from Parts 3 and 4, land on exactly the two accounts Impacket Deep Dive spent an entire post each cracking. That’s not a coincidence built for this series, it’s the actual point: a DCOnly SharpHound run, taking minutes against a domain of any size, would have identified svc-backup and svc-legacy-scan as targets before GetUserSPNs.py or GetNPUsers.py ever needed to run. Impacket’s enumeration posts did by hand, against four accounts, what these two properties do in one query against however many accounts a real domain has.

Where roastable accounts sit on the graph

Knowing an account is roastable answers “can I get a crackable hash from this.” It doesn’t answer “is that hash worth having.” BloodHound’s canned “Shortest Path from Kerberoastable Users to Domain Admins” query chains both questions into one:

neo4j> MATCH (A:User),(B:Group),p=shortestPath((A)-[*1..]->(B)) WHERE A.hasspn=true AND B.name STARTS WITH 'DOMAIN ADMINS' RETURN p;

Against this graph, exactly like Part 4’s plain shortestPath query, that returns nothing. svc-backup is Kerberoastable and a member of Backup Operators, but nothing in the seeded graph connects Backup Operators onward to Domain Admins, for the same reason Part 4 covered: the backup-operator-to-DC-compromise technique is a real OS privilege, not a modeled AD ACL edge. Cracking svc-backup’s ticket, which Impacket Part 4 did successfully with hashcat, gets an attacker Backup Operators membership and, in the real world, a path to the DC’s NTDS.dit through SeBackupPrivilege abuse. It does not get them a graph-confirmed path to Domain Admins, and BloodHound is honest about that rather than overstating it.

svc-legacy-scan, the AS-REP roastable account, fares worse on the graph directly: nothing in the seeded lab gives it group membership or ACL exposure beyond being a normal domain user, until Part 4’s ForceChangePassword chain reaches it from a completely different direction, IT-HELPDESK through Backup Operators, not through its own AS-REP roastability at all. The two paths to svc-legacy-scan, roast-and-crack versus reset-via-ACL-abuse, exist independently in this lab, and a real analyst working from the graph would notice both and, per Part 6, likely prefer the one that doesn’t depend on hashcat’s runtime at all.

Why the graph is worth running before the crack

Password Cracking and Wordlist Engineering’s own numbers put an RTX 4090 at roughly 3.48 GH/s against Kerberoast hashes and 3.56 GH/s against AS-REP hashes, fast, but not instant against a strong password, and every hash cracked is time spent that a graph query would have spent in milliseconds. On a domain where hasspn returns hundreds of accounts, most attached to nothing useful, running BloodHound’s shortest-path query against the whole set first, and only then handing the survivors to GetUserSPNs.py and hashcat, is the difference between cracking two accounts and cracking two hundred to find the same two that matter.

Next: Part 6, where the IT-HELPDESK to Backup Operators to svc-legacy-scan chain from Part 4 gets walked end to end with a real tool, not just queried.