ADGPO-002: Empty GPOs
- Platform
- Active Directory
- Category
- AD Group Policy
- Severity
- Low
- Zero Trust pillar
- Governance (weight 0)
- Golden fixtures
- 3
- Branch coverage
- Observed: fixtures prove the verdicts they exercise
- Provenance
- baseline
What it checks
For each GPO returned by Get-GPO -All, the check calls Get-GPOReport -ReportType Xml and parses the resulting GroupPolicyObject document. It evaluates two child nodes: Computer/ExtensionData and User/ExtensionData. A GPO is reported as empty when both nodes are missing, contain no Extension children, or contain only an empty Extension element with no policy items inside it. Versioning data is also captured: a GPO with Computer.VersionDirectory == 0 and User.VersionDirectory == 0 has never had a setting saved, while a GPO with non-zero version numbers but empty ExtensionData indicates settings that were configured and later removed. Both states are flagged. The check also records the GPO GUID, display name, creation time, modification time, GPO status (AllSettingsEnabled, ComputerSettingsDisabled, UserSettingsDisabled, AllSettingsDisabled), and the list of OUs, sites, and the domain root where the GPO is linked, because an empty GPO linked at the Domain Controllers OU is a different signal than an empty unlinked GPO sitting in CN=Policies.
Why it matters
Empty GPOs are primarily a hygiene problem, but they have two security relevance vectors. First, they slow down Group Policy processing: every linked GPO must be evaluated by the Group Policy engine on each refresh cycle, and empty GPOs still consume LDAP and SYSVOL round trips. On large estates with hundreds of linked but empty GPOs at the site or domain level, this measurably extends logon time and gpupdate duration, which masks real policy-processing failures in the event log. Second, they are a pre-staged surface for an attacker who gains GPO edit rights through a delegated permission, a misconfigured ACL, or a compromised Group Policy Creator Owners member. Modifying an existing empty GPO does not create a new gPLink, does not generate a "new GPO created" audit event, and inherits whatever links the GPO already has. An attacker can drop a Scheduled Task, Immediate Task, Registry preference, or a malicious startup script into an empty GPO linked at the Domain Controllers OU and have it execute on every DC at the next refresh. Empty GPOs are also a frequent residue of GPP password leakage (CVE-2014-1812): the Group Policy Preferences XML was deleted but the GPO shell was not, and the SYSVOL history may still contain cpassword fragments in backup or older policy versions.
Attack path
1. Discovery: the attacker, having compromised a user with delegated GPO edit rights or membership in Group Policy Creator Owners, enumerates GPOs with Get-GPO -All and identifies candidates that are (a) empty and (b) linked to a high-value scope. PowerView Get-DomainGPO and BloodHound (CollectionMethod GPOLocalGroup) surface the same data. 2. Target selection: the attacker prefers an empty GPO linked at the Domain Controllers OU, the Domain Root, or a Tier-0 server OU. Because the GPO already has the gPLink, no link change is needed and no SACL on the OU fires. 3. Weaponization: using GPMC, PowerShell (Set-GPRegistryValue, Set-GPPrefRegistryValue), or SharpGPOAbuse, the attacker writes a malicious payload into the GPO. Common payloads are an Immediate Scheduled Task (T1053.005) running as SYSTEM, a logon script that executes a beacon, or a Restricted Groups entry that adds the attacker to the local Administrators group on every target. 4. Replication: SYSVOL replicates the new gPCFileSysPath contents through DFSR to every DC, and the GPO version number increments. Because the GPO already existed, the change appears in audit logs as a GPO modification on an old object rather than a new GPO creation. 5. Execution: targeted machines pick up the policy at the next refresh cycle (90 minutes by default plus a 30-minute randomization, immediate on next reboot for startup scripts). On the Domain Controllers OU, this is code execution as SYSTEM on every DC. 6. Cleanup: the attacker reverts the GPO to empty after execution, leaving the GPO display name and link unchanged. To a defender skimming GPMC, nothing has changed.
How Guerrilla assesses it
Guerrilla loads the GroupPolicy module, calls Get-GPO -All -Domain $env:USERDNSDOMAIN, then for each GPO calls Get-GPOReport -Guid $gpo.Id -ReportType Xml. The XML is parsed with [xml] cast and the Computer.ExtensionData and User.ExtensionData nodes are inspected. A GPO is classified as empty when neither half contains an Extension node with at least one child policy element. The check pulls supplementary metadata from the GPO object itself (CreationTime, ModificationTime, GpoStatus, Computer.DSVersion, User.DSVersion, Computer.SysvolVersion, User.SysvolVersion) and resolves linked scopes via Get-ADOrganizationalUnit -LDAPFilter "(gPLink=*$($gpo.Id)*)" plus the domain root and Sites container. Output rows include DisplayName, Id, IsEmpty (bool), GpoStatus, IsLinked (bool), LinkedScopes (string array), CreationTime, ModificationTime, and a Notes field that calls out the high-risk combinations: empty plus linked to DC OU, empty plus linked to Domain Root, empty plus all settings disabled, and empty plus modified within the last 30 days.
Recommended value
No empty GPOs in the domain; all GPOs contain at least one configured setting
Remediation
Identify GPOs with no configured settings using Get-GPOReport in XML format and checking for empty ExtensionData elements. Verify that empty GPOs are not placeholders for future use. Delete truly empty GPOs after confirming they are not referenced by any automation or documentation.
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 |
|---|---|
| clean | PASS |
| known-bad | WARN |
| throttled | Not Assessed |
Framework mappings
- NIST SP 800-53
- CM-2, CM-7
- MITRE ATT&CK
- T1484.001