ADSTALE-001: Inactive User Accounts
- Plataforma
- Active Directory
- Categoría
- AD Stale & Obsolete Objects
- Severidad
- Medium
- Pilar de Zero Trust
- Governance (peso 1)
- Fixtures de referencia
- 3
- Cobertura de ramas
- Observada: los fixtures prueban los veredictos que ejercitan
- Procedencia
- baseline
Qué comprueba
The check binds to the domain and enumerates all user objects (objectClass=user, objectCategory=person) where userAccountControl indicates the account is enabled (the 0x2 ACCOUNTDISABLE bit is clear). For each enabled user it reads lastLogonTimestamp, which is the replicated, low-precision (9 to 14 day jitter) authentication timestamp written by every Domain Controller and replicated forest-wide, as opposed to lastLogon, which is per-DC and not replicated. The check converts the Windows FILETIME value to a DateTime, computes the delta against the current time, and flags any account whose lastLogonTimestamp is older than the configured threshold (default 90 days). Accounts that have never authenticated (lastLogonTimestamp = 0 or null) are flagged separately because they typically represent staged accounts that were created and abandoned. The check also reads whenCreated to distinguish brand-new accounts (created in the last 30 days, not yet used) from genuinely stale accounts, and pwdLastSet so the report can surface the related risk of stale accounts whose passwords have not rotated. Service accounts identified by servicePrincipalName population are reported in a separate bucket because they may legitimately never interactively log on. The audit logic mirrors Search-ADAccount -AccountInactive -TimeSpan 90 -UsersOnly, but adds the never-logged-on detection, the SPN partition, and the privileged-group cross-reference.
Por qué importa
Every enabled account is a credential the directory will accept. An inactive account amplifies risk along four axes. First, monitoring blindness: the owner is gone (terminated, on long leave, role change, contractor offboarded) and is not watching their inbox for sign-in alerts, MFA prompts, or password-change emails, which removes the human tripwire that catches opportunistic compromise. Second, residual privilege: accounts created for a project, a migration, or a temporary delegation routinely retain the group memberships they were granted, so a stale Helpdesk or Backup Operators member is still functionally an administrator on the day an attacker phishes or sprays the credential. Third, weak credentials: inactive accounts disproportionately carry stale passwords set under whatever the policy was when the account was created (often a default like Welcome1, Spring2023!, or the company name plus a digit), and the owner is not rotating them. Fourth, audit signal-to-noise: failed authentication noise against active accounts gets investigated; failed authentication against a stale account is often filtered out as the dormant account it always was. The combined effect is that an enabled stale account is the cheapest, lowest-noise foothold in the directory, and attackers know to enumerate them first. CISA, Microsoft, and Mandiant have all published advisories tracing initial access in ransomware intrusions to dormant or forgotten accounts that had retained valid credentials and group memberships.
Ruta de ataque
Step 1: Enumeration. An attacker with any authenticated foothold (intern credential, harvested via phishing, captured from a public code repo) runs an LDAP query for enabled users with stale lastLogonTimestamp. PowerView Get-DomainUser -LDAPFilter "(&(samAccountType=805306368)(!userAccountControl:1.2.840.113556.1.4.803:=2))" and BloodHound collection both expose the data without elevated rights. The attacker also pulls servicePrincipalName, memberOf, and pwdLastSet in the same query to score targets. Step 2: Target selection. The attacker filters for accounts that are (a) enabled, (b) lastLogonTimestamp older than 90 days, (c) in a useful security group (Domain Users with file-share or VPN access, Helpdesk, Backup Operators, a legacy admin group), and (d) have a pwdLastSet older than the current password policy maximum, indicating the password has likely never been forced to rotate. A typical mid-size enterprise yields 50 to 500 such accounts. Step 3: Credential acquisition. The attacker chooses the cheapest exploitation primitive for each target. Password spraying (Rubeus, kerbrute, MSOLSpray): a single seasonal password against the entire stale-account pool, no lockout risk because the account is forgotten. AS-REP roasting (Rubeus asreproast, Impacket GetNPUsers.py): if the stale account has DONT_REQ_PREAUTH set (userAccountControl bit 0x400000), the attacker requests an AS-REP and cracks it offline. Kerberoasting (Rubeus kerberoast, Impacket GetUserSPNs.py): if the stale account is a service account with a Service Principal Name, the attacker requests a TGS, captures the RC4-HMAC-encrypted ticket, and cracks it offline. Step 4: Use the credential. Once cracked, the stale credential is used against VPN, RDP gateways, OWA, Autodiscover, or Azure AD / Entra ID sign-in. Because the account is enabled and the owner is absent, there is no MFA prompt visible to a human and no Conditional Access user-risk signal from anomalous travel. Step 5: Persistence and lateral movement. The attacker harvests further credentials, runs BloodHound to plot a path from the stale-account foothold to Tier 0, and pivots. The compromised stale account becomes the persistent backdoor: it survives password rotation cycles tied to active users, it survives helpdesk credential resets, and it survives EDR alerts that are scoped to known active personas. ADSTALE-001 closes this primitive at the source by ensuring no enabled account remains usable past its operational lifetime.
Cómo lo evalúa Guerrilla
Guerrilla calls Get-ADUser -Filter * with a properties list of (lastLogonTimestamp, lastLogonDate, whenCreated, pwdLastSet, userAccountControl, servicePrincipalName, memberOf, Enabled), filters in-process to those where Enabled is $true, converts lastLogonTimestamp from Windows FILETIME to DateTime, and computes (Get-Date) minus that DateTime. Any delta greater than the configured threshold (default 90 days) raises the account. Accounts with lastLogonTimestamp = 0 are reported as "never authenticated". The check also resolves memberOf transitively against the privileged-group list (Domain Admins, Enterprise Admins, Schema Admins, Account Operators, Backup Operators, Print Operators, Server Operators, Cert Publishers, DNSAdmins, Group Policy Creator Owners, plus any organization-specific Tier 0 groups) and elevates any stale account that lands in those groups to High severity in the report. Accounts with a populated servicePrincipalName are partitioned into a separate "stale service account" report bucket so they can be reviewed against the gMSA migration backlog. The check is read-only, requires only authenticated user rights to the domain partition, and emits no Event 4738 / 4740 / 4767 noise because it does not modify any object. The 9 to 14 day replication jitter on lastLogonTimestamp is documented and is the reason the threshold is set to 90 days rather than something tighter: the underlying attribute is intentionally lossy to reduce inter-DC replication traffic.
Valor recomendado
No enabled user accounts inactive for more than 90 days; inactive accounts disabled or removed
Remediación
Query user accounts where lastLogonTimestamp is older than 90 days and the account is enabled using Search-ADAccount -AccountInactive -TimeSpan 90 -UsersOnly. Verify with account owners or managers before taking action. Disable inactive accounts first, then delete after a 30-day grace period if unclaimed. Remove disabled accounts from all security groups. Implement automated lifecycle management
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 | FAIL |
| throttled | Not Assessed |
Mapeos a marcos de referencia
- NIST SP 800-53
- AC-2(3)
- CIS AD Benchmark
- 9.1.1
- MITRE ATT&CK
- T1078.002