Lesson 3 of 3 · 30 min read

Full AD Remediation — krbtgt Reset Sequence, Credential Rotation, and Eviction

Full AD remediation after domain compromise is one of the most operationally complex IR activities — done wrong, it either leaves the attacker behind or takes the organization offline.


Pre-Remediation Assessment

Before touching anything, establish the full scope:

# 1. Document the current state of all privileged groups
$groups = @('Domain Admins', 'Enterprise Admins', 'Schema Admins', 
            'Account Operators', 'Backup Operators', 'Server Operators')
$groups | ForEach-Object {
  $g = $_
  Get-ADGroupMember -Identity $g -Recursive |
    Select-Object @{N='Group';E={$g}}, SamAccountName, ObjectClass
} | Export-Csv -Path "C:\IR\privileged-groups-$(Get-Date -Format yyyyMMdd).csv" -NoTypeInformation

# 2. Document AdminSDHolder permissions (baseline vs. current)
$adminSDHolder = Get-ADObject -Identity "CN=AdminSDHolder,CN=System,DC=company,DC=local" `
  -Properties nTSecurityDescriptor
$adminSDHolder.nTSecurityDescriptor.Access | 
  Export-Csv -Path "C:\IR\adminsdholder-acl.csv" -NoTypeInformation

# 3. Find attacker-created accounts (accounts created in attack window)
$attack_start = Get-Date "2024-09-01"
Get-ADUser -Filter {Created -gt $attack_start} `
  -Properties Created, MemberOf |
  Select-Object SamAccountName, Created, @{N='Groups';E={$_.MemberOf -join '; '}} |
  Format-Table -AutoSize

# 4. Find all accounts added to privileged groups during attack window
Get-WinEvent -FilterHashtable @{
  LogName = 'Security'; Id = 4728; StartTime = $attack_start
} | ForEach-Object {
  $xml = [xml]$_.ToXml()
  [PSCustomObject]@{
    Time = $_.TimeCreated
    Member = ($xml.Event.EventData.Data | Where-Object {$_.Name -eq 'MemberName'}).'#text'
    Group = ($xml.Event.EventData.Data | Where-Object {$_.Name -eq 'GroupName'}).'#text'
    AddedBy = ($xml.Event.EventData.Data | Where-Object {$_.Name -eq 'SubjectUserName'}).'#text'
  }
}

krbtgt Double Reset Sequence

# Step 1: Record current krbtgt key version (for verification)
Get-ADUser krbtgt -Properties msDS-KeyVersionNumber |
  Select-Object SamAccountName, msDS-KeyVersionNumber

# Step 2: First reset — generate new key, OLD key moves to 'previous'
Set-ADAccountPassword -Identity krbtgt `
  -NewPassword (ConvertTo-SecureString -AsPlainText `
    ([System.Web.Security.Membership]::GeneratePassword(30, 5)) -Force) `
  -Reset

# Verify key version incremented
Get-ADUser krbtgt -Properties msDS-KeyVersionNumber |
  Select-Object SamAccountName, msDS-KeyVersionNumber

# Step 3: Force replication to all DCs immediately
# (ensures all DCs have the new krbtgt key, not just the PDC emulator)
repadmin /syncall /AdeP

# Monitor for replication errors
repadmin /showrepl

# Step 4: WAIT 10 HOURS (MaxTicketAge — allow all valid tickets to expire naturally)
# Set a reminder: second reset must happen 10+ hours after first reset

# Step 5: Second reset (10+ hours later) — original compromised key is now gone
Set-ADAccountPassword -Identity krbtgt `
  -NewPassword (ConvertTo-SecureString -AsPlainText `
    ([System.Web.Security.Membership]::GeneratePassword(30, 5)) -Force) `
  -Reset

# Force replication again
repadmin /syncall /AdeP

# Step 6: Verify — any Golden Tickets created before Step 2 are now invalid
Get-ADUser krbtgt -Properties msDS-KeyVersionNumber, PasswordLastSet |
  Select-Object SamAccountName, msDS-KeyVersionNumber, PasswordLastSet

Full Credential Rotation

# Rotate Domain Admin accounts (run for each compromised admin)
$domain_admins = Get-ADGroupMember -Identity "Domain Admins" -Recursive |
  Where-Object {$_.objectClass -eq 'user'}

$domain_admins | ForEach-Object {
  $new_pwd = [System.Web.Security.Membership]::GeneratePassword(30, 8)
  Set-ADAccountPassword -Identity $_.SamAccountName `
    -NewPassword (ConvertTo-SecureString $new_pwd -AsPlainText -Force) `
    -Reset
  Set-ADUser -Identity $_.SamAccountName `
    -ChangePasswordAtLogon $true
  Write-Host "Rotated: $($_.SamAccountName)"
}

# Rotate service accounts — schedule during maintenance window
$svc_accounts = Get-ADUser -Filter {ServicePrincipalName -ne "$null"} `
  -Properties ServicePrincipalName

$svc_accounts | ForEach-Object {
  # CAUTION: Test each service before rotating — rotation breaks the service
  Write-Host "Pending rotation: $($_.SamAccountName) | SPN: $($_.ServicePrincipalName)"
}

# Rotate local administrator accounts using LAPS or manual rotation
# For non-LAPS environments:
Invoke-Command -ComputerName (Get-ADComputer -Filter * | Select-Object -ExpandProperty Name) `
  -ScriptBlock {
    $new_pwd = [System.Web.Security.Membership]::GeneratePassword(25, 6)
    net user administrator $new_pwd
    Write-Host "$env:COMPUTERNAME : rotated"
  } -ErrorAction SilentlyContinue

Post-Remediation Validation

# Validate no backdoor accounts remain
Get-ADUser -Filter {Enabled -eq $true} `
  -Properties Created, LastLogonDate, MemberOf |
  Where-Object {$_.Created -gt (Get-Date "2024-09-01")} |
  Select-Object SamAccountName, Created, LastLogonDate |
  Format-Table -AutoSize

# Validate no unauthorized DCSync rights
Get-ObjectAcl -DistinguishedName "DC=company,DC=local" -ResolveGUIDs |
  Where-Object {$_.ActiveDirectoryRights -match "DS-Replication-Get-Changes"} |
  Select-Object IdentityReference, ActiveDirectoryRights |
  Where-Object {$_.IdentityReference -notmatch "Domain Controllers|Enterprise Domain Controllers|Administrators"}

# Validate AdminSDHolder is clean
$adminSDHolder = Get-ADObject `
  -Identity "CN=AdminSDHolder,CN=System,DC=company,DC=local" `
  -Properties nTSecurityDescriptor
$adminSDHolder.nTSecurityDescriptor.Access |
  Where-Object {$_.IdentityReference -notmatch "SYSTEM|Domain Admins|Enterprise Admins|Administrators"} |
  Select-Object IdentityReference, ActiveDirectoryRights

# Monitor for re-compromise — set SIEM alerts for:
# 1. Any 4662 with Replication-Get-Changes from non-DC accounts
# 2. Any 4769 with RC4 encryption (0x17)
# 3. Any new accounts added to Domain Admins (4728 on Group = Domain Admins)
# 4. Any Sysmon Event 10 against lsass.exe