Yuval Gordon Security Researcher | Microsoft

Identity systems—particularly Active Directory, which is the primary identity store for most businesses—are constantly under attack by cybercriminals because they are the gateway to an organization’s critical information systems, including valuable customer data.

Here we’ll explore a little-known Discretionary Access Control List (DACL) tactic that attackers can use to hide membership from a group and possibly evade detection. By using the Primary Group ID attribute and a specific Access Control Entry (ACE), you can hide the membership of one group that you’re already a member of—without having any permissions on the group. This cool trick cannot work on members of protected groups such as Domain Admins, but can work on members of normal groups such as DnsAdmins, which can be used to escalate to Domain Admins.

Related Reading

How Active Directory handles normal group membership

To fully understand what this technique is adding, we must first have a basic knowledge of how group memberships are handled by Active Directory. Each domain controller in a domain stores an Active Directory database (ntds.dit). Within each ntds.dit there is a column called DNT (Distinguished Name Tag), which is like the object’s ID in the database. When we add a user to a group, the domain controller adds a row to a table called “link_table” that saves a link between the member DNT (backlink_DNT) and the group DNT (link_DNT).

Pretty simple so far, but that’s not all there is to group membership. There is another attribute called PrimaryGroupID (PGID) that specifies the Relative Identifier (RID) of the user’s primary group, which is the last part of a principal SID (principal SID = Domain SID + RID). This attribute is set by default to 513 (Domain Users) for new users, 515 (Domain Computers) for new computers, 516 (Domain Controllers) for domain controllers, and 521 for Read-only Domain Controllers. This attribute could also be set by clicking on “Set Primary Group” button in the “Member Of” tab inside the Active Directory Users and Computers tool, if the required Write access is present on the target user.

Setting Primary Group in Active Directory
Setting the primary group

Using this tool, we can set the primary group only to a group that the user is already a member of. The special thing about this attribute—and the reason Active Directory administrators should be aware of it to guard against abuse by adversaries—is that by setting Primary Group ID (PGID), you deactivate the membership link between the group and the member. In other words, the link is removed from the aforementioned link_table.

“But how can that be, Yuval? When I am querying for members of the Domain Admins group, I’m also seeing users that have the Primary Group set to domain admins!” Great question, reader!

Most of the tools that query membership are really asking two things. Let’s imagine that we want to get all the members of the Domain Admins group:

  1. The tool will query for the member attribute, which will make the DC check the link_table and return the objects that have a member link with the Domain Admins group.
  2. The tool will query for any object that has a value of 512 (RID of Domain Admins) in the PrimaryGroupID attribute.

Why you need to monitor use of Primary Group ID

By using Primary Group ID, you make the membership to one of the groups be written inside an attribute on the member side. To better understand what can be achieved with this action, let’s look at this from the perspective of an attacker.

Imagine that you want to hide a user’s membership from one of its groups. With normal membership, you could just specify an Active Directory access control entry (ACE) to deny entry to everyone on “read group membership.” But that approach will make the membership of the user almost empty (the primary group will still be shown), which might look suspicious. Plus, with this approach you’ve hidden the membership only from the member side and not from the group side. So if someone tries to read one of the group’s members, the user will still be listed there.

This is where PGID comes into play: By specifying the primary group, you can make the membership listed only on the member side. Now if you add an ACE to deny everyone to read PGID on the object, then one of the group memberships is now hidden—both from the member side and the group side.

How attackers can use Primary Group ID to hide malicious users

By using a simple PowerShell function, you could hide the “haxer” user from the “attackers” group, by changing the PGID to the RID of the “attackers” group, then adding a deny-read on PGID.

Membership from the user side
Membership from the user side

The primary group now shows <None>. Odd, but still might go unnoticed.

Active Directory membership from the group side
Membership from the group side

The following code can be used to hide a membership of a specific group that the user is already a member of, if the running user has Write DACL (Modify Permissions) on the target user. For example, to hide myself (haxer) from the attackers group that I am already a member of (RID 2607), I used the function as shown above. I set the “AddWritePGIDToCurrent” flag to true because I did not have the required permissions—I had only WriteDACL permission.

function Hide-Membership {
  param (
    [parameter(Mandatory = $true)]
    [string]$TargetDN,
             
    [parameter(Mandatory = $true)]
    [int]$RID,

    [parameter(Mandatory = $false)]
    [bool]$AddWritePGIDToCurrent = $false
  )

  $target = [ADSI]"LDAP://$TargetDN"
  $identityReference = (New-Object System.Security.Principal.NTAccount("everyone")).Translate([System.Security.Principal.SecurityIdentifier])
  $guid = New-Object Guid "bf967a00-0de6-11d0-a285-00aa003049e2" # Guid of primaryGroupID attribute

  $target.PsBase.Options.SecurityMasks = "Dacl"

  if ($AddWritePGIDToCurrent) {
    $identityReferenceCurrentUser = ([System.Security.Principal.WindowsIdentity]::GetCurrent()).User
    $aceAllow = New-Object System.DirectoryServices.ActiveDirectoryAccessRule @($identityReferenceCurrentUser,"WriteProperty","Allow",$Guid)
    $target.PsBase.ObjectSecurity.AddAccessRule($aceAllow)
    $target.CommitChanges()
  }

  $aceDeny = New-Object System.DirectoryServices.ActiveDirectoryAccessRule @($identityReference,"ReadProperty","Deny",$Guid)

  $target.PsBase.ObjectSecurity.AddAccessRule($aceDeny)
  $target.CommitChanges()
  $target.primarygroupid = $RID 
  $target.CommitChanges()
}
Hide-Membership -TargetDN "CN=haxer,CN=Users,DC=f047-d01,DC=lab" -RID 2607 -AddWritePGIDToCurrent $true

Note that this technique will not work on members of any group that is protected by the SDPROP process and AdminSDHolder, as we are relying on DACL, which in that case will just be reverted to the DACL of AdminSDHolder every hour or so.

Detecting malicious use of Primary Group ID to hide membership

You can detect the use of PGID to hide membership by monitoring for ACEs set to deny on read for this property. But this can be a difficult task without the proper tools or systems.

One option is to use the below command line periodically to search for any user with a PGID that you cannot read. I am not aware of any legitimate reason for a user to have a non-existing PGID or unreadable one, but it might happen, so you might see some false positives.

Get-ADUser -Filter {-not(primaryGroupID -like “*”)}

An easier way to monitor the malicious use of this technique: Semperis Directory Services Protector includes an indicator that monitors and alerts on users with the configuration “Users and computers without readable PGID.”

Protecting Active Directory from the ongoing barrage of attacks can be challenging. But by continually monitoring your environment for the many ways in which attackers can exploit certain configurations, you can identify and remediate intrusions before they become full-scale breaches.