ADPWD-008: Maximum Password Age
- Platform
- Active Directory
- Category
- AD Password & Lockout Policies
- Severity
- Medium
- Zero Trust pillar
- Identity (weight 3)
- Golden fixtures
- 2
- Branch coverage
- Observed: fixtures prove the verdicts they exercise
- Provenance
- baseline
What it checks
For the domain-wide policy, the check binds to the defaultNamingContext and reads maxPwdAge. AD stores this as a LargeInteger in negative 100-nanosecond intervals: a value of 0 or the sentinel 0x8000000000000000 (-9223372036854775808) means passwords never expire, and any other negative value is converted to days via [TimeSpan]::FromTicks(-maxPwdAge).TotalDays. The check also enumerates every PSO under CN=Password Settings Container,CN=System,DC=domain,DC=tld via LDAP filter (objectClass=msDS-PasswordSettings) and reads: - msDS-MaximumPasswordAge - msDS-PasswordSettingsPrecedence - msDS-PSOAppliesTo (which users or groups receive the policy) Findings are raised when the domain maxPwdAge is 0 (passwords never expire), when it exceeds the configured ceiling for general accounts, or when a PSO that applies to a privileged group (Domain Admins, Enterprise Admins, Tier-0 service accounts) has a maximum age longer than the stricter privileged threshold. The check also cross-references userAccountControl on principals scoped by each PSO to detect accounts that carry the DONT_EXPIRE_PASSWORD flag (0x10000) and therefore bypass the policy entirely.
Why it matters
Maximum password age is one of two opposing risk levers. Too long, and a credential captured in a phishing campaign, an NTDS.dit theft, a Kerberoasting harvest, or an old breach corpus stays valid for years, giving an adversary a stable foothold for lateral movement and persistence (MITRE T1078.002, Valid Accounts: Domain Accounts). Too short, and users adopt predictable rotation patterns (Spring2026!, Spring2026!!) that defeat the policy and erode password entropy, a behavior NIST SP 800-63B explicitly warns against. The correct answer is policy that matches the threat model: a long but bounded rotation for the general population paired with a much shorter cycle for privileged and service principals via Fine Grained Password Policies, plus modern controls (MFA, Protected Users, Authentication Policy Silos, gMSA for services) that reduce reliance on the password itself. A domain where maxPwdAge is 0 or where privileged accounts inherit the same long rotation as general users is functionally relying on a single static secret to gate Tier-0 access.
Attack path
1. Initial collection: the attacker harvests credential material from any of the standard sources, Kerberoasting service principals (Rubeus kerberoast), AS-REP roasting accounts with preauth disabled, LSASS dumps from a foothold, NTDS.dit exfiltration via DCSync, or third-party breach corpora. 2. Offline cracking: hashes are run through hashcat against curated wordlists. Long max password ages mean a hash captured 18 or 24 months ago is still likely valid, so cracking ROI scales directly with the rotation interval. 3. Persistence on stale credentials: if maxPwdAge is 0 or set to a multi-year value, a recovered password from an old breach (the attacker correlates email addresses across HaveIBeenPwned-style corpora) authenticates today. Combined with no MFA on legacy protocols (LDAP simple bind, SMB, RDP via NLA), this is a one-step domain entry. 4. Service account compromise: service accounts frequently carry DONT_EXPIRE_PASSWORD or are scoped to a permissive PSO. The attacker prioritizes these because the credential rarely rotates and is often a member of a privileged group. 5. Skeleton credentials: once an attacker has Domain Admin, weakening or disabling maxPwdAge on a backdoor account (or planting a PSO with maxPwdAge = 0 applied to the backdoor) is a quiet persistence technique that survives broad password-reset incident response unless the policy itself is audited.
How Guerrilla assesses it
Guerrilla queries the domain object via Get-ADDomain or a raw LDAP search and reads maxPwdAge directly from the domain naming context. The value is normalized: 0 and the int64 minimum value are reported as Never Expires; any other negative value is converted to days. PSOs are enumerated with Get-ADFineGrainedPasswordPolicy or LDAP filter (objectClass=msDS-PasswordSettings) under CN=Password Settings Container,CN=System,<DomainDN>, and each PSO is expanded to list its msDS-PSOAppliesTo principals, msDS-PasswordSettingsPrecedence, and msDS-MaximumPasswordAge. The check then queries members of high-value groups (Domain Admins, Enterprise Admins, Schema Admins, Account Operators, Server Operators, Backup Operators, plus any group flagged by ADPRIV-001) and reports the effective max age that applies to each, including the DONT_EXPIRE_PASSWORD bit on userAccountControl which causes the account to ignore policy entirely. Results are emitted with policy name, scope, current value in days, and the gap against the recommended baseline.
Recommended value
Maximum password age between 90-365 days depending on compliance requirements. For privileged accounts, 60 days maximum
Remediation
Configure in Default Domain Policy: Computer Configuration > Policies > Windows Settings > Security Settings > Account Policies > Password Policy > 'Maximum password age' = 365 days (or per compliance requirement). Implement a stricter FGPP for privileged accounts with 60-day maximum
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.
| Scenario | Expected verdict |
|---|---|
| known-bad | FAIL |
| throttled | Not Assessed |
Framework mappings
- NIST SP 800-53
- IA-5(1)
- CIS Benchmark
- 1.1.2
- ANSSI
- R34
- CIS AD Benchmark
- 5.2.4
- MITRE ATT&CK
- T1078.002