@@ -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