Skip to content

AD-INV-001: Group membership differs from the declared state

high   Invariants   Reads: LDAP

The membership of a group declared in the invariants file differs from the declared list: someone was added to it, or a declared member is gone.

Why it matters

Adding an account to Domain Admins or another privileged group is the most common way to keep access after a compromise, and the most common quiet mistake ("temporarily" made admin for a migration). Generic checks report large or non-empty groups, but only the team knows that tier 0 should be exactly these people. Declaring it turns every unreviewed change into a finding on the next scan, and in CI into a failed job.

How ad-doctor detects it

Runs only with --invariants FILE, for each group under [groups]:

[groups]
"Domain Admins" = ["Administrator", "adm_alice"]
"Schema Admins" = []
  • The group is found by name, ignoring case. For the privileged groups ad-doctor already collects (Domain, Enterprise and Schema Admins, Administrators and the four operator groups) it uses their transitive membership from that collection, which also includes accounts made members through primaryGroupID. For any other group it expands nested groups from the snapshot (cycles are cut).
  • Members are compared by sAMAccountName, ignoring case. Only user and computer accounts count; nested groups are expanded, not compared.
  • Each difference is one object: Domain Admins: unexpected svc_sql or Domain Admins: missing adm_alice.

A declared group that does not exist in the snapshot reports every declared member as missing.

Attributes and probes read
  • member
  • sAMAccountName

Severity

Always high. The groups people declare are almost always tier 0, and an unexpected member of one is either a mistake or an attacker's persistence.

False positives and limits

  • Members from another domain or forest (foreign security principals) are not resolved to accounts and are left out of the comparison.
  • Membership through primaryGroupID is counted only for the privileged groups above; for other declared groups it is missed. See AD-PRIV-007.

Fix

  1. Find out who made the change and when before reverting it. On a DC, group changes are logged as events 4728/4732/4756 (member added) when auditing of security group management is on:

    Get-ADReplicationAttributeMetadata 'CN=Domain Admins,CN=Users,DC=corp,DC=example' `
        -Server dc01 -ShowAllLinkedValues | Where-Object AttributeName -eq member |
        Sort-Object LastOriginatingChangeTime -Descending |
        Select-Object -First 10 AttributeValue, LastOriginatingChangeTime, IsLinkValue
    
  2. If the change was not intended, revert it and treat the account as compromised until you know otherwise:

    Remove-ADGroupMember 'Domain Admins' -Members svc_sql -WhatIf
    
  3. If it was intended, change the invariants file in a reviewed pull request, so the decision is recorded.

  4. Run the check in CI so the next drift fails the build; see Invariants and CI integration.

Verify

ad-doctor scan dc01.corp.example --ldaps -u 'CORP\auditor' --invariants invariants.toml --only AD-INV-001

To accept the risk instead, add an entry to the file passed with --accept (see accepted risks):

[[accept]]
check = "AD-INV-001"
objects = ["..."]        # optional; without it the whole check is accepted
reason = "..."
expires = 2027-03-31

References

Microsoft and vendors

Available since 0.1.0.