ADPWD-015: Password Last Set Age Distribution
- Plataforma
- Active Directory
- Categoría
- AD Password & Lockout Policies
- Severidad
- Medium
- Pilar de Zero Trust
- Identity (peso 3)
- Fixtures de referencia
- 3
- Cobertura de ramas
- Observada: los fixtures prueban los veredictos que ejercitan
- Procedencia
- baseline
Qué comprueba
The check binds to the default naming context, runs an LDAP query of (&(objectCategory=person)(objectClass=user)(!(userAccountControl:1.2.840.113556.1.4.803:=2))) to scope to enabled user accounts, and retrieves pwdLastSet, userAccountControl, sAMAccountName, distinguishedName, lastLogonTimestamp, adminCount, and servicePrincipalName for every match. pwdLastSet is a Windows FileTime (100-nanosecond intervals since 1601-01-01 UTC); the check converts it to a DateTime, subtracts from Get-Date, and bins the result. The DONT_EXPIRE_PASSWD bit (0x10000) in userAccountControl is tested separately and these accounts are flagged regardless of age, since they will never rotate under any policy. Accounts with pwdLastSet equal to 0 are flagged as "force change at next logon" candidates that may have stalled and now permit interactive logon with a stale or blank credential under certain conditions. The resulting distribution is presented as histogram buckets (0-30, 31-60, 61-90, 91-180, 181-365, 366-730, 731-1825, 1826+ days) and is cross-joined against the effective maximum password age. Effective MaxPasswordAge is computed by reading the maxPwdAge attribute on the domain naming context and overlaying any msDS-PasswordSettings object (PSO) whose msDS-PSOAppliesTo links to the account or to a group containing it; the per-account override wins over the domain default. Privileged accounts (adminCount=1, members of Domain Admins, Enterprise Admins, Schema Admins, Administrators, the protected groups under AdminSDHolder) and accounts holding a servicePrincipalName are tagged in the output so the operator can triage the highest-value findings first.
Por qué importa
Old passwords are a force multiplier for every credential-attack technique in the ATT&CK matrix. NTLM hashes do not change with time on disk: an NTDS.dit extract from three years ago still works today if the password has not been rotated, which is why dumps from prior incidents continue to surface in fresh intrusions as long as the underlying hashes remain valid. Kerberoasting yields RC4 or AES-256 hashes that are cracked offline at GPU speed, and a 14-character password set in 2018 has been exposed to credential-stuffing wordlists, prior breach corpora, and adversary-controlled rainbow tables for the entire window since it was set. Accounts with DONT_EXPIRE_PASSWD set bypass every Group Policy and PSO control, and in field engagements they are concentrated in three populations: legacy service accounts created when password rotation was operationally painful, break-glass accounts whose owners feared lockout, and individual user accounts where a help-desk technician flipped the bit to suppress a ticket. The blast radius of a single old privileged password is the entire forest. NIST SP 800-63B no longer recommends arbitrary periodic rotation for human users, but it explicitly requires rotation on indicators of compromise and applies different guidance to shared, service, and privileged identities, which is precisely the population this check surfaces. CIS Microsoft Active Directory Benchmark 5.6.x and PCI DSS 8.3 still require demonstrable rotation for service and privileged accounts.
Ruta de ataque
Step 1: Reconnaissance. The attacker, holding any authenticated foothold, runs an LDAP query for (&(objectCategory=person)(objectClass=user)(!(userAccountControl:1.2.840.113556.1.4.803:=2))) and pulls pwdLastSet and userAccountControl. PowerView Get-DomainUser -PreauthNotRequired -Properties samaccountname,pwdlastset,useraccountcontrol or a raw ldapsearch works equally well. No privileges beyond Authenticated Users are required. Step 2: Triage. The attacker filters for accounts where (pwdLastSet age > 365 days) OR (userAccountControl AND 0x10000 != 0), then intersects with adminCount=1 and with accounts holding a servicePrincipalName. The output is a small, prioritized target list of stale privileged identities and stale kerberoastable service accounts. Step 3: Offline cracking against kerberoastable targets. For each SPN-bearing target the attacker requests a service ticket: Rubeus kerberoast /user:svc_target /nowrap or GetUserSPNs.py -request. The ticket carries an RC4-HMAC TGS encrypted with the target service account NTLM hash (or AES-256 if the account is configured for AES). The attacker runs hashcat -m 13100 against a wordlist enriched with breach corpora from the years the password was first set. A 10-character password from 2017 typically falls in hours; a password reused from any prior breach falls instantly. Step 4: Password spraying against human accounts. For non-SPN targets with DONT_EXPIRE_PASSWD set, the attacker sprays one or two candidate passwords per account per lockout window (T1110.003). Stale passwords are disproportionately likely to be seasonal patterns, vendor defaults, or values from public breach corpora because the policy at the time of set was weaker. Step 5: Reuse. The cracked credential is used directly (RDP, WinRM, SMB) or to forge a TGT via S4U2Self for protocol transition. Service accounts often have local admin rights on the servers they run on, so a single cracked stale service account commonly yields lateral movement to dozens of hosts without ever touching a domain controller. Step 6: Persistence. The attacker leaves the password unchanged. As long as DONT_EXPIRE_PASSWD remains set or rotation is not enforced, the credential remains valid indefinitely and survives password resets on other accounts, IR cycles, and credential audits that do not specifically scope this population.
Cómo lo evalúa Guerrilla
Guerrilla binds to the default naming context using the current security principal and issues a paged LDAP search with filter (&(objectCategory=person)(objectClass=user)(!(userAccountControl:1.2.840.113556.1.4.803:=2))) and attribute list pwdLastSet, userAccountControl, sAMAccountName, distinguishedName, lastLogonTimestamp, adminCount, servicePrincipalName, memberOf. The pwdLastSet FileTime is converted via [DateTime]::FromFileTime() and the elapsed days are computed against Get-Date. The DONT_EXPIRE_PASSWD bit is tested with (userAccountControl -band 0x10000). The effective maximum password age is computed by reading maxPwdAge on the domain object (a negative FileTime, converted to a positive day count) and overlaying every msDS-PasswordSettings PSO via the resultant-set algorithm: per-user assignment wins over per-group assignment, and within each tier the PSO with the lowest msDS-PasswordSettingsPrecedence wins. Accounts whose effective age exceeds the effective max age are flagged. The output is structured as histogram buckets (0-30, 31-60, 61-90, 91-180, 181-365, 366-730, 731-1825, 1826+ days) plus a flat list of outliers, with privileged accounts and SPN-bearing accounts tagged. No writes are performed.
Valor recomendado
No enabled user accounts with passwords older than the maximum password age policy. Distribution should show regular rotation patterns
Remediación
Generate a password age distribution report using Get-ADUser -Filter {Enabled -eq $true} -Properties PasswordLastSet | Group-Object {(New-TimeSpan $_.PasswordLastSet (Get-Date)).Days -replace '\d$','0'}. Investigate accounts with passwords older than the policy allows. Force password changes where needed and review Password Never Expires flags
Veredictos probados con fixtures
Cada veredicto de esta tabla está probado por un fixture de referencia en la suite de pruebas que valida el módulo. La tabla se deriva de la última ejecución en verde; no puede editarse a mano.
| Escenario | Veredicto esperado |
|---|---|
| clean | PASS |
| known-bad | WARN |
| throttled | Not Assessed |
Mapeos a marcos de referencia
- NIST SP 800-53
- IA-5(1)
- CIS AD Benchmark
- 5.6.1
- MITRE ATT&CK
- T1078.002