Skip to content

fix(ad-tombstone): collect ownership/DACL reanimation paths, also fix queries import - #15

Open
Hidaro wants to merge 1 commit into
JVBotelho:mainfrom
Hidaro:queries-fix-and-other-paths
Open

fix(ad-tombstone): collect ownership/DACL reanimation paths, also fix queries import#15
Hidaro wants to merge 1 commit into
JVBotelho:mainfrom
Hidaro:queries-fix-and-other-paths

Conversation

@Hidaro

@Hidaro Hidaro commented Jul 30, 2026

Copy link
Copy Markdown

GhostHound_CanReanimate was only emitted for principals holding the formal Reanimate-Tombstones control access right (GUID 45ec5156-db7e-47bb-b53f-dbeb2d03c40f) at the domain NC root. It missed an equally usable path: a principal who owns a tombstone, or holds WRITE_DAC/WRITE_OWNER on it, can rewrite that object's DACL and grant itself the right.

This commit aims to add those paths and also include more queries that can be use to gain more information about the tombstones.

Please note that the code was AI generated, and while this works properly on my end, there's probably slop in there that can be made better. This is just for other people that could have the same issue I encountered and want a quick fix

@Hidaro
Hidaro requested a review from JVBotelho as a code owner July 30, 2026 10:48
///
/// Does **not** check `ace_type`/`ace_flags` -- callers must gate on `is_allow_ace` and
/// `applies_to_self` first.
fn grants_reanimate_right_on_object(ace: &Ace) -> bool {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

grants_reanimate_right_on_object() classifies an ACE naming the Reanimate-Tombstones GUID on the tombstone itself as a formally held ReanimateRight. This conflicts with the existing NC-root model and is not demonstrated by the evidence in ADR-0007, which confirms object writability but not that the DC honors this ACE placement during restoration.
Please validate this in a disposable lab using a principal with this per-object ACE but no effective Reanimate-Tombstones right at the NC root. If restoration fails, this must not be emitted as reanimate_right.

domain_rights: &[String],
object_paths: &[ReanimationPath],
) -> Vec<Edge> {
let mut mechanisms_by_sid: BTreeMap<&str, BTreeSet<ReanimateMechanism>> = BTreeMap::new();

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

domain_rights and object_paths are merged as interchangeable alternatives. This emits GhostHound_CanReanimate when the principal has only the NC-root right or only control over the deleted object.
ACL writability alone does not demonstrate that object control is an independent restoration mechanism. Please verify end-to-end whether ownership, WRITE_DAC, or WRITE_OWNER, without any effective NC-root right, can actually restore the object. Until that is demonstrated, these should be emitted as separate facts rather than collapsed into the same confirmed relationship.

let start = bloodhound_opengraph::EdgeEndpoint::new(sid.clone(), "id");
let end = bloodhound_opengraph::EdgeEndpoint::new(target_id.clone(), "id");
builder.add_edge(Edge::new(start, end, "GhostHound_CanReanimate"));
for edge in reanimation_edges(

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The final CanReanimate edge is emitted without evaluating CREATE_CHILD on lastKnownParent or another selected destination container. This is an independent restoration prerequisite.
This gap predates the PR, but the new mechanisms make the relationship's claim broader. Please either collect the destination ACL or explicitly scope the relationship and documentation as a partial capability whose destination permissions have not been verified.


if let Some(dacl) = &sd.dacl {
for ace in &dacl.aces {
if !is_allow_ace(ace.ace_type) || !applies_to_self(ace.ace_flags) {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

analyses discard deny ACEs and treat each applicable allow ACE as effective access. An allow ACE is evidence of a grant, but not necessarily effective permission when ordered deny ACEs and the requester's complete token are considered.
This is partly pre-existing and can be handled as a tracked follow-up, but the output should not claim confirmed effective access unless that calculation is performed. Please also add a regression test containing a deny followed by an allow for the same SID and right.

// ACL-rewrite path on the NC root is a different (far broader) finding than
// tombstone reanimation. Per-object ownership is handled by
// `analyze_reanimation_control`.
if is_allow_ace(ace.ace_type)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

analyses discard deny ACEs and treat each applicable allow ACE as effective access. An allow ACE is evidence of a grant, but not necessarily effective permission when ordered deny ACEs and the requester's complete token are considered.
This is partly pre-existing and can be handled as a tracked follow-up, but the output should not claim confirmed effective access unless that calculation is performed. Please also add a regression test containing a deny followed by an allow for the same SID and right.

pub fn analyze_reanimation_control(sd: &SecurityDescriptor) -> Vec<ReanimationPath> {
let mut by_sid: BTreeMap<String, BTreeSet<ReanimateMechanism>> = BTreeMap::new();

if let Some(owner) = &sd.owner {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ownership and WRITE_OWNER are treated as unconditionally leading to WRITE_DAC. Owner Rights restrictions and BlockOwnerImplicitRights can prevent that inference in some environments. This does not need to block the PR by itself, but the mechanism should be documented as conditional or potential rather than universally effective.

// principal has no READ_CONTROL on this object; an unparseable one means a malformed blob.
// Neither is fatal to enumerating the rest of the tombstone, so both degrade to "no
// ownership/ACL data" rather than failing the whole object.
let (owner_sid, reanimation_paths) = raw_attr(entry, "nTSecurityDescriptor")

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SecurityDescriptor::parse(...).ok() discards parsing failures and produces the same result as a missing or unreadable attribute. This makes collection failures indistinguishable from malformed data.
Please preserve an explicit status such as Parsed, Missing, or Malformed, so that absence of findings is diagnosable.

DACL rather than only walking the ACEs (`analyze_reanimation_control`, `ReanimateMechanism`).
- **Reading `nTSecurityDescriptor` requires the `SD_FLAGS` control** (`1.2.840.113556.1.4.801`,
`OWNER|GROUP|DACL`). Ask for the attribute without it and AD tries to include the SACL, which
needs `SeSecurityPrivilege` — so the DC drops the attribute from the response entirely rather

@JVBotelho JVBotelho Aug 6, 2026

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Readme present all four mechanisms as operationally equivalent forms of CanReanimate. If the object-only lab cases fail, the relationship must be split into separately named capabilities and these descriptions and queries must be updated accordingly.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The queries present all four mechanisms as operationally equivalent forms of CanReanimate. If the object-only lab cases fail, the relationship must be split into separately named capabilities and these descriptions and queries must be updated accordingly.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The queries present all four mechanisms as operationally equivalent forms of CanReanimate. If the object-only lab cases fail, the relationship must be split into separately named capabilities and these descriptions and queries must be updated accordingly.

/// ownership plus WRITE_DAC/WRITE_OWNER on the tombstone, and no formal extended right
/// anywhere.
#[test]
fn test_edges_from_object_control_only() {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test_edges_from_object_control_only and test_edge_source_for_write_dac_only assert that object control alone produces a final CanReanimate edge. These tests correctly describe the current implementation, but they also lock in the disputed domain assumption. If the object-only lab cases fail, the tests should expect either no final restoration edge or a separate partial-capability relationship.

}

#[test]
fn test_edge_source_for_write_dac_only() {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test_edges_from_object_control_only and test_edge_source_for_write_dac_only assert that object control alone produces a final CanReanimate edge. These tests correctly describe the current implementation, but they also lock in the disputed domain assumption. If the object-only lab cases fail, the tests should expect either no final restoration edge or a separate partial-capability relationship.

@JVBotelho JVBotelho left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey hey @Hidaro , first of all thanks for the PR!

Could you validate the new mechanisms end-to-end in a disposable AD lab and attach the ACL setup, commands, LDAP result code, and final object state for each case below?
Per-object Reanimate-Tombstones GUID ACE only, with no effective right at the NC root.
Ownership only, with no effective NC-root right.
WRITE_DAC only, with no effective NC-root right.
WRITE_OWNER only, with no effective NC-root right.
NC-root right plus object write access, but no CREATE_CHILD on the destination.
NC-root right, required object write access, and destination CREATE_CHILD as the positive control.
Please verify the principal's effective group membership and the NC-root ACL in every case. bloodyAD get writable is useful evidence of ACL writability, but what we need to establish here is whether the actual restore operation succeeds or fails, and with which LDAP result code.
If you do not have the time or a suitable lab environment to run these tests, that is completely fine. We can convert the PR back to draft, and I can take ownership of the lab validation. Once the results are available, we can update the relationship semantics, documentation, and tests as needed before moving the PR back to ready for review.
The collection work itself is valuable; the purpose of this validation is only to ensure that the emitted CanReanimate edges accurately represent the authorization checks enforced by the DC.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The README and ADR-0007 state that the saved-query pack moved from the legacy crates/ad-tombstone/queries.json file to one JSON file per query under crates/ad-tombstone/queries/. ADR-0007 also explains that the legacy format is not directly importable by BloodHound CE. The old file is still present because this PR does not delete it. Please remove it to avoid shipping two contradictory query packs.

@JVBotelho JVBotelho added enhancement New feature or request rust Pull requests that update rust code labels Aug 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request rust Pull requests that update rust code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants