Identity Containment
Disabling compromised accounts, resetting passwords in the right order, revoking tokens and sessions, rotating service accounts, and why credential resets during an active adversary incident are irreversible.
You've confirmed that the attacker compromised a domain admin account. The instinct is to immediately disable that account. But the attacker has been in the environment for 21 days — they've almost certainly created additional accounts, added themselves to groups, established OAuth token persistence, and created service account credentials you haven't found yet. If you disable the one account you know about, the attacker switches to one of their six other backdoor accounts and goes quiet for a few days while you think you've evicted them. Identity containment requires understanding every foothold before resetting anything — then resetting everything simultaneously.
Account Audit Before Containment
Before disabling or resetting any credential, audit the full identity landscape. The goal is to find every account the attacker touched, created, or could use.
# Audit AD accounts created since a given date (attacker may have created new accounts)
$compromiseDate = Get-Date "2026-08-01" # adjust to approximate compromise start
# Accounts created after the estimated compromise date
Get-ADUser -Filter * -Properties Created,LastLogonDate,MemberOf |
Where-Object { $_.Created -gt $compromiseDate } |
Select-Object SamAccountName, Created, LastLogonDate,
@{N="Groups"; E={ ($_.MemberOf | Get-ADGroup | Select -Expand Name) -join ", " }} |
Sort-Object Created |
Format-Table -AutoSize
# Accounts with admin group membership — compare against known-good baseline
$adminGroups = @("Domain Admins","Enterprise Admins","Schema Admins",
"Administrators","Account Operators","Backup Operators")
foreach ($group in $adminGroups) {
Write-Host "`n== $group ==" -ForegroundColor Cyan
Get-ADGroupMember $group -Recursive | Select-Object SamAccountName, objectClass
}
# Recently modified accounts — attacker may have added to privileged groups
Get-ADUser -Filter * -Properties Modified,WhenChanged |
Where-Object { $_.WhenChanged -gt $compromiseDate } |
Select-Object SamAccountName, WhenChanged |
Sort-Object WhenChanged -Descending |
Format-Table
# Azure AD / Entra ID audit — requires AzureAD or Microsoft.Graph module
# Install-Module Microsoft.Graph -Scope CurrentUser
Connect-MgGraph -Scopes "User.Read.All","AuditLog.Read.All","Directory.Read.All"
# Users created recently
$sinceDate = "2026-08-01T00:00:00Z"
Get-MgUser -Filter "createdDateTime gt $sinceDate" |
Select-Object DisplayName, UserPrincipalName, CreatedDateTime |
Sort-Object CreatedDateTime
# Users with Global Administrator role
$gaRole = Get-MgDirectoryRole | Where-Object { $_.DisplayName -eq "Global Administrator" }
Get-MgDirectoryRoleMember -DirectoryRoleId $gaRole.Id |
Select-Object -ExpandProperty AdditionalProperties |
ForEach-Object { [PSCustomObject]@{ UPN = $_["userPrincipalName"]; DisplayName = $_["displayName"] } }
# Recent risky sign-ins (requires Azure AD P2 license)
Get-MgAuditLogRiskyUser | Where-Object { $_.RiskLastUpdatedDateTime -gt $sinceDate } |
Select-Object UserDisplayName, UserPrincipalName, RiskLevel, RiskState
The Disable Sequence: Order Matters
Reset credentials in an order that denies the attacker access without triggering their detection alarms prematurely.
Identity Containment Sequence
═══════════════════════════════════════════════════════════════════
Phase 1 — Silent audit (no attacker impact):
├── Enumerate all accounts accessed or created since compromise
├── Identify all service accounts and their dependencies
├── Identify Kerberoastable / AS-REP roastable accounts
├── Audit OAuth app grants and refresh tokens
└── Identify Conditional Access policy gaps
Phase 2 — Simultaneous reset (maximum disruption to attacker):
T=0:00 Disable ALL identified compromised accounts simultaneously
T=0:00 Reset ALL identified compromised passwords simultaneously
T=0:01 Revoke all Azure AD/Entra refresh tokens (single command)
T=0:02 Revoke Kerberos TGTs (krbtgt password — double reset)
T=0:05 Disable all unknown/unrecognized admin accounts
T=0:10 Reset service account passwords in dependency order
T=0:15 Rotate API keys for affected services
Phase 3 — Block re-entry:
├── Enforce MFA on all privileged accounts (break glass if needed)
├── Block legacy auth protocols (no NTLM/basic auth for admin accounts)
├── Enable Conditional Access — block from known attacker geolocations
└── Monitor for new account creation and group membership changes
The worst sequence is: disable one account → wait → disable another.
The attacker pivots between each reset. All resets must be simultaneous.
Active Directory Credential Reset
# Bulk disable and reset compromised AD accounts
# Run this as Domain Admin from a clean, non-compromised workstation
$compromisedAccounts = @("jsmith","mwilliams","svc_backup","svc_deploy")
foreach ($account in $compromisedAccounts) {
# Disable the account
Disable-ADAccount -Identity $account
# Reset password to a complex random value
$newPassword = [System.Web.Security.Membership]::GeneratePassword(24, 4)
Set-ADAccountPassword -Identity $account `
-Reset -NewPassword (ConvertTo-SecureString $newPassword -AsPlainText -Force)
# Force password change at next login
Set-ADUser -Identity $account -ChangePasswordAtLogon $true
# Record the reset in the case log
Write-Host "$(Get-Date -Format 'HH:mm:ss') Reset: $account"
}
# Also: revoke all active sessions by forcing logoff on DCs
# (kicks existing sessions that may still use old credentials via Kerberos)
Invoke-Command -ComputerName (Get-ADDomainController -Filter *).HostName -ScriptBlock {
quser 2>$null | Where-Object { $_ -match "jsmith|mwilliams" } |
ForEach-Object {
$sessionId = ($_ -split "\s+")[3]
logoff $sessionId /server:$env:COMPUTERNAME 2>$null
}
}
krbtgt Reset — The Kerberos Nuclear Option
If the attacker had domain admin access, assume they extracted the krbtgt hash for Golden Ticket creation. A krbtgt password reset invalidates all existing Kerberos tickets — including legitimate ones. This causes a brief (~1 hour) authentication disruption while clients obtain new tickets.
Active Directory keeps the previous krbtgt password in addition to the current one so that tickets issued before a rotation remain valid briefly. After the first reset, the attacker's Golden Tickets are invalid for new TGTs but may still work for service tickets (ST) using the old key. The second reset — performed at least 10 hours after the first (the maximum Kerberos ticket lifetime) — removes the previous password entirely and invalidates any remaining tickets created with the pre-reset key. If you only reset once, the attacker may still have a viable path for up to 10 hours.
# krbtgt reset — WILL DISRUPT AUTHENTICATION for active sessions
# Microsoft recommends using the Reset-KrbtgtKeyInteractive script from
# https://github.com/microsoft/New-KrbtgtKeys.ps1 for safer execution
# Manual reset (simpler, use only in emergency):
Set-ADAccountPassword -Identity "krbtgt" `
-Reset -NewPassword (ConvertTo-SecureString `
([System.Web.Security.Membership]::GeneratePassword(32,6)) `
-AsPlainText -Force)
Write-Host "krbtgt reset 1 complete at $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss UTC' -AsUTC)"
Write-Host "Wait at least 10 hours, then repeat this reset"
# After reset, users will need to re-authenticate to get new TGTs
# Kerberos-reliant services may need to be restarted to flush cached tickets
# Monitor for Kerberos error events: 4769 (KRB_AP_ERR_MODIFIED), 4771 (pre-auth failures)
Azure AD / Entra: Revoke All Sessions
# Revoke all refresh tokens for a user (invalidates all active sessions)
# Requires AzureAD module: Install-Module AzureAD
Connect-AzureAD
# Single user
Revoke-AzureADUserAllRefreshToken -ObjectId "jsmith@corp.com"
# All users at once (nuclear option — use carefully)
Get-AzureADUser -All $true | ForEach-Object {
Revoke-AzureADUserAllRefreshToken -ObjectId $_.ObjectId
}
# Via Microsoft Graph (modern approach):
# POST https://graph.microsoft.com/v1.0/users/{id}/revokeSignInSessions
# After token revocation: users will be prompted to re-authenticate
# This forces new Conditional Access policy evaluation on re-auth
Service Account Rotation
Service accounts are high-value targets because they often have persistent, non-expiring passwords and broad permissions. Rotating them requires understanding every service that depends on each account before touching the password.
| Service account type | Risk if compromised | Reset complexity | Rotation approach |
|---|---|---|---|
| Scheduled task / Windows service logon | Persistent code execution, credential relay | Medium — must update each service definition | Find all services using the account via Get-WmiObject Win32_Service, update password in each, reset AD password |
| SQL Server service account | All data in SQL is accessible; xp_cmdshell for lateral movement | Medium — SQL Server Configuration Manager must be used, not just AD reset | SQL Server Configuration Manager for service credentials, then AD reset |
| Application pool identity (IIS) | IIS sites running as this account could be used to drop web shells | Low — IIS Manager or web.config | Update in IIS Manager, then reset AD password |
| gMSA (Group Managed Service Account) | Lower risk by design — AD auto-rotates the password | Low — no manual rotation needed typically | Reset the AD object to force immediate KDS root key regeneration |
# Find every service using a given service account across the domain
# Must run as Domain Admin; queries all domain computers
$serviceAccount = "CORP\svc_deploy"
$computers = Get-ADComputer -Filter * | Select-Object -ExpandProperty Name
Invoke-Command -ComputerName $computers -ScriptBlock {
param($acct)
Get-WmiObject Win32_Service |
Where-Object { $_.StartName -like "*$acct*" } |
Select-Object @{N="Computer";E={$env:COMPUTERNAME}},
Name, DisplayName, StartName, State
} -ArgumentList $serviceAccount -ErrorAction SilentlyContinue |
Where-Object { $_ } |
Sort-Object Computer, Name |
Format-Table -AutoSize
Q & A
Q: You reset a compromised domain admin account. 20 minutes later the attacker is still active. Why?
Several possibilities: (1) The attacker had additional accounts you didn't identify — Golden Ticket using the pre-reset krbtgt hash, a second compromised admin account, or a newly created backdoor account. (2) The attacker had cached credentials on machines they already accessed — a password reset doesn't invalidate Kerberos tickets already issued under the old password until those tickets expire (up to 10 hours by default). (3) The attacker has a non-credential-based persistence mechanism — a scheduled task, service, WMI subscription, or registry runkey running as SYSTEM. Continued activity after a credential reset means either the reset was incomplete or the attacker has a non-credential persistence path. Go back to the persistence hunting checklist (Ch10).
Q: You suspect an insider threat. Do you disable their account before or after notifying HR?
This is a judgment call that must involve both legal and HR — not a unilateral IT security decision. The standard process: notify the HR/legal team first (use out-of-band communication), get explicit authorization to take the account action, then act. In some jurisdictions and employment law contexts, disabling an employee's account before a formal notification process constitutes constructive dismissal and creates legal liability. If the threat is actively exfiltrating data or causing ongoing damage, the IR lead may need to disable the account immediately and notify HR/legal simultaneously, documenting that the emergency action was necessary to prevent active harm. This decision and its justification must be documented in real time.
Q: The attacker has MFA bypass via an OAuth refresh token from a compromised app consent grant. Resetting the password doesn't revoke it. What do you do?
OAuth refresh tokens are independent of AD credentials — they remain valid even after a password reset because they were issued by the identity provider as a separate authentication artifact. The correct action: (1) Revoke all refresh tokens using Revoke-AzureADUserAllRefreshToken (as shown above) or the Graph API equivalent. (2) Review and revoke all enterprise application consents — go to Azure AD → Enterprise Applications → [app] → Users and Groups, and revoke the consent. (3) Disable the specific OAuth application if it's malicious (an attacker-registered app that the user was tricked into consenting to). (4) After IR: implement policies requiring admin consent for OAuth app registrations to prevent recurrence. Refresh token theft and abuse is a common post-initial-access technique in O365/Azure environments that survives simple credential resets.