Abusing sAMAccountName Hijacking in "GPP: Local Users and Groups"
Introduction
While developing GPOHound, a tool for dumping and analysing Group Policy Objects (GPOs), I explored Microsoft's documentation and community discussions. During this research, I discovered that Microsoft allows the use of variables in text fields within Group Policy Preferences (GPP). Although this feature is designed to offer flexibility, it can be abused under specific conditions to escalate privileges locally.
Group Policy Preferences overview
Group Policy Preferences are configurable settings that allow administrators to define user and computer configurations on domain-joined machines. Unlike traditional "Group Policy Settings", which enforce configurations and prevent users from changing them, "Preferences" are more flexible. Users can often modify these settings after they're applied.
You can configure GPP settings through the "Group Policy Management Console" (GPMC) by editing an existing or new GPO and navigating to:
- User Configuration > Preferences : Applied when a user logs on.
- Computer Configuration > Preferences : Applied during system startup.
Additionally, GPP settings (along with some regular "Policy Settings") may also be reapplied periodically in the background through a "Group Policy Refresh", which occurs by default every 90 minutes, with a randomized offset of up to 30 minutes.
As shown above, GPP offers a wide variety of preference types, some of which are specific to user or computer configurations. In this article, we will focus on the "Local Users and Groups" preference type, which is available under both user and computer configurations.
Local Users and Groups
According to Microsoft, the "Local Users and Groups" preference allows administrators to create, modify, or delete local user accounts and local security groups on target systems. In this article, we will focus specifically on the "Local Group" setting, whose primary function is to add or remove users, computers, or groups as members of a specified local group.
In the interface above, you can configure which local group to modify and specify the members to be added or removed. In this example, we're updating the "Administrators" local group by adding two members: "alice" and "bob". You'll notice that "alice" does not have a Security Identifier (SID) specified, while "bob" does. This difference is due to the two methods GPP supports for specifying local group members.
SID-Resolved members
Clicking the "Add..." button opens the "Local Group Member" dialog. Selecting the three-dot button next to the "Name" field brings up the standard "Select User, Computer, or Group" window.
Once a domain object is selected, the "Name" field becomes grayed out. This indicates that the object's SID has been resolved and it will appear as such in the group properties. We will refer to these members as "SID-Resolved" members.
Name-Only members
Alternatively, you can manually type a name in the "Name" field of the "Local Group Member" dialog. In this case, the field remains editable and no SID is associated. We will refer to these as "Name-Only" members.
For "Name-Only" members, SID resolution happens client-side, during the GPO application on the target machine. The system uses the LsaLookupNames
function to resolve these names into SIDs. This is demonstrated in the following Wireshark capture, taken during the application of the above GPO:
The LsaLookupNames
function supports several naming formats:
Name Type | Format | Example |
---|---|---|
Down-level logon name | NetBIOSDomainName\sAMAccountName | MINILAB\alice |
DNS-based down-level logon name | DnsDomainName\sAMAccountName | mini.lab\alice |
User Principal Name (UPN) | userPrincipalName | alice@mini.lab |
Isolated name | sAMAccountName | alice |
The LsaLookupNames
function resolves user, group, and local group names by following a specific lookup order:
- Well-known names
- Built-in accounts
- Local system accounts
- Primary domain accounts
- Trusted domains accounts
If a name cannot be resolved, it stays untranslated, meaning the user will not be added to the local group. Additionally, using "Isolated names" can create ambiguity and resolve to the wrong account if the same username exists across multiple domains or trusts.
Abusing Name-Only members
This led to a simple idea: What happens if the "Name-Only" member does not exist at the time the GPO is applied? Can we hijack the group membership by creating or modifying an account to match the expected sAMAccountName
? Let's put that to the test.
Non-Existent member hijacking
We create a user named priv_usr
, who has GenericWrite
permissions over another user account named low_priv
. We then create a GPP configuration that adds a "Name-Only" member, nonexistentuser
, to the local "Administrators" group.
When the GPO is applied to a client workstation, the system attempts to resolve the nonexistentuser
. Since no such account currently exists, no user is added to the "Administrators" group, as expected.
Now comes the interesting part. Using the delegated GenericWrite
permissions, we modify the sAMAccountName
of the low_priv
user and set it to nonexistentuser
. I'm using bloodyAD to perform the modification:
> bloodyAD --host "192.168.58.30" -u "priv_usr" -p '0penSesame42!' get object "nonexistentuser"
(...)
bloodyAD.exceptions.NoResultError: [-] No object found in DC=mini,DC=lab with filter: (sAMAccountName=nonexistentuser)
> bloodyAD --host "192.168.58.30" -u "priv_usr" -p '0penSesame42!' set object "CN=low_priv,CN=Users,DC=mini,DC=lab" sAMAccountName -v "nonexistentuser"
[+] low_priv\'s sAMAccountName has been updated
> bloodyAD --host "192.168.58.30" -u "priv_usr" -p '0penSesame42!' get object "nonexistentuser" --attr sAMAccountName
distinguishedName: CN=low_priv,CN=Users,DC=mini,DC=lab
sAMAccountName: nonexistentuser
Now that the account exists with the targeted sAMAccountName
, the final step is to either wait for a "Group Policy Refresh" as the "Local Groups and Users" policy can be applied in the background during runtime or manually trigger the update. Once the GPO is applied, the low_priv
user, now answering to nonexistentuser
, is successfully added to the local Administrators group.
Existing member with UPN format hijacking
In this scenario, we continue using the same setup with the low_priv
and priv_usr
users. This time however, we introduce a new user called existingusr
with the UPN existingusr@mini.lab
. We add it to the local Administrators group as a "Name-Only" member, using the UPN name format.
After applying the GPO on a client workstation, the existingusr
account is correctly resolved and added to the local Administrators group, as it exists in the domain.
Now let's attempt to hijack that membership.
Because the sAMAccountName
attribute only restricts the following characters: / \ [ ] : ; | " = , + * ? < >
, it still allows the @
character. That means it's perfectly valid to set a user's sAMAccountName
to a value like existingusr@mini.lab
, even though this more closely resembles a UPN than a traditional sAMAccountName
.
Let's exploit this by changing the sAMAccountName
of low_priv
to existingusr@mini.lab
using priv_usr
, again via bloodyAD:
> bloodyAD --host "192.168.58.30" -u "priv_usr" -p '0penSesame42!' get object "existingusr" --attr userPrincipalName
distinguishedName: CN=existingusr,CN=Users,DC=mini,DC=lab
userPrincipalName: existingusr@mini.lab
> bloodyAD --host "192.168.58.30" -u "priv_usr" -p '0penSesame42!' set object "CN=low_priv,CN=Users,DC=mini,DC=lab" sAMAccountName -v "existingusr@mini.lab"
[+] CN=low_priv,CN=Users,DC=mini,DC=lab\'s sAMAccountName has been updated
> bloodyAD --host "192.168.58.30" -u "priv_usr" -p '0penSesame42!' get object "CN=low_priv,CN=Users,DC=mini,DC=lab" --attr sAMAccountName
distinguishedName: CN=low_priv,CN=Users,DC=mini,DC=lab
sAMAccountName: existingusr@mini.lab
When the GPO is reapplied on the client, the low_priv
user gets added to the local Administrators group instead of the intended existingusr
.
This happens because the LsaLookupNames
function first looks for an exact match of the string existingusr@mini.lab
in the sAMAccountName
attribute. Since low_priv
now holds that value, the client resolves it directly and assigns the group membership without first attempting to resolve the userPrincipalName
attribute with the value existingusr@mini.lab
.
Summary of the abuse
To exploit this behavior, two key conditions must be met:
- A "Local Users and Groups" GPP setting must include a "Name-Only" member referencing either:
- A non-existent object
- An object using a UPN name format
- Control over a user or group in the primary or a trusted domain, with the capability to either:
- Modify the user account's
sAMAccountName
and log in with the user. - Modify the group's
sAMAccountName
and add members to the group.
- Modify the user account's
Limitations
There are a couple limitations to this abuse:
- Computers can't be used to hijack group memberships unless a computer is explicitly configured as a "Name-Only" member because computer accounts require a trailing
$
in theirsAMAccountName
. Removing the$
was possible prior to the patches for CVE-2021-42278 and CVE-2021-42287, both part of the noPac attack chain. - The
sAMAccountName
for user object is limited to 20 characters due to legacy constraints. This makes user object abuse somewhat limited in scope. However, group objects support up to 256 characters insAMAccountName
, making them more flexible and better suited for the abuse. - Some members may be local accounts. While they seem non-existent in the domain, the system still resolves them locally.
If you've ever configured this type of GPP policy, you might now be wondering: Why would anyone use "Name-Only" members instead of "SID-Resolved" members? Well, I was asking myself the same thing... Until I discovered that you can use variables in "Name-Only" entries.
Preference Process Variables
While making research for GPOHound, I needed to dig into how "Local Users and Groups" worked. One of the first resources I found while searching for "GPO Local Users and Groups"
was this article by Brandon Wilson: Using Group Policy Preferences to Manage the Local Administrator Group. In this post, there's a noteworthy section demonstrating how to assign permissions per-machine using a "Name-Only" member like %DomainName%\%ComputerName%_Administrators
.
This method takes advantage of the "Preference Process Variables", a feature that lets you insert dynamic, environment-based variables into GPP fields. When you're entering text in a GPP textbox, pressing F3 brings up a menu where you can select from predefined variables like %DomainName%
, %ComputerName%
, and more. These variables are resolved on the client side when the GPO is applied. You can find the complete list of variables here: Preference Process Variables (Microsoft Docs)
I came across many blog posts that leverage these variables, particularly %ComputerName%
. This enables administrators to dynamically link each computer's local group to a domain group named after the client computer's NetBIOS name. Administrators can then manage local group memberships centrally by adding users to the corresponding domain groups.
However, when GPOs are applied too broadly, for example, linked to generic Organizational Units (OUs), they may apply to computers that weren't intended to be managed. This can result in references to non-existent domain groups, creating opportunities to hijack those unresolved group entries.
Abusing Process Variables
We take for example a GPO that adds MINILAB\%ComputerName%_adm
as a "Name-Only" member to the local Administrators group. This GPO is linked to an "OU" containing the computer WS
.
An attacker can identify the GPO and determine the targeted computers. They can then check whether the corresponding domain groups exist, such as WS_adm
. If a group is missing, the attacker can hijack its sAMAccountName
as explained previously :
> bloodyAD --host "192.168.58.30" -u "priv_usr" -p '0penSesame42!' get object "WS_adm"
(...)
bloodyAD.exceptions.NoResultError: [-] No object found in DC=mini,DC=lab with filter: (sAMAccountName=WS_adm)
> bloodyAD --host "192.168.58.30" -u "priv_usr" -p '0penSesame42!' set object "CN=low_priv,CN=Users,DC=mini,DC=lab" sAMAccountName -v "WS_adm"
[+] CN=low_priv,CN=Users,DC=mini,DC=lab\'s sAMAccountName has been updated
> bloodyAD --host "192.168.58.30" -u "priv_usr" -p '0penSesame42!' get object "WS_adm" --attr sAMAccountName
distinguishedName: CN=low_priv,CN=Users,DC=mini,DC=lab
sAMAccountName: WS_adm
When the GPO is applied to the WS
workstation, %ComputerName%
resolves to WS
, and the system looks for MINILAB\WS_adm
. And again, the low_priv
user gets added to the Administrators group.
Exploitability
To validate how impactful this could be in real-world environments, I reviewed data from old pentests engagements using GPOHound. In one case, I found a GPO that used a "Name-Only" member with the value SRV_%ComputerName%_Admins
added to the local group "Remote Desktop Users".
Using BloodHound data, I found out that the policy was linked to 110 computers, yet just 20 matching domain groups were present. The only thing that I couldn't confirm was whether WMI filtering was used, but even so, this highlights the scale of potential abuse.
Applicability in Restricted Group Policy
Although this article focuses on "Group Policy Preferences", similar abuse can also occur with "Restricted Groups" policy, which is another method for managing local group memberships. However, unlike GPP, "Restricted Groups" do not support "Name-Only" UPN format and environment variables.
If you're interested in understanding this policy setting further, I recommend this article by Daniel Petri, which provides a good overview. It even presents the addition of non-existent members as a feature, without addressing the risk of misconfiguration.
Integration in GPOHound
As a result of this research, I've integrated detection capabilities into GPOHound to flag potentially abusable members in "Local Users and Groups" and "Restricted Group" policies. This includes detection of %ComputerName%
variables in GPP and attempts to resolve the corresponding sAMAccountName
for each computer affected by the GPO.
Final words
This type of abuse is quite niche and requires several specific conditions. Since I haven't found any articles addressing it online, I decided to write this one. I haven't had the opportunity to test this in a production environment, but based on what I've observed, abusing this misconfiguration could leave traces. Always ensure you have authorization before testing this in client environments.
With that said, I appreciate you following along. If you have any questions or want to dive deeper, just let me know on X : @Toffyrak.