TL;DR how to check privilege delegation in AD against a whitelist. You can jump to the code here if this explanation is too long for you.
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
This idea is mostly for ‘Misconfiguration Debt’ in Windows domain environments. Let’s say for example that you recently took over a domain from a previous administrator who was a bit sloppy. Maybe they didn’t document things very well, or at all, maybe they gave out rights with little forethought or planning, maybe you simply have no idea what they did.
Here at test.local we do not claim that we are the first to have this idea. We probably simply re-invented the wheel, there are lots of more user friendly free tools out there (BloodHound, BlueHound, Ping Castle, Purple Knight, etc), however we wanted to make a proof of concept. We also learned a lot along the way. That’s the whole point of doing lab projects after all.
The Blue Team Auditor
We put a simple function together that accepts a CSV file as input.
One puts the OUs they wish to check in the top row and then puts the groups that should have been delegated ‘Dangerous Rights’ on each OU in the column below it.
Then simply
Get-ACLAudit .\TestII.csv
The check
- pops hot on any non-whitelisted users or groups
- shows the AD object and ACE in question
- outputs the results to a text file, named by OU
- resolves the GUIDs to the right
We gave it a couple things to find and made sure that at least one of them had a specific GUID. (Details of how to do this are in the Set-Acl cheatsheet.)
Will Schroeder’s Invoke-ACLScanner, part of PowerView, has a really nice feature that resolves GUIDs. We considered using it, as essentially our check uses ‘Get-Acl | Where-Object {$_.X etc etc}’ and one can also do ‘Invoke-ACLScanner -ResolveGUIDs | Where-Object {$_.X etc etc}’. However as we saw previously in this series PowerView trips Defender and requires the AMSI bypass to run.
We wanted to use builtin Microsoft tools, plus I also just figured it would make a good educational project.
Important Notes
- This function relies on the Active Directory PowerShell module, as does almost everything we have posted in this series.
- The code whitelists many default groups in AD, like Domain Admins for example. It also whitelists some users/groups that exist in our hybrid AD environment like the service account used by Azure AD Connect. These are stored in the variable “$Safe_Users”.
- We also whitelisted the names of our DCs and a few other random entities that have rights on builtin containers. We originally built the core of this tool back in Part XI of this series, and we were running this check against the entire domain, not just specific OUs.
Important: If you use this then you will want to tweak that variable’s contents for your own environment!
- Additionally, as we have stated earlier throughout this howto, we don’t really see a reason to delegate quasi/sorta/moderately but not highly trusted groups the rights to modify attributes on OUs like the address, for example. If you have a legitimate business need to do this for groups that are not allowed to link GPOs, then bear in mind that this code will pop hot on that group.
The code
Function Get-ACLAudit {
param (
[parameter(Mandatory=$True)]
[ValidateNotNullOrEmpty()]$File
)
Try {
$CurrentPath = (Get-Location).Path
$Sheet = Import-Csv -Path $File
$OUs = (Get-Content $File | Select-Object -First 1).Split(",")
Import-Module ActiveDirectory
Set-Location AD:
$ADRoot = (Get-ADDomain).DistinguishedName
$DomainSID = (Get-ADDomain).DomainSID.Value
$Safe_Users = "Domain Admins|Enterprise Admins|BUILTIN\\Administrators|NT AUTHORITY\\SYSTEM|$env:userdomain\\CERT Publishers|$env:userdomain\\Administrator|BUILTIN\\Account Operators|BUILTIN\\Terminal Server License Servers|NT AUTHORITY\\SELF|NT AUTHORITY\\ENTERPRISE DOMAIN CONTROLLERS|$env:userdomain\\Enterprise Read-only Domain Controllers|CREATOR OWNER|$env:userdomain\\DNSAdmins|$env:userdomain\\Key Admins|$env:userdomain\\Enterprise Key Admins|$env:userdomain\\Domain Computers|$env:userdomain\\Domain Controllers|$env:userdomain\\<Azure AD Connect service account>|$env:userdomain\\BackupDC*|$env:userdomain\\TestDC|$DomainSID-519|S-1-5-32-548|$env:userdomain\\<Azure AD Cloud Sync gMSA>"
$DangerousRights = "GenericAll|WriteDACL|WriteOwner|GenericWrite|WriteProperty|Self"
$DangerousGUIDs = "1131f6aa-9c07-11d1-f79f-00c04fc2dcd2|1131f6ad-9c07-11d1-f79f-00c04fc2dcd2|00000000-0000-0000-0000-000000000000|00299570-246d-11d0-a768-00aa006e0529"
$FishyGUIDs = "ab721a56-1e2f-11d0-9819-00aa0040529b|ab721a54-1e2f-11d0-9819-00aa0040529b"
ForEach($OU in $OUs)
{
$OU_DN = (Get-ADOrganizationalUnit -Filter * -Properties * | Where-Object {$_.Name -eq "$OU"}).DistinguishedName
$Accounts = $Sheet.$OU
$MyGroups = $Accounts.ForEach{[regex]::Escape($_)} -join '|'
$SafeGroups = $MyGroups.Replace('\','')
While($SafeGroups.Substring($SafeGroups.Length-1) -eq "|"){$SafeGroups = $SafeGroups.Substring(0,$SafeGroups.Length-1)}
$ADCS_Objects = (Get-ADObject -Filter * -SearchBase "$OU_DN").DistinguishedName
ForEach ($object in $ADCS_Objects)
{
$BadACE = (Get-Acl $object -ErrorAction SilentlyContinue).Access | Where-Object {(($_.IdentityReference -notmatch $Safe_Users) -and ($_.IdentityReference -notmatch $SafeGroups)) -and (($_.ActiveDirectoryRights -match $DangerousRights) -or ((($_.ActiveDirectoryRights -like "*ExtendedRight*") -and (($_.ObjectType -match $DangerousGUIDs) -or ($_.ObjectType -match $FishyGUIDs))))) -and ($_.AccessControlType -eq "Allow")}
If ($BadACE)
{
Write-Host "Object: $object" -ForegroundColor Red
$BadACE
$object | Out-File "$CurrentPath\$OU Offenders.txt" -Append
$BadACE | Out-File "$CurrentPath\$OU Offenders.txt" -Append
If($BadACE.ObjectType.Guid -ne "00000000-0000-0000-0000-000000000000")
{$GUID = $BadACE.ObjectType.Guid
Get-Content "$CurrentPath\GUID_List.txt" | Select-String "$GUID" | Out-File "$CurrentPath\$OU Offenders.txt" -Append}
If($BadACE.InheritedObjectType.Guid -ne "00000000-0000-0000-0000-000000000000")
{$GUID = $BadACE.ObjectType.Guid
Get-Content "$CurrentPath\GUID_List.txt" | Select-String "$GUID" | Out-File "$CurrentPath\$OU Offenders.txt" -Append}
} #Close If ($BadACE)
} #Close ForEach ($object in $ADCS_Objects)
} #Close ForEach($OUs in $OUs)
} #Close the Try
Catch {Write-Host "Error, check the file name & path"}
Write-Host "Anything found was written to text files, named by OU, in your present working directory."
Set-Location $CurrentPath
} #Close the function
Function Get-GUID {
param (
[parameter(Mandatory=$False)]
[ValidateNotNullOrEmpty()]$GUID
)
Get-Content "$CurrentPath\GUID_List.txt" | Select-String "$GUID"
Write-Host " "
Write-Host "If you need more information, please see https://medium.com/@happycamper84/dangerous-rights-cheatsheet-33e002660c1d and Ctrl + F the GUID"
}
Medium can be a bit dicey when it comes to quotations marks and dashes, but overall it is not a bad platform for technical writing. Regardless, the code is available on Mishky’s GitHub here.
Ownership
This checks ACLs, but what if the current owner is screwy?
I’m sure that someone somewhere out there had a legitimate business reason to change the default owner of users, groups, OUs, etc. If that’s you, then ignore this part.
A quick & dirty method of checking ownership against a whitelist:
Import-Module ActiveDirectory
Set-Location AD:
$ADRoot = (Get-ADDomain).DistinguishedName
$ADCS_Objects = (Get-ADObject -Filter * -SearchBase $ADRoot).DistinguishedName
$Safe_Users = "Domain Admins|BUILTIN\\Administrators|NT AUTHORITY\\SYSTEM"
ForEach ($object in $ADCS_Objects)
{
$BadOwner = (Get-Acl $object -ErrorAction SilentlyContinue).Owner -notmatch $Safe_Users
If ($BadOwner)
{
Write-Host "Object: $object" -ForegroundColor Red
(Get-Acl $object -ErrorAction SilentlyContinue).Owner
}
}
Summary
I took the CRTP exam back around Nov 2021 and almost failed it because I didn’t know about ACLs in AD at the time. I got stuck early on during the 24 hour hands on exam, went back through the lab PDF, and realized I should use Will Schroeder’s Invoke-ACLScanner.
We have since done a deep dive on the topic, learned a lot, created our own versions of Red Team and Blue Team checks for ‘Dangerous Rights’ … and at the end of it I really have to say that BloodHound and PowerView simply do a really good job at this ‘out of the box’.
In closing, if you want to gain a deeper understanding of the topic then do what we did.
If you just want to check for ‘Dangerous Rights’ then use BloodHound and/or PowerView.
References
PowerView: https://github.com/PowerShellMafia/PowerSploit/blob/dev/Recon/PowerView.ps1
Filter & remove the last letter of a string: https://devblogs.microsoft.com/scripting/two-simple-powershell-methods-to-remove-the-last-letter-of-a-string/
Well known SIDs: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/81d92bba-d22b-4a8c-908a-554ab29148ab