OUs and GPOs and WMI Filters, Oh My!

Abusing GPOs is a tactic that’s been actively in-play for many years. ACL-based path-finding for GPOs was introduced to BloodHound 1.5 in 2018, and other tools have been released such as SharpGPOAbuse which implement various abuse primitives.

You may be familiar with this representation, where a machine is a member of an OU and a GPO is linked to that OU. If you could therefore modify the GPO, you can push policies and configuration changes to the machine to compromise it.

However, there’s an aspect to GPOs that BloodHound is blind to – WMI Filters.

A WMI filter provides a further means of filtering the targets of a GPO, and are typically created in GPMC. This example query should make sense to anyone familiar with the WMI query language (it’s very reminiscent of SQL). It will essentially only return computers whose name contains the string “WIN”.

The filter is then linked to a GPO. A GPO may only have 1 WMI filter assigned to it at a time.

Based on this we can conclude that the “Example GPO” will not be applied to the “DESKTOP” machine, because the WMI filter will exclude it. This is definitely the kind of thing that can screw you over on an engagement, so it’s useful to know how to find them. Luckily, this is fairly straight forward and can be done with any old LDAP query tool. I still prefer PowerView because I’m a dinosaur and it’s just so well designed.

WMI filters themselves are stored in CN=System, CN=WmiPolicy, CN=SOM and are of class msWMI-Som. The display name is stored on an attribute called msWMI-Name and actual content of the filter on msWM-IParm2.

PS C:\> Get-DomainObject -SearchBase "CN=SOM, CN=WmiPolicy, CN=System, DC=lab, DC=dev" -LDAPFilter "(objectclass=msWMI-Som)" -Properties name,mswmi-name,mswmi-parm2 | fl


name        : {6C1E8CAB-D8B0-46C5-A83B-FC7B44078C3D}
mswmi-parm2 : 1;3;10;58;WQL;root\CIMv2;SELECT * FROM Win32_ComputerSystem WHERE Name LIKE "%WIN%";
mswmi-name  : Example Filter

Take note of the CN {6C1E8CAB-D8B0-46C5-A83B-FC7B44078C3D}, as we need to cross-reference this in just a second…

Behind the scenes, a GPO is linked to an OU by modifying the gPLink attribute on the OU. An OU knows which GPOs are linked to it, but the GPOs don’t know which OUs they’re linked to (if that makes sense). In the same fashion, a WMI filter is linked to a GPO by modifying the gPCWQLFilter attribute of the GPO. A GPO knows which WMI filter is linked to it, but a filter does not know which GPOs it’s linked to.

The upshot of this is that we can’t just query a WMI filter and ask it which GPOs it’s linked to. Instead, we have to query all the GPOs (or just a single GPO of interest) and see if the gPCWQLFilter attribute is empty or not.

GPOs are found in CN=System, CN=Policies and are of class groupPolicyContainer.

PS C:\> Get-DomainObject -SearchBase "CN=Policies, CN=System, DC=lab, DC=dev" -LDAPFilter "(objectclass=groupPolicyContainer)" -Properties displayname,gpcwqlfilter | fl


displayname : Default Domain Policy

displayname : Default Domain Controllers Policy

displayname  : Example GPO
gpcwqlfilter : [lab.dev;{6C1E8CAB-D8B0-46C5-A83B-FC7B44078C3D};0]

Here, we can see that a WMI filter of CN {6C1E8CAB-D8B0-46C5-A83B-FC7B44078C3D} is linked to the “Example GPO”, which matches the CN of the “Example Filter” above.

Danger Zone

The goal of this post was to provide enumeration information only, as a prerequisite or sanity-check before attempting to exploit an ACL-abuse primitive on a GPO. But what if we wanted to be a little more reckless? Can we “bypass” WMI filters?

There are two methods that I can think of to do this. The first is to just remove the filter, which is as simple as removing the content of the gPCWQLFilter attribute of the GPO. This is the more viable route, since you will most likely have the privilege to do this if you already have an ACL-based attack on the GPO, but it is rather like hitting the problem with a sledgehammer.

In this example, my “rasta” user has “Edit settings” rights over Example GPO.

PS C:\> whoami
dev\rasta

PS C:\> Set-DomainObject -Identity "{249177CF-9AE9-4603-9246-2E246E50A66C}" -Clear gpcwqlfilter -Verbose
VERBOSE: [Get-DomainSearcher] search base: LDAP://WIN-ND2N5O4UQAG.LAB.DEV/DC=LAB,DC=DEV
VERBOSE: [Get-DomainObject] Get-DomainObject filter string:
(&(|(|(samAccountName={249177CF-9AE9-4603-9246-2E246E50A66C})(name={249177CF-9AE9-4603-9246-2E246E50A66C})(displayname={249177CF-9AE9-4603-9246-2E246E50A66C}))))
VERBOSE: [Set-DomainObject] Clearing 'gpcwqlfilter' for object ''

