ADSTALE-008: Orphaned SID History

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 performs an LDAP search across the entire domain (and optionally the forest) for security principals where sIDHistory is populated: (&(objectClass=securityPrincipal)(sIDHistory=*)) It then reads sIDHistory back as a list of binary SID values for each principal (user, group, computer, gMSA, sMSA, inetOrgPerson) and decomposes each entry: - Identifier authority and revision (S-1-5-... NT Authority is the only legitimate value for migrated principals). - Domain identifier (the sub-authorities before the RID, e.g. 21-1004336348-1177238915-682003330). - RID (the trailing sub-authority). For each parsed SID the check cross-references the domain identifier against: 1. The local domain SID (a sIDHistory entry pointing at the current domain is same-domain SID History, the classic Sean Metcalf "Sneaky Persistence #14" persistence pattern and itself a finding). 2. The trustedDomain objects under CN=System,DC=... in every domain of the forest (msDS-TrustForestTrustInfo, securityIdentifier, trustPartner, trustDirection, trustAttributes, trustType). 3. The Partitions container (CN=Partitions,CN=Configuration,...) for any in-forest domain naming context. 4. The forest trust SID list (msDS-TrustForestTrustInfo blob) for any inbound or outbound forest trust that whitelists the SID. For each entry the check classifies the SID into one of five buckets: - LIVE: domain identifier matches an active trusted domain and the RID resolves to an extant principal. No finding. - ORPHAN_DOMAIN: domain identifier has no matching trust or in-forest domain. The source domain has been decommissioned or the trust has been deleted. - ORPHAN_RID: domain identifier is live but the RID does not resolve to any existing principal in the source domain (the source account was deleted post-migration). - SAME_DOMAIN: domain identifier equals the current domain SID. This is never produced by legitimate ADMT migration and is a strong indicator of malicious DSInternals or Mimikatz SID injection. - WELL_KNOWN_PRIVILEGED: RID belongs to a high-value group (512 Domain Admins, 519 Enterprise Admins, 518 Schema Admins, 544 Administrators, 500 built-in Administrator) regardless of which domain the SID points at. Always emitted as a separate severity-elevated finding. The check also reads the LSA Authentication policy on the PDC emulator to determine whether SID filtering (Quarantine) is enabled on each external and forest trust (Netdom trust /quarantine:yes, trustAttributes bit 0x4 TRUST_ATTRIBUTE_QUARANTINED_DOMAIN, bit 0x40 TRUST_ATTRIBUTE_TREAT_AS_EXTERNAL) so that the finding context can report whether an orphan SID would currently survive cross-trust authentication.

Por qué importa

SID History is one of the few AD constructs where the access token is derived from data the attacker can write rather than data the OS computes. Every SID in sIDHistory is unconditionally added to the user access token at TGT issuance and is honoured by every Windows access check unless SID filtering removes it at a trust boundary. This produces three distinct risks: 1. Hidden privilege. A standard user account whose sIDHistory contains the SID of the source-domain Domain Admins group (or, far worse, the SID of Enterprise Admins from the forest root) becomes effectively Domain Admin without holding any visible group membership. Standard privilege audits (Get-ADGroupMember "Domain Admins", BloodHound default queries against memberOf) miss this entirely because the attacker is not a group member, they are carrying the group SID in their token. Detection requires explicit sIDHistory enumeration, which is exactly what ADSTALE-008 performs. 2. Persistence after eviction. SID History injection (T1134.005) is the canonical AD persistence technique that survives password resets, krbtgt rotation, and account disable. An attacker who obtains DRSUAPI write access for ten seconds (Mimikatz misc::addsid, DSInternals Add-ADDBSidHistory, or direct LDAP write where Self-Membership ACLs allow it) can stamp a privileged SID onto any throwaway account and then evict their original foothold. The throwaway account looks unremarkable in every standard audit yet retains Domain Admin authority indefinitely. 3. Cross-trust escalation. SID filtering on inbound trusts (Quarantine) strips foreign SIDs from inbound tokens, but the filter exempts the local domain SID and well-known SIDs. An attacker who controls a trusted external domain can stamp the local-forest Enterprise Admins SID onto a foreign account and then ride the trust into the target forest. CVE-2022-26923 (certifried) and the broader research on cross-forest SID injection by SpecterOps, Dirk-jan Mollema, and Charlie Clark demonstrate that trust topology assumptions break down quickly when sIDHistory is uncontrolled. Orphaned entries are the easiest of these to triage and the easiest to ignore. They are easiest to triage because they provide no business value: by definition the SID points at nothing, so removing it cannot break any access. They are easiest to ignore because they appear inert: a SID for a domain that no longer exists cannot match an ACL on any resource the local forest holds. The risk is not the orphan itself, the risk is that environments which carry orphans demonstrably are not auditing sIDHistory at all. Every breach investigation that surfaces a malicious SID injection starts with an environment whose orphan count was nonzero and unmonitored, because the operational hygiene to clean up legitimate orphans is the same hygiene that detects malicious additions.

Ruta de ataque

