ADPRIV-010: Privileged Users Password Never Expires

Platform
Active Directory
Category
AD Privileged Account Security
Severity
Critical
Zero Trust pillar
Identity (weight 2)
Golden fixtures
3
Branch coverage
Observed: fixtures prove the verdicts they exercise
Provenance
baseline

What it checks

The check issues a single LDAP query against the default naming context filtering on (&(objectCategory=person)(objectClass=user)(adminCount=1)(userAccountControl:1.2.840.113556.1.4.803:=65536)). The matching rule 1.2.840.113556.1.4.803 is the bitwise-AND operator, and 65536 (0x10000) is the ADS_UF_DONT_EXPIRE_PASSWD flag. For every returned object the check records: - sAMAccountName, distinguishedName, objectSid - userAccountControl (full bitmask, decomposed) - adminCount (will be 1 for any current or historical protected member) - pwdLastSet (raw FILETIME, converted to DateTime, age in days) - lastLogonTimestamp - servicePrincipalName (presence converts the account into a Kerberoasting target) - memberOf (with recursive expansion to detect Domain Admins, Enterprise Admins, Schema Admins, Account Operators, Backup Operators, Server Operators, Print Operators, DnsAdmins, and any custom group that carries DCSync rights) - msDS-ResultantPSO (the Fine-Grained Password Policy actually applied to the principal, so coverage gaps can be reported) - Whether the account is also in Protected Users (S-1-5-21-domain-525) The check additionally cross-references adminCount = 1 accounts that no longer hold protected-group membership (SDProp orphans) since these frequently carry the legacy DONT_EXPIRE_PASSWORD flag set during a long-ago Tier 0 assignment and never cleared. Findings are emitted per-account with the full UAC decomposition, the password age in days, and the resolved privileged-group path so an analyst can triage without re-querying AD.

Why it matters

Maximum password age exists for one reason: to bound the useful life of a stolen credential. The moment the password rotates, every NT hash, every RC4 long-term key, every cached Kerberos ticket, every offline crack derived from a prior NTDS dump becomes worthless against that account. DONT_EXPIRE_PASSWORD removes that bound. For a privileged account the consequences compound across three independent attack surfaces. First, Kerberoasting (T1558.003). Any Domain Admin or Tier 0 account that also carries a servicePrincipalName is Kerberoastable by any authenticated user. The attacker requests a TGS-REP and cracks the RC4 (or AES) encrypted blob offline. With DONT_EXPIRE_PASSWORD set, the attacker can run a multi-month hashcat campaign against the captured ticket and the recovered password will still be valid when the run completes. Without the flag the same ticket would expire from utility as soon as the next rotation cycle hit. Second, NTDS.dit hash longevity. Once an attacker has DCSync rights or restores a Domain Controller backup, they extract the NT hash and Kerberos long-term keys for every account. Hashes for accounts with DONT_EXPIRE_PASSWORD will continue to authenticate via NTLM and Kerberos indefinitely. Pass-the-hash, overpass-the-hash, and silver-ticket forgery against the principal all remain viable for the lifetime of the account, surviving incident response, password resets on other accounts, even tabletop-cleared eviction events. Third, AS-REP roasting persistence and stale-credential reuse. Combined with DONT_REQUIRE_PREAUTH (ADKERB-002), DONT_EXPIRE_PASSWORD produces an account whose AS-REP material can be harvested anonymously and cracked at leisure with no time pressure. More generally, every breach-corpus leak (HaveIBeenPwned, COMB, RockYou2024) that contains a match against a non-expiring privileged password remains exploitable forever. Microsoft and Mandiant incident-response retainers repeatedly cite long-static privileged passwords as the reason a six-month-old NTDS dump still gave the attacker Domain Admin on the day they came back. The Microsoft administrative-tier model and Microsoft Security Compliance Toolkit baselines explicitly require that Tier 0 credentials rotate on a defined cadence, with the rotation enforced by policy rather than by intent. DONT_EXPIRE_PASSWORD on a privileged account directly defeats that requirement.

Attack path

