Module 16: Active Directory Persistence
Keeping Domain Access
Domain Group Memberships
Built-in privileged security groups
Domain Admins
Grants full control of the domain, is a member of the built-in administrators group on all domain controllers in a domain, and are administrators on the domain-joined machines
Enterprise Admins
Grants full control of all domains in a forest and is a member of the built-in administrators group on all domain controllers in a forest
Administrators
Grants full control of all the domain controllers in a domain
Group scope definitions
Universal
Can be assigned in any domain in the same forest or trusting forests
Global
Can be assigned in any domain in the same forest or trusting domains or forests
Domain Local
Can only be assigned in the current domain
Listing account management audit policy settings
PS C:\Windows\system32> auditpol /get /category:"Account Management"
System audit policy
Category/Subcategory Setting
Account Management
Computer Account Management Success
Security Group Management Success
Distribution Group Management No Auditing
Application Group Management No Auditing
Other Account Management Events No Auditing
User Account Management Success
There are three conditions that will trigger an alert from this audit policy:
A security group is created, changed, or deleted
A security group has a member added or removed
A security group is changed to a distribution group or vice versa
Event IDs for group membership changes
4728
A member was added to a security-enabled global group
4729
A member was removed from a security-enabled global group
4732
A member was added to a security-enabled local group
4733
A member was removed from a security-enabled local group
4756
A member was added to a security-enabled universal group
4757
A member was removed from a security-enabled universal group
XPath XML filter for all security group changes
<QueryList>
<Query Id="0" Path="Security">
<Select Path="Security">*[System[(EventID=4728 or EventID=4729 or EventID=4732 or EventID=4733 or EventID=4756 or EventID=4757)]]</Select>
</Query>
</QueryList>
XPath XML filter for targeted security group changes
<QueryList>
<Query Id="0" Path="Security">
<Select Path="Security">*[System[(EventID=4728 or EventID=4729 or EventID=4732 or EventID=4733 or EventID=4756 or EventID=4757)]]
And
*[EventData[Data[@Name='TargetUserName'] and (Data='Domain Admins' or Data='Administrators' or Data='Enterprise Admins')]]
</Select>
</Query>
</QueryList>
XPath filter for all security group changes for three named groups
$FilterXML = @'
<QueryList>
<Query Id="0" Path="Security">
<Select Path="Security">*[System[(EventID=4728 or EventID=4729 or EventID=4732 or EventID=4733 or EventID=4756 or EventID=4757)]]
and
*[EventData[Data[@Name='TargetUserName'] and (Data='Administrators' or Data='Domain Admins' or Data='Enterprise Admins')]]
</Select>
</Query>
</QueryList>
'@
$Logs = Get-WinEvent -FilterXml $FilterXML
ForEach ($L in $Logs) {
[xml]$XML = $L.toXml()
$TimeStamp = $XML.Event.System.TimeCreated.SystemTime
$MemberName = $XML.Event.EventData.Data[0].'#text'
$GroupName = $XML.Event.EventData.Data[2].'#text'
$SubjectUserName = $XML.Event.EventData.Data[6].'#text'
[PSCustomObject]@{'TimeStamp' = $TimeStamp; 'MemberName' = $MemberName; 'GroupName' = $GroupName; 'SubjectUserName' = $SubjectUserName; 'ChangeType' = "($EventID) $ChangeType" }
}
Function to provide event descriptions
Function Get-ChangeType ([System.String]$Id) {
Begin {
$ChangeTable = @{
'4728' = '(4728) A member was added to a security-enabled global group.'
'4729' = '(4729) A member was removed from a security-enabled global group.'
'4732' = '(4732) A member was added to a security-enabled local group.'
'4733' = '(4733) A member was removed from a security-enabled local group.'
'4756' = '(4756) A member was added to a security-enabled universal group.'
'4757' = '(4757) A member was removed from a security-enabled universal group.'
}
}
Process {
$Value = $ChangeTable[$Id]
If (!$Value) {
$Value = $Id
}
}
End {
return $Value
}
}
Complete output from the security group audit script
PS C:\Users\offsec\Desktop\Persistence> .\Get-SecurityGroupChanges.ps1
TimeStamp : 2022-01-19T18:46:30.146129500Z
MemberName : CN=John Doe,OU=Staff,DC=corp,DC=com
GroupName : Enterprise Admins
SubjectUserName : Administrator
ChangeType : (4756) A member was added to a security-enabled universal group.
TimeStamp : 2022-01-19T18:42:45.830841000Z
MemberName : cn=dadmin,ou=Staff,DC=corp,DC=com
GroupName : Domain Admins
SubjectUserName : Administrator
ChangeType : (4728) A member was added to a security-enabled global group.
Domain User Modifications
Listing the account management sub-categories
PS C:\Windows\system32> auditpol /get /category:"Account Management"
System audit policy
Category/Subcategory Setting
Account Management
Computer Account Management Success
Security Group Management Success
Distribution Group Management No Auditing
Application Group Management No Auditing
Other Account Management Events No Auditing
User Account Management Success
XPath XML filter for user account management events
<QueryList>
<Query Id="0" Path="Security">
<Select
Path="Security">*[System[Provider[@Name='Microsoft-Windows-Security-Auditing'] and Task = 13824]]
and
*[EventData[Data[@Name='SubjectUserName'] and
(Data='dadmin')]]
</Select>
</Query>
</QueryList>
Function to provide user account management event descriptions
Function Get-ChangeType ([System.String]$EventId) {
Begin {
$ChangeTable = @{
'4720' = “($EventId) A user account was created.”
'4722' = “($EventId) A user account was enabled.”
'4723' = “($EventId) An attempt was made to change an account''s password.”
'4724' = “($EventId) An attempt was made to reset an account''s password.”
'4738' = “($EventId) A user account was changed.”
'4740' = “($EventId) A user account was locked out.”
'4765' = “($EventId) SID History was added to an account.”
'4766' = “($EventId) An attempt to add SID History to an account failed.”
'4767' = “($EventId) A user account was unlocked.”
'4780' = “($EventId) The ACL was set on accounts which are members of administrators groups.”
'4781' = “($EventId) The name of an account was changed.”
'4794' = “($EventId) An attempt was made to set the Directory Services Restore Mode administrator password.”
'4798' = “($EventId) A user''s local group membership was enumerated.”
'5376' = “($EventId) Credential Manager credentials were backed up.”
'5377' = “($EventId) Credential Manager credentials were restored from a backup.”
'5379' = 'Credential Manager credentials were read'
}
}
Process {
$Value = $ChangeTable[$EventId]
If (!$Value) {
$Value = $EventId
}
}
End {
return $Value
}
}
Running the user change audit script
PS C:\Users\offsec\Desktop\Persistence> .\Get-UserChanges.ps1
TimeStamp SubjectUserName TargetUserName ChangeType
--------- --------------- -------------- ----------
2022-03-09T19:57:30.859931700Z dadmin notahacker (4724) An attempt was made to reset an account's passw...
2022-03-09T19:57:30.859864400Z dadmin notahacker (4738) A user account was changed.
...
Golden Tickets
Typical kerberos ticket
PS C:\Users\offsec.CORP> klist
Current LogonId is 0:0xf5cad
Cached Tickets: (6)
#0> Client: offsec @ CORP.COM
Server: krbtgt/CORP.COM @ CORP.COM
KerbTicket Encryption Type: AES-256-CTS-HMAC-SHA1-96
Ticket Flags 0x40e10000 -> forwardable renewable initial pre_authent name_canonicalize
Start Time: 3/9/2022 12:30:03 (local)
End Time: 3/9/2022 22:30:03 (local)
Renew Time: 3/16/2022 12:30:03 (local)
Session Key Type: AES-256-CTS-HMAC-SHA1-96
Cache Flags: 0x1 -> PRIMARY
Kdc Called: DC01
...
Function to retrieve key values from the GPOReport
Function Get-KerberosSettings {
Begin {
[xml]$XML = Get-GPOReport -Name 'Default Domain Policy' -ReportType xml
}
Process {
$Kerberos = $XML.GPO.Computer.ExtensionData.Extension.Account | Where-Object { $_.Type -eq 'Kerberos' }
}
End {
return [PSCustomObject]@{'MaxClockSkew' = $Kerberos[0].SettingNumber; 'MaxRenewAge' = $Kerberos[1].SettingNumber;
'MaxServiceAge' = $Kerberos[2].SettingNumber; 'MaxTicketAge' = $Kerberos[3].SettingNumber; 'TicketValidateClient' = $Kerberos[4].SettingBoolean
}
}
}
Executing the Get-Kerberos Settings function
PS C:\Users\offsec\Desktop\Persistence> . .\Get-KerberosSettings.ps1
PS C:\Users\offsec\Desktop\Persistence> Get-KerberosSettings
MaxClockSkew : 5
MaxRenewAge : 7
MaxServiceAge : 600
MaxTicketAge : 10
TicketValidateClient : true
A cached golden ticket
PS C:\Users\offsec.CORP\Desktop\Persistence> klist
Current LogonId is 0:0xa54c6
Cached Tickets: (1)
#0> Client: dadmin @ corp.com
Server: krbtgt/corp.com @ corp.com
KerbTicket Encryption Type: RSADSI RC4-HMAC(NT)
Ticket Flags 0x40e00000 -> forwardable renewable initial pre_authent
Start Time: 3/9/2022 12:39:54 (local)
End Time: 3/6/2032 12:39:54 (local)
Renew Time: 3/6/2032 12:39:54 (local)
Session Key Type: RSADSI RC4-HMAC(NT)
Cache Flags: 0x1 -> PRIMARY
Kdc Called:
Kerberos tickets are assigned to logon sessions, identified by logon IDs. Executing klist
without any parameters only displays cached tickets for the current session
Running the klist command
PS C:\Users\offsec\Desktop\Persistence> klist
Current LogonId is 0:0x1fa47a
...
Running the klist sessions command
PS C:\Users\offsec.CORP\Desktop\Persistence> klist sessions
Current LogonId is 0:0xa54c6
[0] Session 2 0:0xa5996 CORP\offsec Negotiate:RemoteInteractive
[1] Session 2 0:0xa54c6 CORP\offsec Kerberos:RemoteInteractive
[2] Session 2 0:0xa06a5 Window Manager\DWM-2 Negotiate:Interactive
...
[12] Session 0 0:0x3e7 CORP\CLIENT03$ Negotiate:(0)
Runnin the klist command with a targetd logon ID
PS C:\Users\offsec.CORP\Desktop\Persistence> klist -li 0x3e7
Current LogonId is 0:0xa54c6
Targeted LogonId is 0:0x3e7
Cached Tickets: (6)
#0> Client: client03$ @ CORP.COM
Server: krbtgt/CORP.COM @ CORP.COM
KerbTicket Encryption Type: AES-256-CTS-HMAC-SHA1-96
Ticket Flags 0x60a10000 -> forwardable forwarded renewable pre_authent name_canonicalize
Start Time: 3/9/2022 12:19:05 (local)
End Time: 3/9/2022 22:19:02 (local)
Renew Time: 3/16/2022 12:19:02 (local)
Session Key Type: AES-256-CTS-HMAC-SHA1-96
Cache Flags: 0x2 -> DELEGATION
Kdc Called: dc01.corp.com
...
Unfortunately, the klist command doesn't offer a method to retrieve cached tickets for every session on the computer in one go.
PowerShell one-liner to dump all cached tickets
PS C:\Users\offsec.CORP\Desktop\Persistence> (klist sessions 2>&1) | ? {$_ -like '* Session*'} | % {(($_ -split ' ')[3]).substring(2)} | ForEach-Object {klist -li $_}
Function to provide all logon IDs
Function Get-LogonIds {
Begin {
$Klist = klist sessions
}
Process {
$Sessions = $Klist | ? { $_ -like '* Session*' } | % { (($_ -split ' ')[3]).substring(2) }
}
End {
return $Sessions
}
}
Function to retrieve session tickets
Function Get-Tickets {
[cmdletbinding()]
param (
[parameter(mandatory = $false, ValueFromPipeline = $true)]
[System.String]$LogonId
)
Begin {
$CachedTickets = @()
$Klist = klist
$Current = ((($klist) -split 'Current LogonId is')[2] -split ':')[1]
}
Process {
try {
if ($LogonId -eq $Current -or $LogonId -eq '') {
$Klist = klist
$LogonId = $Current
}
else {
$Klist = klist -li $LogonId
}
$Tickets = 5..$Klist.count | ForEach-Object { $Klist[$_] } | Where-Object { $_ }
if ($Klist -notcontains 'Cached Tickets: (0)') {
0..$(($Tickets | Select-String "^#\d>").Count - 1) | ForEach-Object {
$Index = $_ * 10
$Properties = [ordered]@{
'LogonId' = $LogonId
'Ticket' = $_
'Client' = $($Tickets[0 + $Index] -split ':')[1].Trim()
'Server' = $($Tickets[1 + $Index] -split ':')[1].Trim()
'EncryptionType' = $($Tickets[2 + $Index] -split ':')[1].Trim()
'TicketFlags' = $($Tickets[3 + $Index] -split 'Ticket Flags')[1].Trim()
'StartTime' = $($Tickets[4 + $Index] -split 'Start Time:')[1].Trim()
'EndTime' = $($Tickets[5 + $Index] -split 'End Time:')[1].Trim()
'RenewTime' = $($Tickets[6 + $Index] -split 'Renew Time:')[1].Trim()
'SessionKeyType' = $($Tickets[7 + $Index] -split ':')[1].Trim()
'CacheFlags' = $($Tickets[8 + $Index] -split ':')[1].Trim()
'KdcCalled' = $($Tickets[9 + $Index] -split ':')[1].Trim()
}
if ($Properties) {
$CachedTickets += New-Object -TypeName PSObject -Property $Properties
}
}
}
}
catch {
if ($_ -like "*Error calling API*") {
$_ | Out-null
}
}
}
End {
return $CachedTickets
}
}
Running the Get-LogonIds and Get-Tickets together
PS C:\Users\offsec.CORP\Desktop\Persistence> . .\Get-LogonIds.ps1
PS C:\Users\offsec.CORP\Desktop\Persistence> . .\Get-Tickets.ps1
PS C:\Users\offsec.CORP\Desktop\Persistence> Get-LogonIds | Get-Tickets
LogonId : 0xa54c6
Ticket : 0
Client : dadmin @ corp.com
Server : krbtgt/corp.com @ corp.com
EncryptionType : RSADSI RC4-HMAC(NT)
TicketFlags : 0x40e00000 -> forwardable renewable initial pre_authent
StartTime : 3/9/2022 12:39:54 (local)
EndTime : 3/6/2032 12:39:54 (local)
RenewTime : 3/6/2032 12:39:54 (local)
SessionKeyType : RSADSI RC4-HMAC(NT)
CacheFlags : 0x1 -> PRIMARY
KdcCalled :
...
Retrieving ticket time values
PS C:\Users\offsec.CORP\Desktop\Persistence> Get-LogonIds | Get-Tickets | Select LogonId,StartTime,EndTime | Sort EndTime
LogonId StartTime EndTime
------- --------- -------
0xa54c6 3/9/2022 12:39:54 (local) 3/6/2032 12:39:54 (local)
0x3e7 3/9/2022 12:19:05 (local) 3/9/2022 22:19:02 (local)
0x3e7 3/9/2022 12:19:02 (local) 3/9/2022 22:19:02 (local)
...
Function to analyze ticket values
Function Invoke-GoldenSweep {
[cmdletbinding()]
param (
[parameter(mandatory = $true, ValueFromPipeline = $true)]
$Ticket
)
Process {
# Time Beacons
$StartTime = ($Ticket.StartTime -split ' ')[0]
$EndTime = ($Ticket.EndTime -split ' ')[0]
$RenewTime = ($Ticket.RenewTime -split ' ')[0]
if ((New-TimeSpan -Start $StartTime -End $EndTime).Days -gt 10) {
$Flagged = $Ticket
}
if ($RenewTime -ne 0) {
if ((New-TimeSpan -Start $StartTime -End $RenewTime).Days -gt 7) {
$Flagged = $Ticket
}
}
}
End {
return $Flagged
}
}
Running a golden ticket discovery chain
PS C:\Users\offsec.CORP\Desktop\Persistence> . .\Invoke-GoldenSweep.ps1
PS C:\Users\offsec.CORP\Desktop\Persistence> Get-LogonIds | Get-Tickets | Invoke-GoldenSweep
LogonId : 0xa54c6
Ticket : 0
Client : dadmin @ corp.com
Server : krbtgt/corp.com @ corp.com
EncryptionType : RSADSI RC4-HMAC(NT)
TicketFlags : 0x40e00000 -> forwardable renewable initial pre_authent
StartTime : 3/9/2022 12:39:54 (local)
EndTime : 3/6/2032 12:39:54 (local)
RenewTime : 3/6/2032 12:39:54 (local)
SessionKeyType : RSADSI RC4-HMAC(NT)
CacheFlags : 0x1 -> PRIMARY
KdcCalled :
Logic to detect the RC4 encryption type value
# Encryption Beacons
$EncryptionType = $Ticket.EncryptionType
If ($EncryptionType -eq ‘RSADSA RC4-HMAC(NT)’) {
$Flagged = $Ticket
}
Last updated