ADGPO-003: Unlinked GPOs
- Platform
- Active Directory
- Category
- AD Group Policy
- Severity
- Low
- Zero Trust pillar
- Governance (weight 1)
- Golden fixtures
- 3
- Branch coverage
- Observed: fixtures prove the verdicts they exercise
- Provenance
- baseline
What it checks
The check enumerates every groupPolicyContainer object under CN=Policies,CN=System,DC=<domain> and collects each policy GUID from the cn attribute (the {GUID} form). It then walks three scopes for the gPLink attribute: the domain root (DC=<domain>), every organizationalUnit object via an LDAPFilter of (objectClass=organizationalUnit), and every site under CN=Sites,CN=Configuration,DC=<forest>. Each gPLink value is a concatenated string of [LDAP://cn={GUID},cn=policies,cn=system,DC=...;Flag] entries; the check parses the string, extracts each referenced GUID, and builds the set of linked GUIDs. The difference between the full GPO set and the linked set is the unlinked set. For every unlinked GPO the check additionally reads gPCFileSysPath (the SYSVOL path), nTSecurityDescriptor (DACL on the AD object), and the on-disk ACL of the SYSVOL folder, then inspects \\<domain>\SYSVOL\<domain>\Policies\{GUID}\ for high-risk files: Groups.xml, Services.xml, ScheduledTasks.xml, DataSources.xml, Drives.xml, Printers.xml (all GPP files that may contain cpassword), plus Scripts.ini, psscripts.ini, and any .ps1, .bat, .vbs, .cmd in the Machine\Scripts and User\Scripts subtrees. Findings include the GPO display name, GUID, creation and modification timestamps from whenCreated and whenChanged, the writeable principals on the AD object, and a content classification (contains GPP, contains scripts, contains software installation, empty).
Why it matters
Unlinked GPOs are not a vulnerability on their own, which is why this check is rated Low. They become a vulnerability under three conditions. First, content that should have been retired is left in place: GPP cpassword values (CVE-2014-1812) remain readable by any authenticated user from SYSVOL regardless of whether the GPO is linked, because SYSVOL is world-readable to the Authenticated Users group by default. Second, a delegated principal with Edit settings or Edit, delete, modify security rights on the GPO can re-link it: if a help-desk group, an application service account, or a stale delegation holds gPLink write on a high-value OU (for example the Domain Controllers OU or a Tier 0 server OU), they can attach an unlinked GPO that contains an attacker-controlled startup script or scheduled task and obtain code execution as SYSTEM on every machine in that OU at the next gpupdate cycle. Third, unlinked GPOs frequently retain weaker DACLs than active ones because change-control processes do not cover them, making them easier to modify by lower-tier principals (T1484.001, Group Policy Modification). The blast radius scales with where the GPO is eventually linked: a script GPO linked at the domain root or at the Domain Controllers OU yields full domain compromise.
Attack path
Step 1: Discovery. The attacker, authenticated as any domain user, enumerates SYSVOL with a simple file listing of \\<domain>\SYSVOL\<domain>\Policies\ to obtain the GUID list of every GPO, then queries LDAP for groupPolicyContainer objects to retrieve display names and gPCFileSysPath. They compare against gPLink values on OUs (Get-ADOrganizationalUnit -Filter * -Properties gPLink) to identify unlinked candidates. PowerView surfaces the same data via Get-DomainGPO -Properties displayname,gpcfilesyspath and Get-DomainOU -Properties gplink. Step 2: Content harvest. For each unlinked GPO the attacker reads the SYSVOL contents. They run Get-GPPPassword (PowerSploit) or gpp-decrypt against Groups.xml, ScheduledTasks.xml, Services.xml, DataSources.xml, Drives.xml, and Printers.xml to extract cpassword values and decrypt them with the published AES key. Any plaintext credentials, service account passwords, or local administrator passwords recovered are added to the credential set. Step 3: ACL review. The attacker enumerates the nTSecurityDescriptor on each unlinked GPO via Get-DomainObjectAcl to find any non-default Allow ACE granting WriteProperty on gPCMachineExtensionNames, WriteProperty on gPCUserExtensionNames, or write access to the SYSVOL folder for the GPO. They concurrently enumerate writeable gPLink attributes on OUs using Find-InterestingDomainAcl or a BloodHound GPLink edge query. Step 4: Weaponize. The attacker uses SharpGPOAbuse (FSecureLABS) or pyGPOAbuse to inject an immediate scheduled task or a startup script into a GPO they can modify. Example: SharpGPOAbuse --AddComputerTask --TaskName "Update" --Author NT AUTHORITY\SYSTEM --Command "cmd.exe" --Arguments "/c net user attacker P@ss /add && net localgroup administrators attacker /add" --GPOName "Unlinked-Legacy-Policy". Step 5: Link. Using the writeable gPLink they identified in step 3, they link the now-weaponized GPO to the target OU: Set-ADOrganizationalUnit -Identity "OU=Tier0,DC=contoso,DC=com" -Add @{gPLink="[LDAP://cn={GUID},cn=policies,cn=system,DC=contoso,DC=com;0]"}. If no writeable gPLink is available, they use the GPO content (cpassword or script) for lateral movement via the credentials harvested in step 2. Step 6: Execute and clean up. They wait one Group Policy refresh interval (90 minutes default, or force with gpupdate /force on the foothold), confirm code execution, then either remove the link or leave a deniable persistence mechanism via a benign-looking scheduled task.
How Guerrilla assesses it
Guerrilla performs a four-stage enumeration. Stage 1, GPO inventory: it binds to CN=Policies,CN=System,<domain DN> with an LDAPFilter of (objectClass=groupPolicyContainer) and collects distinguishedName, cn (the {GUID}), displayName, gPCFileSysPath, whenCreated, whenChanged, and nTSecurityDescriptor for every GPO. Stage 2, link inventory: it reads gPLink on the domain root, on every (objectClass=organizationalUnit) via Get-ADOrganizationalUnit -Filter * -Properties gPLink, and on every (objectClass=site) under CN=Sites in the Configuration NC. Stage 3, parsing: each gPLink value is split on the "][" delimiter, the cn={GUID} fragment is extracted from each [LDAP://...] entry, and a HashSet of all linked GUIDs is built. Stage 4, diff and classify: the check computes (all GPO GUIDs) minus (all linked GUIDs), and for each unlinked GPO it reads the SYSVOL contents via the gPCFileSysPath UNC, scans for GPP XML files known to carry cpassword, enumerates Scripts.ini, psscripts.ini, and Machine\Scripts and User\Scripts directories, and reports the discovered payloads alongside the AD DACL. The check also flags GPOs where the SYSVOL folder is missing (orphaned AD object) or where the AD object is missing (orphaned SYSVOL folder), since both indicate a broken or partially deleted GPO and a hygiene problem.
Recommended value
No unlinked GPOs unless documented as templates or backups with appropriate access controls
Remediation
Identify unlinked GPOs by comparing all GPO GUIDs against gPLink attributes on all OUs, sites, and the domain root. Review each unlinked GPO to determine if it should be linked, archived, or deleted. Remove sensitive content from unlinked GPOs that are kept as templates.
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