1. Discovery: from any authenticated context the attacker runs a single LDAP filter that returns every privileged non-expiring account: Get-ADUser -LDAPFilter "(&(adminCount=1)(userAccountControl:1.2.840.113556.1.4.803:=65536))" -Properties pwdLastSet,servicePrincipalName,memberOf. PowerView Get-DomainUser -AdminCount -SPN -PreauthNotRequired and BloodHound (HasSession, AdminTo, GenericAll edges combined with the "Password Never Expires" node property) expose the same set. 2. Target ranking: the attacker sorts by pwdLastSet ascending. Accounts whose password has been static for years are highest priority because they predate modern complexity requirements and are most likely to crack against a wordlist. The attacker also flags any returned account with a servicePrincipalName (Kerberoasting candidate) and any with DONT_REQUIRE_PREAUTH (AS-REP candidate). 3. Kerberoasting, if SPN present: Rubeus kerberoast /user:<DA> /nowrap /format:hashcat, or Impacket GetUserSPNs.py -request -dc-ip <dc> <domain>/<user>. The resulting krb5tgs hash is loaded into hashcat -m 13100 with rockyou + best64. Because the password never rotates, even a slow GPU run completes successfully. 4. AS-REP roasting, if DONT_REQUIRE_PREAUTH also set: Rubeus asreproast /user:<DA> /nowrap /format:hashcat, or Impacket GetNPUsers.py. Hashcat -m 18200. Same indefinite shelf life. 5. Offline NTDS crack: when the attacker reaches a DC via any path (DCSync, vssadmin, ntdsutil ifm, backup restore), they extract NT hashes with Impacket secretsdump.py or Mimikatz lsadump::dcsync /all. Hashes from DONT_EXPIRE_PASSWORD accounts are flagged for indefinite reuse: the attacker burns them later, post-incident-response, knowing the password has not changed. 6. Persistence: even after the victim resets krbtgt twice and rotates the headline compromised accounts, the attacker re-authenticates as the non-expiring privileged account weeks or months later, often through a VPN, RDS, or M365 sign-in surface that re-validates against the same unchanged hash. Microsoft Incident Response has published case studies where attacker re-entry six months post-eviction was traced to a single privileged service account with DONT_EXPIRE_PASSWORD whose hash the attacker had retained.

How Guerrilla assesses it

Guerrilla binds to a writable Domain Controller and issues the LDAP filter (&(objectCategory=person)(objectClass=user)(adminCount=1)(userAccountControl:1.2.840.113556.1.4.803:=65536)) against the default naming context with -Properties sAMAccountName, distinguishedName, objectSid, userAccountControl, adminCount, pwdLastSet, lastLogonTimestamp, servicePrincipalName, memberOf, msDS-ResultantPSO. The bitwise-AND matching rule 1.2.840.113556.1.4.803 isolates the DONT_EXPIRE_PASSWORD bit (0x10000 / 65536) without enumerating every UAC permutation. Membership in Domain Admins, Enterprise Admins, Schema Admins, Account Operators, Backup Operators, Server Operators, Print Operators, DnsAdmins, and Protected Users is resolved by a second pass that recursively expands each group and intersects the result with the candidate set. msDS-ResultantPSO is read on every returned principal to determine whether a Fine-Grained Password Policy actually applies; an account with DONT_EXPIRE_PASSWORD has its policy maximum age silently ignored regardless of the FGPP value, so Guerrilla also reports this as a coverage gap. SDProp orphans (adminCount=1 with no current protected-group membership) are surfaced separately so they can be remediated and the flag cleared. The check is read-only and requires only authenticated user rights to the domain object. No event is written by an LDAP read, so the check produces no 4738 or 4670 noise.

Recommended value

No privileged accounts with Password Never Expires flag set. All privileged accounts subject to password rotation policy of 60 days or less

Remediation

Identify privileged accounts with PasswordNeverExpires using Get-ADUser -Filter {PasswordNeverExpires -eq $true -and AdminCount -eq 1}. Clear the flag and ensure these accounts are covered by an appropriate password policy. Implement FGPP for privileged accounts with a 60-day maximum password age

Fixture-proven verdicts

Every verdict below is proven by a golden fixture in the module's gating test suite. This table derives from the last green run; it cannot be edited by hand.

Verdict scenarios for ADPRIV-010
ScenarioExpected verdict
cleanPASS
known-badFAIL
throttledNot Assessed

Framework mappings

NIST SP 800-53
IA-5(1), AC-2
CIS Benchmark
1.1.4
ANSSI
R36
CIS AD Benchmark
4.3.1
MITRE ATT&CK
T1078.002