TL;DR how to scrub Group Policy for domain groups being put in local admins on workstations.
Welcome to the annexes of our Auditing AD Series!
Part I: 3 warm up questions to get us started
Part II: Who can reset the CISO’s password?
Part III: Who can execute DCSync?
Part IV: Who can modify the Domain Admins group?
Part V: Domain dominance in minutes via ACL abuse (i.e. why auditing AD matters)
Part VI: Why Allow statements matter a LOT more than Deny ones
Part VII: Sneaky persistence via hidden objects in AD
Part VIII: SDDL, what is it, does it matter?
Part IX: Do you know who owns your domain?
Part X: Who can push ransomware via Group Policy?
Part XI: Free ways to simplify auditing AD
Part XII: Sidenote on arcane rights like Self
Part XIII: Sidenote on the ScriptPath right
Part XIV: Self & so called “Effective Permissions”
Part XV: Inheritance Explained & an Example
Part XVI: Summary of our Auditing AD Series
Annex A: Scrubbing Group Policy for local admins
Annex B: What Property Sets in AD are, & why they matter
Annex C: Dangerous Rights & RE GUIDs Cheatsheet
Annex D: Mishky’s Blue Team Auditor
Annex E: Even ChatGPT gets this stuff wrong
Annex F: Get-ADPermission Cheatsheet
Annex G: Mishky’s Red Team Enumerate & Attack tools
Background
Like many ideas we have tried in the home lab, this one was prompted by a question. Someone was asking how to find what AD groups have been put in the local administrators group on workstations via a GPO.
It’s a good question. We have covered auditing who can create, modify, or link GPOs to an OU, but not what already existed. This is important to consider as a prior system administrator might have gotten sloppy, for lack of a better word. Often there is a “just make it work” mindset, and security is not always contemplated.
In an ideal world there should be documentation, but we all know it is not always ideal. So how would one go about finding what pre-existing GPOs are putting AD groups in local admins?
Restricted Groups in Group Policy
This part of Group Policy is rather oddly and counterintuitively named. As I am sure most readers already know, it puts the specified AD group in the specified local group. The path is
Computer Configuration\Policies\Windows Settings\Security Settings\Restricted Groups
Finding if this is set in a GPO somewhere
My initial thought was to simply check the local admins group on the workstations.
$Computers = (Get-ADComputer -Filter * -SearchBase "ou=clients,dc=test,dc=local").Name
ForEach($Computer in $Computers)
{
Invoke-Command -ComputerName $Computer -ScriptBlock {Get-LocalGroupMember -Group "Administrators" | Export-Csv .\LocalAdmins.csv}
}
However this will only show you what groups were added, not what GPO was responsible for it. It also relies on the workstations being powered on and online, not to mention it would be slow to check if there are very many endpoints.
However, there is another way to go about this. What if we checked the SYSVOL directly? After all, GPO configurations are stored there in folders by their GUID.
The thought occurred to me that one could simply ‘grep’ the SYSVOL for the SID of the local admins group and then parse the resulting data to identify the GPO in question, what AD group it applies to, and so on.
One can get the SID for the local groups via:
Get-LocalGroup | Select-Object SID, Name, Description
I have not put any domain groups in the local admins on workstations in the lab, quite the opposite actually. We have demonstrated how to setup things so that the helpdesk can manage the workstations using LAPS and how to delegate them privileges on the OU.
Hence we used ‘Remote Desktop Users’ with SID S-1–5–32–555 in our test case. We use this group in the lab since most of our VMs are in ESXi, including domain workstations used for testing.
The query
This is not the prettiest code, however it will dump the GPO DisplayName and AD group to a text file for any GPOs that put an AD group in the specified local group.
If you are checking local admins then just put $LocalGroup = “S-1–5–32–544”.
#'grep' SYSVOL & find the GPO that puts an AD group in a local group
$ErrorActionPreference = 'SilentlyContinue'
$LocalGroup = "S-1–5–32–555"
$DomainFQDN = (Get-ADDomain).DNSRoot
$GPOfiles = (Get-ChildItem \\$DomainFQDN\SYSVOL\$DomainFQDN\Policies\ -Recurse | Select-String "$LocalGroup" -List | Select Path).Path
ForEach($GPOfile in $GPOfiles)
{
$GPOGUID = ($GPOfile.Split("{")[1]).Split("}")[0]
$RawContent = Get-Content $GPOfile | Select-String -Pattern "$LocalGroup"
#Parse the data & get the AD group's SID from the raw data
$Temp = (($RawContent -split("=") | ConvertFrom-String).P2).Replace('*','')
$string = ($Temp | Out-String) ; $GroupSID = $string -replace '\s',''
$ADGroup = (Get-ADGroup -Filter * -Properties * | Where-Object {$_.SID -like "*$GroupSID*"}).CN
Add-Content -Path C:\Users\Public\Documents\GPOs.txt "This is the AD group that's added to the specifid local group via GPO:"
$ADGroup | Out-File C:\Users\Public\Documents\GPOs.txt -Append
Add-Content -Path C:\Users\Public\Documents\GPOs.txt " "
#Find what OU the GPO is applied to
$OUAppliedTo = (Get-ADOrganizationalUnit -Filter * -Properties * | Where-Object {$_.gPLink -like "*$GPOGUID*"}).Name
Add-Content -Path C:\Users\Public\Documents\GPOs.txt "This is the OU that the GPO is applied to:"
$OUAppliedTo | Out-File C:\Users\Public\Documents\GPOs.txt -Append
Add-Content -Path C:\Users\Public\Documents\GPOs.txt " "
#Show the GPO that is applying this group
Add-Content -Path C:\Users\Public\Documents\GPOs.txt "This is the GPO's Display Name that is adding the group above to the specified local group:"
$root = (Get-ADDomain).DistinguishedName ; (Get-ADObject -Filter * -SearchBase "cn=policies,cn=system,$root" -Properties * | Where-Object {$_.Name -like "*$GPOGUID*"}).DisplayName | Out-File C:\Users\Public\Documents\GPOs.txt -Append
Add-Content -Path C:\Users\Public\Documents\GPOs.txt " "
Add-Content -Path C:\Users\Public\Documents\GPOs.txt " - - Next GPO - - "
Add-Content -Path C:\Users\Public\Documents\GPOs.txt " "
}
One can then simply query any additional information desired, such as all members of the AD group:
(Get-ADGroupMember -Identity “RDP Users” -Recursive).SamAccountName
Or grab all the information related to the GPO:
$root = (Get-ADDomain).DistinguishedName ; Get-ADObject -Filter * -SearchBase “cn=policies,cn=system,$root” -Properties * | Where-Object {$_.DisplayName -eq “Mishky’s RDP Policy for users”}
Or all the computers in the OU that the GPO is applied to:
$DN = (Get-ADOrganizationalUnit -Filter {Name -like “*Clients*”}).DistinguishedName ; (Get-ADComputer -Filter * -SearchBase $DN).SamAccountName
Bear in mind that in a sloppy domain environment there could easily be more than one GPO that is putting an AD group in a local group, hence why we ran a ForEach loop.
Summary
We ran a 15 part series on auditing privileges in AD, aka checking delegation of privileges. We have also covered things like finding stale users, checking for non-compliant users, and of course checking for things like ASREProastable accounts, Kerberoastable accounts, Delegation being enabled on endpoints, and so on.
However we had not considered the case of a prior system administrator putting AD groups in local groups previously. This is an important consideration if one is taking over or auditing a poorly documented environment, so it was an excellent question.
We learned a few things while finding the answer to it. If at nothing else it was a good exercise in parsing data. I figured I’d post my notes in case it helps anyone else, or I need to do this again in the future. That is after all why any of these howtos are on here in the first place.
References
Group Policy Restricted Groups: https://learn.microsoft.com/en-us/troubleshoot/windows-server/group-policy/description-of-group-policy-restricted-groups
Select-String: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/select-string?view=powershell-7.3
Remove whitespace: https://stackoverflow.com/questions/24355760/removing-spaces-from-a-variable-input-using-powershell-4-0
ConvertFrom-String: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/convertfrom-string?view=powershell-5.1