1. Initial Domain Admin access. The attacker reaches Tier 0 through any of the standard paths (Kerberoasting a service account in Domain Admins, ADCS ESC1/ESC8, unconstrained delegation on a DC, NTLM relay to LDAPS, DCSync via Replicating Directory Changes). 2. Pick the carrier account. The attacker chooses a low-privilege, low-attention user account: a disabled migration account, a service account in a forgotten OU, a contractor account flagged for offboarding. These accounts are not in any privileged group and are not in the BloodHound shortest-path-to-Domain-Admins output. 3. Stamp the privileged SID. With Domain Admin, the attacker uses Mimikatz (misc::addsid /sam:targetUser /new:S-1-5-21-<domain>-512) which calls DRSUAPI IDL_DRSAddSidHistory and writes directly to the carrier account sIDHistory. Alternatively the attacker uses DSInternals Add-ADDBSidHistory offline against a stolen NTDS.dit, or invokes the documented ADMT API path for the same effect. The write produces no Event ID 4738 because the attribute change goes via DRSUAPI not via SAM. 4. Plant orphan cover. The attacker also writes one or two genuinely orphaned SIDs to the same account so that the malicious entry looks like routine migration cruft. An environment with thousands of orphan entries from a 2008 ADMT migration provides perfect camouflage for one new entry. 5. Use the privilege. The carrier account signs in. The KDC reads sIDHistory, builds the PAC with all SIDs present, and the user token contains S-1-5-21-<domain>-512 (Domain Admins). The account can now DCSync, modify the AdminSDHolder, dump LSASS on any DC, etc., despite holding no visible privileged group membership. 6. Survive eviction. The IR team identifies the attacker original foothold, rotates krbtgt twice, resets every Domain Admin password, and declares the incident closed. The carrier account, never identified as privileged, retains its SID-injected Domain Admin authority. The next time the attacker needs access they sign in as the carrier and repeat from step 5. 7. Cross-forest variant. In a multi-forest scenario the attacker compromises a trusted external forest (often the smaller "lab" or "dev" forest that shares a trust with prod), stamps the prod-forest Enterprise Admins SID onto a foreign account, and authenticates inbound. Unless the prod forest has SID filtering (Quarantine, trustAttributes 0x4) enabled on the trust, the injected SID survives the inbound PAC validation and the attacker holds Enterprise Admin in the prod forest. Tools observed in real engagements: Mimikatz misc::addsid, DSInternals Add-ADDBSidHistory, BloodHound CollectionMethod=ACL plus manual sIDHistory enumeration, Impacket secretsdump.py (read path) followed by addsid-equivalent write tooling, native Move-ADObject and Repadmin /writespn primitives where Self-Membership ACLs allow direct LDAP write to sIDHistory on objects the attacker controls.

Cómo lo evalúa Guerrilla

Guerrilla connects to the domain via LDAP (Get-ADObject -LDAPFilter "(sIDHistory=*)" -Properties sIDHistory, objectClass, sAMAccountName, distinguishedName, whenChanged) and pulls every security principal carrying a sIDHistory value across the entire defaultNamingContext. Each binary SID is decoded into its (Authority, Domain, RID) tuple. In parallel, the check enumerates Get-ADTrust -Filter * and reads the configuration NC Partitions container to build an authoritative list of (DomainSid, DomainName, TrustType, TrustDirection, TrustAttributes, IsForestTrust) tuples. For forest trusts the msDS-TrustForestTrustInfo blob is parsed to extract the TDO_FOREST_TRUST_DOMAIN_INFO records that whitelist additional SIDs. Each sIDHistory entry is then matched against this trust map: - Match on DomainSid AND RID resolves: LIVE. Logged for inventory only. - Match on DomainSid AND RID unresolvable: ORPHAN_RID. - No match on DomainSid AND DomainSid != local: ORPHAN_DOMAIN. - DomainSid == local domain SID: SAME_DOMAIN (highest scrutiny: never produced by legitimate ADMT). - RID in {500, 512, 518, 519, 544, 548, 549, 550, 551, 552, 9 (Enterprise Domain Controllers), 526, 527} regardless of domain match: WELL_KNOWN_PRIVILEGED, severity elevated. The check additionally cross-references whenChanged on the carrying object against the trust creation/deletion dates and the known migration project windows the operator configures (so a sIDHistory entry written ten years ago is treated differently than one written yesterday). The whenChanged correlation is the single most useful signal for separating dormant 2008-era migration debris from active malicious injection. Secondary detection: Guerrilla also reads the Directory Service Access audit policy (auditpol /get /subcategory:"Directory Service Changes") and the schema ACL on the sIDHistory attribute to confirm that Event ID 4662 with property GUID for sIDHistory writes will fire on the DC security log, and that Event 4765 (SID History added) and 4766 (SID History add failed) are being collected centrally. A check finding includes a note when audit coverage is missing.

Valor recomendado

No SID History entries referencing non-existent domains. SID History cleaned after migration completion

Remediación

Query all user and group accounts with SID History using Get-ADUser -Filter {SIDHistory -like '*'} -Properties SIDHistory. Cross-reference each SID History domain component against existing trusts to identify orphaned entries. Remove SID History entries for completed migrations using Remove-ADUser with the -Remove parameter or Netdom trust /CleanupSIDHistory. Monitor for new SID History additions using Event ID 4765

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.

Escenarios de veredicto de ADSTALE-008
EscenarioVeredicto esperado
cleanPASS
known-badWARN
throttledNot Assessed

Mapeos a marcos de referencia

NIST SP 800-53
AC-2
CIS AD Benchmark
9.4.2
MITRE ATT&CK
T1134.005