Group Policy Objects (GPOs) is a subject I’ve wanted to write about for a long time and I’m happy to have finally started.

If you’re not too familiar with GPOs, I highly recommend you go and read A Red Teamer’s Guide to GPOs and OUs by Andy Robbins. He recaps how GPO enforcement works, how to use BloodHound to find GPO-control based attack paths, and explains a few ways to execute those attacks.

In terms of weaponisation, Will Schroeder made some ground when he published Abusing GPO Permissions and implemented New-GPOImmediateTask into PowerView. However, the function was later removed with the commment:

inconsistent and better done manually

The aim for this series of blog posts is to demonstrate how to enumerate these abuse opportunities; and exploit them for both privilege escalation and persistence purposes.

Enumeration

There are a couple of interesting permissions that we may want to look for. The ones that spring to mind are:

  • Who can create new GPOs in the domain.
  • Who can link GPOs to which OUs.
  • Who can modify existing GPOs (that may or may not be currently linked).

The reason I think of them this way, is because they’re permissions that are individually delegated. For example:

  • Delegating permissions to create GPOs doesn’t also grant the permission to link them to an OU.
  • A user may be able to modify an existing GPO, but it may be unlinked and they can’t link it themselves.
  • Or a user may not be able to modify a GPO but can link it to another OU.

And so the combination of what permissions you have will depend on how you implement this abuse.

Create GPOs

In the Group Policy Management Console (GPMC), delegated permission to create GPOs in the domain looks like this:

They can be easy to enumerate PowerView with the following:

PS > Get-DomainObjectAcl -SearchBase "CN=Policies,CN=System,DC=testlab,DC=local" -ResolveGUIDs | Where-Object { $_.ObjectAceType -eq "Group-Policy-Container" }

AceQualifier           : AccessAllowed
ObjectDN               : CN=Policies,CN=System,DC=testlab,DC=local
ActiveDirectoryRights  : CreateChild                                       <--- CreateChild just means "Create GPO" in this context
ObjectAceType          : Group-Policy-Container
ObjectSID              :
InheritanceFlags       : None
BinaryLength           : 56
AceType                : AccessAllowedObject
ObjectAceFlags         : ObjectAceTypePresent
IsCallback             : False
PropagationFlags       : None
SecurityIdentifier     : S-1-5-21-407754292-3742881058-3910138598-1106     <--- SID of the user/group
AccessMask             : 1
AuditFlags             : None
IsInherited            : False
AceFlags               : None
InheritedObjectAceType : All
OpaqueLength           : 0
PS > Convert-SidToName S-1-5-21-407754292-3742881058-3910138598-1106
LAB\Desktop Admins

Get-DomainOU shows us all the Organizational Units in AD. Here, we only have the default Domain Controllers OU and a custom Workstations OU.

PS > Get-DomainOU

