Skip to content

Commit db1c4ee

Browse files
authored
fix: update shadow credential detection to exclude forcechangepassword rights (#332)
1 parent 383db3a commit db1c4ee

1 file changed

Lines changed: 17 additions & 13 deletions

File tree

ares-cli/src/orchestrator/automation/shadow_credentials.rs

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -233,16 +233,18 @@ fn extract_target_user(
233233
/// NT hash via certipy auth).
234234
///
235235
/// Includes the obvious primitives (GenericAll, GenericWrite, WriteDacl,
236-
/// WriteOwner) plus three that the lab's BloodHound exposed but the
236+
/// WriteOwner) plus two that the lab's BloodHound exposed but the
237237
/// original matcher missed:
238-
/// - `allextendedrights`: subsumes User-Force-Change-Password and most
239-
/// extended rights — equivalent to GenericAll for shadow-creds purposes.
240-
/// - `writeproperty`: a property write that explicitly covers
241-
/// msDS-KeyCredentialLink (BloodHound's targetedwrite analogue).
242-
/// - `forcechangepassword`: while normally used to reset the password,
243-
/// the same WriteProperty extended right also lets us write
244-
/// msDS-KeyCredentialLink, so certipy_shadow works without destroying
245-
/// the lab's seeded password.
238+
/// - `allextendedrights`: subsumes every extended right on the target,
239+
/// including the property-write needed for msDS-KeyCredentialLink —
240+
/// equivalent to GenericAll for shadow-creds purposes.
241+
/// - `writeproperty`: a property write that covers msDS-KeyCredentialLink
242+
/// (BloodHound's targetedwrite analogue).
243+
///
244+
/// `forcechangepassword` is deliberately excluded: the User-Force-Change-
245+
/// Password extended right grants password reset only, not the property
246+
/// write required for msDS-KeyCredentialLink. Those vulns are routed to
247+
/// `auto_dacl_abuse` → `bloodyad_set_password` instead.
246248
///
247249
/// All forms accept both the bare and `acl_`-prefixed shapes emitted by
248250
/// ldap_acl_enumeration's parser.
@@ -256,14 +258,12 @@ pub(crate) fn is_shadow_cred_candidate(vuln_type: &str) -> bool {
256258
| "shadow_credentials"
257259
| "allextendedrights"
258260
| "writeproperty"
259-
| "forcechangepassword"
260261
| "acl_genericall"
261262
| "acl_genericwrite"
262263
| "acl_writedacl"
263264
| "acl_writeowner"
264265
| "acl_allextendedrights"
265266
| "acl_writeproperty"
266-
| "acl_forcechangepassword"
267267
)
268268
}
269269

@@ -295,11 +295,9 @@ mod tests {
295295
assert!(is_shadow_cred_candidate("allextendedrights"));
296296
assert!(is_shadow_cred_candidate("AllExtendedRights"));
297297
assert!(is_shadow_cred_candidate("writeproperty"));
298-
assert!(is_shadow_cred_candidate("forcechangepassword"));
299298
// ACL-prefixed forms emitted by ldap_acl_enumeration parser.
300299
assert!(is_shadow_cred_candidate("acl_allextendedrights"));
301300
assert!(is_shadow_cred_candidate("acl_writeproperty"));
302-
assert!(is_shadow_cred_candidate("acl_forcechangepassword"));
303301
assert!(is_shadow_cred_candidate("acl_writeowner"));
304302
}
305303

@@ -311,6 +309,12 @@ mod tests {
311309
assert!(!is_shadow_cred_candidate("unconstrained_delegation"));
312310
assert!(!is_shadow_cred_candidate("genericall_computer"));
313311
assert!(!is_shadow_cred_candidate(""));
312+
// ForceChangePassword only grants password reset, not
313+
// msDS-KeyCredentialLink writes. Routed to auto_dacl_abuse →
314+
// bloodyad_set_password instead of certipy_shadow.
315+
assert!(!is_shadow_cred_candidate("forcechangepassword"));
316+
assert!(!is_shadow_cred_candidate("ForceChangePassword"));
317+
assert!(!is_shadow_cred_candidate("acl_forcechangepassword"));
314318
}
315319

316320
#[test]

0 commit comments

Comments
 (0)