Refreshing the view in GPMC will verify that the filter is no longer linked to the GPO, #NoFilter.

The other route is less likely to be possible, and that’s modifying the content of the filter. WMI filters have their own DACL and delegation properties in GPMC, just like GPOs. But it’s probably not as common to see principals with edit rights over these.

Nevertheless, you can enumerate the ACLs with the good ole’ Get-DomainObjectAcl cmdlet.

PS C:\> Get-DomainObjectAcl -SearchBase "CN=SOM, CN=WmiPolicy, CN=System, DC=lab, DC=dev" -LDAPFilter "(objectclass=msWMI-Som)" -ResolveGUIDs | ? { $_.ActiveDirectoryRights -like "*WriteProperty*" }


AceType               : AccessAllowed
ObjectDN              : CN={6C1E8CAB-D8B0-46C5-A83B-FC7B44078C3D},CN=SOM,CN=WMIPolicy,CN=System,DC=lab,DC=dev
ActiveDirectoryRights : ReadProperty, WriteProperty, GenericExecute
OpaqueLength          : 0
ObjectSID             :
InheritanceFlags      : None
BinaryLength          : 36
IsInherited           : False
IsCallback            : False
PropagationFlags      : None
SecurityIdentifier    : S-1-5-21-4201895101-3124449509-559437207-1104
AccessMask            : 131124
AuditFlags            : None
AceFlags              : None
AceQualifier          : AccessAllowed



PS C:\> ConvertFrom-SID S-1-5-21-4201895101-3124449509-559437207-1104
DEV\rasta

To modify the filter, we can just change the value of the msWM-IParm2 attribute. Let’s have a look at the current value again:

1;3;10;58;WQL;root\CIMv2;SELECT * FROM Win32_ComputerSystem WHERE Name LIKE "%WIN%";

From what I’ve been able to ascertain, the string 1;3;10 is constant and 58 is the length of the query. WQL is the query language and root\CIMv2 is the namespace.

PS C:\> $str = 'SELECT * FROM Win32_ComputerSystem WHERE Name LIKE "%WIN%"'
PS C:\> $str.Length
58

If we wanted to keep the query as it is but also include the “DESKTOP” machine, we could do:

SELECT * FROM Win32_ComputerSystem WHERE Name LIKE "%WIN%" OR Name = "DESKTOP"

That would make our final value:

1;3;10;78;WQL;root\CIMv2;SELECT * FROM Win32_ComputerSystem WHERE Name LIKE "%WIN%" OR Name = "DESKTOP";
PS C:\> Set-DomainObject -Identity "{6C1E8CAB-D8B0-46C5-A83B-FC7B44078C3D}" -Set @{ 'mswmi-parm2' = '1;3;10;78;WQL;root\CIMv2;SELECT * FROM Win32_ComputerSystem WHERE Name LIKE "%WIN%" OR Name = "DESKTOP";' } -Verbose
VERBOSE: [Get-DomainSearcher] search base: LDAP://WIN-ND2N5O4UQAG.LAB.DEV/DC=LAB,DC=DEV
VERBOSE: [Get-DomainObject] Get-DomainObject filter string:
(&(|(|(samAccountName={6C1E8CAB-D8B0-46C5-A83B-FC7B44078C3D})(name={6C1E8CAB-D8B0-46C5-A83B-FC7B44078C3D})(displayname={6C1E8CAB-D8B0-46C5-A83B-FC7B44078C3D}))))
VERBOSE: [Set-DomainObject] Setting 'mswmi-parm2' to '1;3;10;78;WQL;root\CIMv2;SELECT * FROM Win32_ComputerSystem WHERE Name LIKE "%WIN%" OR Name = "DESKTOP";' for
object ''

As before, refreshing GPMC confirms the change has been made. From an attacker’s perspective, Get-DomainObject will also return the new value.

Now… I named this portion of the post the Danger Zone for a fairly obvious reason. The general purpose of WMI filters is to exclude machines from picking up and applying a GPO, and one must assume it’s excluded for good reason.

By manipulating the GPO and/or WMI filter in this way, you risk pushing changes to the machine that will break or damage something.

,

Related posts

ANYSIZE_ARRAY in C#

There are multiple structures in Windows that contain fixed sized arrays. The instance...

SafeHandle vs IntPtr

C# is a popular language in both the commercial space (think ASP.NET Core, MVC,...

C# Source Generators

Introduction

C# Source Generators made their first appearance around the release of .NET 5 and...

Latest posts

ANYSIZE_ARRAY in C#

There are multiple structures in Windows that contain fixed sized arrays. The instance...

SafeHandle vs IntPtr

C# is a popular language in both the commercial space (think ASP.NET Core, MVC,...

C# Source Generators

Introduction

C# Source Generators made their first appearance around the release of .NET 5 and...