usncreated             : 6031
systemflags            : -1946157056
iscriticalsystemobject : True
gplink                 : [LDAP://CN={6AC1786C-016F-11D2-945F-00C04fB984F9},CN=Policies,CN=System,DC=testlab,DC=local;0]     <--- GUID(s) of GPO(s) already linked to the OU
whenchanged            : 06/01/2019 13:14:24
objectclass            : {top, organizationalUnit}
showinadvancedviewonly : False
usnchanged             : 6031
dscorepropagationdata  : {06/01/2019 13:15:24, 01/01/1601 00:00:01}
name                   : Domain Controllers
description            : Default container for domain controllers
distinguishedname      : OU=Domain Controllers,DC=testlab,DC=local
ou                     : Domain Controllers
whencreated            : 06/01/2019 13:14:24
instancetype           : 4
objectguid             : d312c411-7c7c-4fb7-b4f9-cbf0637b551f
objectcategory         : CN=Organizational-Unit,CN=Schema,CN=Configuration,DC=testlab,DC=local

usncreated            : 12790
name                  : Workstations
gplink                : [LDAP://cn={7DD7A136-334C-47C1-8890-D9766D449EFA},cn=policies,cn=system,DC=testlab,DC=local;0]     <--- GUID(s) of GPO(s) already linked to the OU
whenchanged           : 07/01/2019 07:18:51
objectclass           : {top, organizationalUnit}
usnchanged            : 13118
dscorepropagationdata : {07/01/2019 07:18:51, 07/01/2019 07:17:22, 07/01/2019 07:14:37, 06/01/2019 13:52:27...}
distinguishedname     : OU=Workstations,DC=testlab,DC=local
ou                    : Workstations
whencreated           : 06/01/2019 13:28:56
instancetype          : 4
objectguid            : 4f733ab3-1809-4a31-b299-e07a3b7b4669
objectcategory        : CN=Organizational-Unit,CN=Schema,CN=Configuration,DC=testlab,DC=local

The Delegation of Control Wizard in Active Directory Users and Computers (ADUC) has a predefined template for “Manage Group Policy links”. It’s useful for delegating different types of privilege to principals over particular objects.

In this example, we delegate this to the LAB\Desktop Admins group.

This is also easy to enumerate in PowerView by piping Get-DomainOu into Get-DomainObjectAcl and looking for the GP-Link ACE.

PS > Get-DomainOU | Get-DomainObjectAcl -ResolveGUIDs | Where-Object { $_.ObjectAceType -eq "GP-Link" }

AceQualifier           : AccessAllowed
ObjectDN               : OU=Workstations,DC=testlab,DC=local               <--- The OU Distinguished Name
ActiveDirectoryRights  : ReadProperty, WriteProperty                       <--- WriteProperty (GP-Link is a property on the OU object that you can see in the Attribute Editor of ADUC)
ObjectAceType          : GP-Link
ObjectSID              :
InheritanceFlags       : ContainerInherit                                  <--- This will be interesting later
BinaryLength           : 56
AceType                : AccessAllowedObject
ObjectAceFlags         : ObjectAceTypePresent
IsCallback             : False
PropagationFlags       : None
SecurityIdentifier     : S-1-5-21-407754292-3742881058-3910138598-1106     <--- SID of the user/group
AccessMask             : 48
AuditFlags             : None
IsInherited            : False
AceFlags               : ContainerInherit
InheritedObjectAceType : All
OpaqueLength           : 0

Modify GPO

We can also pipe Get-DomainGPO into Get-DomainObjectAcl to find which principals can modify them. Here we look for ActiveDirectoryRights that match WriteProperty, WriteDacl or WriteOwner. (In most cases we only expect to find WriteProperty, but having WriteDacl or WriteOwner will allow us to grant WriteProperty to ourselves and modify the GPO anyway).

We put a match in for the SecurityIdentifier so we only list RIDs > 1000 to avoid seeing Domain Admins and Enterprise Admins etc for every GPO.

PS > Get-DomainGPO | Get-DomainObjectAcl -ResolveGUIDs | Where-Object { $_.ActiveDirectoryRights -match "WriteProperty|WriteDacl|WriteOwner" -and $_.SecurityIdentifier -match "S-1-5-21-407754292-3742881058-3910138598-[\d]{4,10}" }

AceType               : AccessAllowed
ObjectDN              : CN={7DD7A136-334C-47C1-8890-D9766D449EFA},CN=Policies,CN=System,DC=testlab,DC=local
ActiveDirectoryRights : CreateChild, DeleteChild, Self, WriteProperty, DeleteTree, Delete, GenericRead, WriteDacl, WriteOwner
OpaqueLength          : 0
ObjectSID             :
InheritanceFlags      : None
BinaryLength          : 36
IsInherited           : False
IsCallback            : False
PropagationFlags      : None
SecurityIdentifier    : S-1-5-21-407754292-3742881058-3910138598-1105     <--- SID of the user/group
AccessMask            : 983295
AuditFlags            : None
AceFlags              : None
AceQualifier          : AccessAllowed

AceType               : AccessAllowed
ObjectDN              : CN={7DD7A136-334C-47C1-8890-D9766D449EFA},CN=Policies,CN=System,DC=testlab,DC=local
ActiveDirectoryRights : CreateChild, DeleteChild, ReadProperty, WriteProperty, GenericExecute
OpaqueLength          : 0
ObjectSID             :
InheritanceFlags      : ContainerInherit
BinaryLength          : 36
IsInherited           : False
IsCallback            : False
PropagationFlags      : None
SecurityIdentifier    : S-1-5-21-407754292-3742881058-3910138598-1109     <--- SID of the user/group
AccessMask            : 131127
AuditFlags            : None
AceFlags              : ContainerInherit
AceQualifier          : AccessAllowed
PS > Get-DomainGPO | Where-Object { $_.DistinguishedName -eq "CN={7DD7A136-334C-47C1-8890-D9766D449EFA},CN=Policies,CN=System,DC=testlab,DC=local" } | Select-Object DisplayName

displayname
-----------
Workstation Policy

As seen in the Details tab of GPMC, LAB\bwallace is the owner of this GPO called Workstation Policy.

Creators of a GPO are automatically granted explicit Edit settings, delete, modify security, which manifests as CreateChild, DeleteChild, Self, WriteProperty, DeleteTree, Delete, GenericRead, WriteDacl, WriteOwner.

Also in this example, LAB\tlockhart has been granted explicit Edit settings, which are CreateChild, DeleteChild, ReadProperty, WriteProperty, GenericExecute.

Mapping GPOs & OUs

This can be done from a couple of different angles. You could have an interesting GPO and want to know which OUs and/or computers that single GPO applies; you may want to list every GPO that applies to a particular OU; or you may want to list every GPO that applies to a particular computer.

By Computer

Here, we list every GPO that applies to ws-1.testlab.local - displaying only the Display Name and GUID name.

PS > Get-DomainGPO -ComputerIdentity ws-1 -Properties Name, DisplayName

displayname           name
-----------           ----
Demo GPO              {ECB75201-82D7-49F3-A0E0-86788EE7DC36}
Workstation Policy    {7DD7A136-334C-47C1-8890-D9766D449EFA}
Default Domain Policy {31B2F340-016D-11D2-945F-00C04FB984F9}

GPO’s are a bit funny since they have a Display Name, GUID Name and an Object GUID. The later two in particular are easy to confuse.

By GPO

Here, we list every OU that the Demo GPO applies to. We use the GUID name in the GPLink search filter.

PS > Get-DomainOU -GPLink "{ECB75201-82D7-49F3-A0E0-86788EE7DC36}" -Properties DistinguishedName

distinguishedname
-----------------
OU=Domain Controllers,DC=testlab,DC=local
OU=Workstations,DC=testlab,DC=local

If you then need to know which computers are in these OUs, you can do so with:

PS > Get-DomainComputer -SearchBase "LDAP://OU=Workstations,DC=testlab,DC=local" -Properties DistinguishedName

distinguishedname
-----------------
CN=WS-1,OU=Workstations,DC=testlab,DC=local
CN=WS-2,OU=Workstations,DC=testlab,DC=local
CN=WS-3,OU=Workstations,DC=testlab,DC=local

By OU

This one is more of a pig.

If we get the GPLink attribute for the Workstations OU (which tells us every GPO that is linked to it), the results are returned as a single text string which means we can’t just pipe it straight into Get-DomainGPO and find their corresponding Display Names.

PS > Get-DomainOU -Identity "Workstations" -Properties GPLink

gplink
------
[LDAP://cn={ECB75201-82D7-49F3-A0E0-86788EE7DC36},cn=policies,cn=system,DC=testlab,DC=local;0][LDAP://cn={7DD7A136-334C-47C1-8890-D9766D449EFA},cn=policies,cn=system,DC=test...

Instead, we can do it this way:

PS > $GPLink = (Get-DomainOU -Identity "Workstations" -Properties GPLink).gplink
PS > [Regex]::Matches($GPLink, '(?<={)(.*?)(?=})') | Select-Object -ExpandProperty Value | ForEach-Object { Get-DomainGPO -Identity "{$_}" -Properties DisplayName }

displayname
-----------
Demo GPO
Workstation Policy

The astute amongst you may notice in these examples, how some return GPOs included via inheritance (e.g. the Default Domain Policy) and others do not.

Inheritance

I find inheritance pretty interesting, especially when it comes to the Delegation of Control Wizard. By default, it will enable inheritance for This object and all descending objects.

PS > Get-DomainOU | Get-DomainObjectAcl -ResolveGUIDs | Where-Object { $_.ObjectAceType -eq "GP-Link" }

AceQualifier           : AccessAllowed
ObjectDN               : OU=Workstations,DC=testlab,DC=local
ActiveDirectoryRights  : ReadProperty, WriteProperty
ObjectAceType          : GP-Link
ObjectSID              :
InheritanceFlags       : ContainerInherit     <---
BinaryLength           : 56
AceType                : AccessAllowedObject
ObjectAceFlags         : ObjectAceTypePresent
IsCallback             : False
PropagationFlags       : None
SecurityIdentifier     : S-1-5-21-407754292-3742881058-3910138598-1106
AccessMask             : 48
AuditFlags             : None
IsInherited            : False                <--- This OU *is not* inheriting from elsewhere
AceFlags               : ContainerInherit     <---
InheritedObjectAceType : All
OpaqueLength           : 0

If we now subsequently create a new OU inside this one, LAB\Desktop Admins will inherit the same GP-Link privileges over it.

AceQualifier           : AccessAllowed
ObjectDN               : OU=DAs,OU=Workstations,DC=testlab,DC=local     <--- DA OU is a child of Workstation OU
ActiveDirectoryRights  : ReadProperty, WriteProperty
ObjectAceType          : GP-Link
ObjectSID              :
InheritanceFlags       : ContainerInherit                <---
BinaryLength           : 56
AceType                : AccessAllowedObject
ObjectAceFlags         : ObjectAceTypePresent
IsCallback             : False
PropagationFlags       : None
SecurityIdentifier     : S-1-5-21-407754292-3742881058-3910138598-1106
AccessMask             : 48
AuditFlags             : None
IsInherited            : True                            <--- This OU *is* inheriting
AceFlags               : ContainerInherit, Inherited     <---
InheritedObjectAceType : All
OpaqueLength           : 0

If we manually modify the inheritance on the Workstations OU to This object only, the new ACL looks like this:

AceQualifier           : AccessAllowed
ObjectDN               : OU=Workstations,DC=testlab,DC=local
ActiveDirectoryRights  : ReadProperty, WriteProperty
ObjectAceType          : GP-Link
ObjectSID              :
InheritanceFlags       : None    <---
BinaryLength           : 56
AceType                : AccessAllowedObject
ObjectAceFlags         : ObjectAceTypePresent
IsCallback             : False
PropagationFlags       : None
SecurityIdentifier     : S-1-5-21-407754292-3742881058-3910138598-1106
AccessMask             : 48
AuditFlags             : None
IsInherited            : False    <---
AceFlags               : None     <---
InheritedObjectAceType : All
OpaqueLength           : 0

Finally, if you have nested children like this:

The DA OU will inherit from both Workstations and Admins. So we have a delegation on Workstations for LAB\Desktop Admins and a delegation on Admins for LAB\Team 2 - the DA OU will inherit both.

AceQualifier           : AccessAllowed
ObjectDN               : OU=Workstations,DC=testlab,DC=local
ActiveDirectoryRights  : ReadProperty, WriteProperty
ObjectAceType          : GP-Link
ObjectSID              :
InheritanceFlags       : ContainerInherit
BinaryLength           : 56
AceType                : AccessAllowedObject
ObjectAceFlags         : ObjectAceTypePresent
IsCallback             : False
PropagationFlags       : None
SecurityIdentifier     : S-1-5-21-407754292-3742881058-3910138598-1106
AccessMask             : 48
AuditFlags             : None
IsInherited            : False
AceFlags               : ContainerInherit
InheritedObjectAceType : All
OpaqueLength           : 0

AceQualifier           : AccessAllowed
ObjectDN               : OU=Admins,OU=Workstations,DC=testlab,DC=local
ActiveDirectoryRights  : ReadProperty, WriteProperty
ObjectAceType          : GP-Link
ObjectSID              :
InheritanceFlags       : ContainerInherit
BinaryLength           : 56
AceType                : AccessAllowedObject
ObjectAceFlags         : ObjectAceTypePresent
IsCallback             : False
PropagationFlags       : None
SecurityIdentifier     : S-1-5-21-407754292-3742881058-3910138598-1110
AccessMask             : 48
AuditFlags             : None
IsInherited            : False
AceFlags               : ContainerInherit
InheritedObjectAceType : All
OpaqueLength           : 0

AceQualifier           : AccessAllowed
ObjectDN               : OU=Admins,OU=Workstations,DC=testlab,DC=local
ActiveDirectoryRights  : ReadProperty, WriteProperty
ObjectAceType          : GP-Link
ObjectSID              :
InheritanceFlags       : ContainerInherit
BinaryLength           : 56
AceType                : AccessAllowedObject
ObjectAceFlags         : ObjectAceTypePresent
IsCallback             : False
PropagationFlags       : None
SecurityIdentifier     : S-1-5-21-407754292-3742881058-3910138598-1106
AccessMask             : 48
AuditFlags             : None
IsInherited            : True
AceFlags               : ContainerInherit, Inherited
InheritedObjectAceType : All
OpaqueLength           : 0

AceQualifier           : AccessAllowed
ObjectDN               : OU=DAs,OU=Admins,OU=Workstations,DC=testlab,DC=local
ActiveDirectoryRights  : ReadProperty, WriteProperty
ObjectAceType          : GP-Link
ObjectSID              :
InheritanceFlags       : ContainerInherit
BinaryLength           : 56
AceType                : AccessAllowedObject
ObjectAceFlags         : ObjectAceTypePresent
IsCallback             : False
PropagationFlags       : None
SecurityIdentifier     : S-1-5-21-407754292-3742881058-3910138598-1110
AccessMask             : 48
AuditFlags             : None
IsInherited            : True
AceFlags               : ContainerInherit, Inherited
InheritedObjectAceType : All
OpaqueLength           : 0

AceQualifier           : AccessAllowed
ObjectDN               : OU=DAs,OU=Admins,OU=Workstations,DC=testlab,DC=local
ActiveDirectoryRights  : ReadProperty, WriteProperty
ObjectAceType          : GP-Link
ObjectSID              :
InheritanceFlags       : ContainerInherit
BinaryLength           : 56
AceType                : AccessAllowedObject
ObjectAceFlags         : ObjectAceTypePresent
IsCallback             : False
PropagationFlags       : None
SecurityIdentifier     : S-1-5-21-407754292-3742881058-3910138598-1106
AccessMask             : 48
AuditFlags             : None
IsInherited            : True
AceFlags               : ContainerInherit, Inherited
InheritedObjectAceType : All
OpaqueLength           : 0

I think that’s enough enumeration for now! In the next part, we’ll start with some abuses.