Blue Team Basics: Active Directory Security Assessments

If you manage Active Directory and you haven't run BloodHound or PingCastle against your own domain yet, you should probably do that before someone else does. This post covers the main ways attackers move through AD once they're inside, and what you can do to spot them.

I've been doing AD security assessments at client sites for a while now, and the same problems keep showing up. Overprivileged accounts, stale delegations, SID history nobody's looked at in years. The tools attackers use to find these are free and easy to run. Your defenses should be too.

Enumeration: what attackers see when they get in

Once someone has a foothold on a domain-joined machine, they can query AD for everything. User accounts, group memberships, service accounts, who's logged in where. Tools like BloodHound map out attack paths that would take hours to find manually, and PingCastle gives you a security risk score with specific findings in minutes.

You can't stop someone from querying AD if they're on a domain-joined machine (it's how AD works), but you can make it harder and you can detect it:

  • Lock down who can query what. Most users don't need to enumerate the entire directory. Restrict read access where you can.
  • Watch your logs. Failed logins, unusual LDAP queries, accounts accessing things they normally don't. Set up alerts in your SIEM (I use Sentinel, but whatever you've got works).
  • Honeypot accounts. Create fake admin accounts that look tempting. Match your org's actual naming convention (if your service accounts are "SVC-APP-ENV," don't use "svc_backup_admin"), give them realistic metadata (logon history, group memberships, SPNs), and stick them in a visible OU so they show up in enumeration. If anyone touches them, you know something's wrong. Just make sure you're actually monitoring them, or they're pointless.
  • Endpoint protection on everything. Defender for Endpoint is an endpoint protection platform with EDR, antivirus, and attack surface reduction built in. It catches a lot of enumeration tools. Not all of them, but it raises the bar.

Check for failed logins and special privilege logon events:

# Failed login attempts (use Get-WinEvent, Get-EventLog is not available in PowerShell 6+)
Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4625} -MaxEvents 50 |
    Select-Object TimeCreated, @{N='Account';E={$_.Properties[5].Value}}, @{N='SourceIP';E={$_.Properties[19].Value}}

# Special privileges assigned to a logon session
Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4672} -MaxEvents 50 |
    Select-Object TimeCreated, @{N='Account';E={$_.Properties[1].Value}}

Expected output:

TimeCreated           Account          SourceIP
-----------           -------          --------
2023-03-01 14:22:31   jsmith           10.0.1.45
2023-03-01 14:22:28   jsmith           10.0.1.45
2023-03-01 14:21:55   administrator    192.168.5.12
2023-03-01 14:20:03   svc_backup       10.0.1.100
2023-03-01 13:58:41   admin.legacy     172.16.0.88

TimeCreated           Account
-----------           -------
2023-03-01 14:30:01   administrator
2023-03-01 14:22:31   svc_sqlprod
2023-03-01 14:15:12   administrator
2023-03-01 13:58:41   CORP\da-thomas

What you're looking at: The first table shows failed logon attempts: who tried to log in, when, and from which IP. Multiple rapid failures from the same IP against the same account suggests brute force. Failures from unusual IPs against admin accounts are worth investigating immediately. The second table shows every time an account with elevated privileges (Domain Admin, backup rights, debug privileges, etc.) started a new session. If you see accounts here that shouldn't have admin rights, or logon times that don't match normal working hours, dig deeper.

Note: the original version of this post used Get-EventLog which is not available in PowerShell 6+. Get-WinEvent is faster and works in PowerShell 7+.

Finding overprivileged accounts

At almost every client I've worked with, there are more Domain Admins than there should be. Service accounts with admin rights "because it was easier." Credentials cached on workstations from that one time someone logged in to fix a printer.

Attackers know this. They look for stored credentials first because it's the easiest win.

Start by finding out who's actually in your admin groups:

# List all members of privileged groups
$AdminGroups = "Domain Admins", "Enterprise Admins", "Schema Admins", "Administrators"
$AdminUsers = foreach ($Group in $AdminGroups) {
    Get-ADGroupMember -Identity $Group -Recursive |
        Where-Object { $_.objectClass -eq "user" } |
        Select-Object Name, SamAccountName, @{Name="Group"; Expression={$Group}}
}
$AdminUsers | Sort-Object Group | Format-Table -AutoSize

Expected output:

Name              SamAccountName    Group
----              --------------    -----
Thomas Grome      tgrome            Domain Admins
SQL Service       svc_sql           Domain Admins
Backup Admin      svc_backup        Domain Admins
Old Contractor    jdoe_ext          Domain Admins
Thomas Grome      tgrome            Enterprise Admins
Admin             Administrator     Administrators
Thomas Grome      tgrome            Administrators
SQL Service       svc_sql           Administrators

What you're looking at: Every user in your four most privileged groups, broken out by group. If you see service accounts in Domain Admins (like svc_sql above), they almost certainly don't need to be there. Accounts appearing in multiple groups is common but worth questioning: does svc_backup really need Domain Admin, or just Backup Operators? Anything you don't recognize by name needs investigation. In a healthy environment, this table should be short — three to five accounts max across all groups.

If that list is longer than you expected, that's your first problem to fix.

While you're at it, check your file shares too. Attackers love finding shares with overly permissive ACLs:

# Check ACLs on file shares, filtering out expected accounts
# Replace the path with your actual shares (\\server\share or local paths)
Get-Acl -Path "\\FileServer\SharedData" |
    Select-Object -ExpandProperty Access |
    Where-Object {
        $_.IdentityReference -notlike "BUILTIN\Administrators" -and
        $_.IdentityReference -notlike "NT AUTHORITY\SYSTEM"
    } | Format-Table IdentityReference, FileSystemRights, AccessControlType

Expected output:

IdentityReference              FileSystemRights    AccessControlType
-----------------              ----------------    -----------------
CORP\Domain Users              ReadAndExecute      Allow
Everyone                       Read                Allow
CORP\Helpdesk                  FullControl         Allow

What you're looking at: Every non-standard permission on the share. "Everyone" with any access is a red flag. "Domain Users" with read access means every authenticated user in the domain can browse this share. "FullControl" for a helpdesk group means anyone in that group can modify, delete, or plant files. The dangerous ones to watch for: broad groups (Domain Users, Authenticated Users, Everyone) with write access, and any group with FullControl that doesn't absolutely need it.

This is separate from AD permissions, but it's where credentials and sensitive data often end up. I've found password spreadsheets on open shares more times than I'd like to admit.

SID history: the hidden backdoor

This one's sneaky. When you migrate accounts between domains, Windows keeps the old SID in a "SID history" attribute. The idea is that the migrated account can still access resources from the old domain. The problem is that attackers can inject privileged SIDs into that history and effectively give themselves admin access without being in any admin group.

Quick primer if you're not familiar with SIDs: a SID is just a unique identifier that Windows assigns to every account and group. Some are well-known, like S-1-5-32-544 (Administrators), S-1-5-32-545 (Users), and S-1-5-32-555 (Remote Desktop Users). If someone injects a privileged SID like 544 into an account's sIDHistory, Windows treats that account as a member of the Administrators group. No group membership change needed, no audit trail in group modification logs. It just works.

You should be checking for accounts with unexpected SID history, especially if you haven't done a domain migration recently. If nobody migrated anything and you find SID history entries, something's wrong.

# Find accounts with SID history (if you haven't migrated domains, this should be empty)
Get-ADUser -Filter * -Properties sIDHistory |
    Where-Object { $_.sIDHistory.Count -gt 0 } |
    Select-Object Name, SamAccountName, @{N='SIDHistory';E={$_.sIDHistory -join ', '}}

# Same for groups
Get-ADGroup -Filter * -Properties sIDHistory |
    Where-Object { $_.sIDHistory.Count -gt 0 } |
    Select-Object Name, @{N='SIDHistory';E={$_.sIDHistory -join ', '}}

Expected output (clean environment — no SID history found):

# No output = good. No accounts have SID history.

Expected output (accounts with SID history found):

Name              SamAccountName    SIDHistory
----              --------------    ----------
Jane Smith        jsmith            S-1-5-21-8675309-1234567-7654321-1105
MigratedAdmin     old_admin         S-1-5-21-8675309-1234567-7654321-512, S-1-5-32-544

What you're looking at: Any account that has SID values from another domain baked into it. If you recently migrated domains, you'll see entries here and that's expected — the SIDs should belong to the old domain. The red flags: SID history containing well-known privileged SIDs like -512 (Domain Admins) or S-1-5-32-544 (Administrators), SIDs from domains you don't recognize, or SID history on accounts created after any migration was completed. The second example above (old_admin) has both a regular SID and the built-in Administrators SID in its history — that account effectively has admin rights without appearing in any admin group.

If you have forest trusts, check whether SID filtering is still turned on. SID filtering stops SIDs from untrusted domains from being honored in access tokens. It's on by default for forest trusts, but the default filter is weaker than you'd expect: it allows SID history from domains within the trusted forest through. For full protection, verify quarantine mode is explicitly enabled (netdom trust /quarantine:yes). Beyond that, someone might've disabled SID filtering entirely years ago for a migration and never turned it back on. If an attacker compromises a trusted forest and SID filtering is off, they get cross-forest privilege escalation for free. I've seen more than one environment where trusts were created and literally nobody ever checked the settings again.

# Check SID filtering status on all trusts
Get-ADTrust -Filter * -Properties SIDFilteringQuarantined, SIDFilteringForestAware, TrustDirection |
    Select-Object Name, TrustDirection, SIDFilteringQuarantined, SIDFilteringForestAware |
    Format-Table -AutoSize

Expected output:

Name                TrustDirection    SIDFilteringQuarantined    SIDFilteringForestAware
----                --------------    -----------------------    -----------------------
partner.local       BiDirectional     True                       False
child.corp.local    BiDirectional     False                      False
legacy.local        Inbound           False                      True

What you're looking at: Each trust and its SID filtering status. SIDFilteringQuarantined = True is the strictest setting — only SIDs from the trusted domain itself are allowed through (good). SIDFilteringQuarantined = False on an external trust means SID history abuse is possible. SIDFilteringForestAware = True means a forest trust has been relaxed to allow SID history with RID >= 1000 through — this is weaker than default forest trust filtering and worth investigating why it was set. In the example above, "legacy.local" has relaxed filtering that someone probably enabled for a migration and forgot to revert.

One more thing on SID history: not all of it is malicious. Accounts that were migrated during domain consolidations will have legitimate SID history. Same with built-in accounts from in-place upgrades, or child domain accounts that interact with parent trusts. That's all normal. The red flag is SID history showing up on accounts that were created after the migration was done, or SIDs from domains you don't recognize. Those are the ones worth investigating.

AdminSDHolder: the ACL nobody checks

AdminSDHolder is an AD object that controls the permissions on all built-in privileged groups (Domain Admins, Enterprise Admins, etc.). Every 60 minutes, a process called SDProp resets the ACLs on those groups to match whatever's on AdminSDHolder.

If an attacker modifies AdminSDHolder's ACL to include their account, SDProp will propagate that access to every protected group automatically. And most teams don't monitor AdminSDHolder because they don't know it exists.

# List who has permissions on AdminSDHolder
# Note: you need the AD: drive from the ActiveDirectory module
Import-Module ActiveDirectory
$adminSDHolder = "AD:\CN=AdminSDHolder,CN=System," + (Get-ADDomain).DistinguishedName
(Get-Acl $adminSDHolder).Access |
    Select-Object IdentityReference, ActiveDirectoryRights, AccessControlType |
    Format-Table -AutoSize

# List recursive members of Domain Admins (cross-reference with above)
Get-ADGroupMember -Identity "Domain Admins" -Recursive |
    Get-ADUser |
    Select-Object SamAccountName, DistinguishedName

Expected output (AdminSDHolder ACL):

IdentityReference              ActiveDirectoryRights     AccessControlType
-----------------              ---------------------     -----------------
NT AUTHORITY\SYSTEM            GenericAll                Allow
BUILTIN\Administrators         GenericAll                Allow
CORP\Domain Admins             GenericAll                Allow
CORP\Enterprise Admins         GenericAll                Allow
CORP\Schema Admins             ReadProperty, WriteP...   Allow
NT AUTHORITY\Authenticated...  GenericRead               Allow
NT AUTHORITY\SELF              ReadProperty, WriteP...   Allow
BUILTIN\Account Operators      GenericRead               Allow
...

SamAccountName      DistinguishedName
--------------      -----------------
Administrator       CN=Administrator,CN=Users,DC=corp,DC=local
tgrome              CN=Thomas Grome,OU=Admins,DC=corp,DC=local

What you're looking at: The full ACL on AdminSDHolder and your current Domain Admins. Cross-reference the two lists: any identity in the ACL that isn't a known admin group or built-in account needs investigation. The Domain Admins list at the bottom tells you who has legitimate access. If you see a random user account (like "CORP\backdooruser") in the ACL with GenericAll or WriteDACL rights, that's an attacker persistence mechanism — they'll get their access restored every 60 minutes by SDProp.

That first query shows everything. This one filters out the expected accounts so you only see anomalies:

# AdminSDHolder ACL - only unexpected entries
# Adjust the domain prefix to match your environment
$domain = (Get-ADDomain).NetBIOSName
$expected = @(
    "NT AUTHORITY\SYSTEM",
    "NT AUTHORITY\SELF",
    "NT AUTHORITY\Authenticated Users",
    "BUILTIN\Administrators",
    "BUILTIN\Account Operators",
    "BUILTIN\Server Operators",
    "BUILTIN\Print Operators",
    "BUILTIN\Backup Operators",
    "BUILTIN\Pre-Windows 2000 Compatible Access",
    "$domain\Domain Admins",
    "$domain\Enterprise Admins",
    "$domain\Schema Admins",
    "$domain\Key Admins",
    "$domain\Enterprise Key Admins"
)
$adminSDHolder = "AD:\CN=AdminSDHolder,CN=System," + (Get-ADDomain).DistinguishedName
(Get-Acl $adminSDHolder).Access |
    Where-Object { $_.IdentityReference.Value -notin $expected } |
    Select-Object IdentityReference, ActiveDirectoryRights, AccessControlType |
    Format-Table -AutoSize

Expected output (clean environment):

# No output = good. No unexpected ACL entries.

Expected output (compromised or misconfigured):

IdentityReference       ActiveDirectoryRights    AccessControlType
-----------------       ---------------------    -----------------
CORP\svc_webadmin       GenericAll               Allow

What you're looking at: Any ACL entry that isn't in the expected defaults list. Empty output is what you want. If you see anything here, someone (or something) added a non-standard account to AdminSDHolder. In the example above, svc_webadmin has GenericAll on AdminSDHolder, meaning SDProp will give that account full control over Domain Admins, Enterprise Admins, and every other protected group — every 60 minutes, automatically. This is either a serious misconfiguration or an attacker persistence mechanism.

If you see accounts in AdminSDHolder's ACL that you don't recognize, investigate immediately. That's not normal.

Kerberos attacks: golden and silver tickets

Kerberos is how Windows handles authentication in AD environments. When you log in, you get a Ticket Granting Ticket (TGT) that lets you request access to services without entering your password again. It works well until someone forges a ticket.

Golden ticket: an attacker who gets the KRBTGT account hash can forge TGTs for any account, with any group membership. Full domain compromise. They don't even need network access to the domain controller to use it.

Silver ticket: same idea but scoped to a single service. The attacker needs the service account's hash and can forge tickets for just that service. Smaller blast radius, harder to detect.

You can't prevent these entirely (if someone has your KRBTGT hash, you have bigger problems), but you can detect and limit the damage:

# Find accounts with SPNs (potential Kerberoasting / silver ticket targets)
# This catches regular user accounts used as service accounts, not just MSAs/gMSAs
Get-ADUser -Filter {ServicePrincipalName -like "*"} -Properties ServicePrincipalName, PasswordLastSet |
    Select-Object Name, SamAccountName, PasswordLastSet, ServicePrincipalName

# Check your own Kerberos ticket cache
klist tgt

# Find accounts with passwords that haven't been changed in a long time
# (old passwords = more time for attackers to crack them)
Get-ADUser -Filter {Enabled -eq $true} `
    -Properties PasswordLastSet, LastLogonDate |
    Where-Object { $_.PasswordLastSet -lt (Get-Date).AddDays(-90) } |
    Select-Object Name, SamAccountName, PasswordLastSet, LastLogonDate |
    Sort-Object PasswordLastSet

Expected output (accounts with SPNs):

Name             SamAccountName   PasswordLastSet          ServicePrincipalName
----             --------------   ---------------          --------------------
SQL Service      svc_sql          2019-06-15 09:30:00      {MSSQLSvc/sql01.corp.local:1433}
Web Service      svc_iis          2020-11-22 14:15:00      {HTTP/web01.corp.local}
Legacy App       svc_legacy       2017-03-01 10:00:00      {HTTP/app02.corp.local, HTTP/app02}
krbtgt           krbtgt           2023-02-28 16:45:00      {kadmin/changepw}

What you're looking at: Every user account with a Service Principal Name — these are your Kerberoasting and silver ticket attack surface. The key column is PasswordLastSet: older passwords have had more time to be cracked offline. In the example above, svc_legacy hasn't had its password changed since 2017 — that's six years for an attacker to brute-force the hash after requesting a Kerberos service ticket. krbtgt will appear here too, which is normal. Focus on accounts with old passwords and ask: can these be converted to Group Managed Service Accounts (gMSAs) which rotate passwords automatically?

Rotate your KRBTGT password. Twice (because AD keeps the previous password as a fallback). If you've never done it, do it now. If you can't remember when you last did it, do it now. Wait at least 12 hours between the two resets (or your domain's maximum ticket lifetime, whichever is greater) to let existing tickets expire and replication complete across all DCs. This won't prevent golden tickets if someone already has the hash, but it invalidates any forged tickets they've already made.

Here's how to check it actually worked:

# Verify KRBTGT password was reset on all DCs
Get-ADDomainController -Filter * | foreach {
    $dc = $_.HostName
    Get-ADUser "krbtgt" -Server $dc -Properties PasswordLastSet |
        Select-Object @{N='DC';E={$dc}}, PasswordLastSet
}

# Quick replication health check
repadmin /replsummary

Expected output (KRBTGT verification):

DC                          PasswordLastSet
--                          ---------------
dc01.corp.local             2023-03-01 16:45:22
dc02.corp.local             2023-03-01 16:45:22
dc03.corp.local             2023-03-01 16:45:22

Replication Summary Start Time: 2023-03-01 17:00:15

Beginning data collection for replication summary, this may take awhile:
  .....

Source DSA          largest delta    fails/total %%   error
 DC01               04m:12s          0 /   5    0
 DC02               03m:55s          0 /   5    0
 DC03               04m:01s          0 /   5    0

What you're looking at: The first table shows the KRBTGT password's last reset time as seen by each domain controller. All dates should be identical — if one DC shows an older date, replication hasn't completed to that DC yet, and you must wait before doing the second reset. The repadmin output below shows replication health: "largest delta" is the time since the last successful replication, and "fails/total" should be 0. Any failures here mean you have a replication problem that needs fixing before KRBTGT rotation will work correctly.

If the PasswordLastSet dates don't match across DCs, replication hasn't finished yet. Don't do the second reset until they're consistent.

Related: Credential Guard: Protect Windows from pass-the-hash and pass-the-ticket attacks (note: Credential Guard uses virtualization-based security to protect NTLM hashes and Kerberos TGTs in LSA, which mitigates pass-the-hash and TGT theft respectively. It does not protect already-issued service tickets.)

Delegation: the feature that keeps backfiring

Delegation lets one account impersonate another to access services on a different server. It's useful for things like a web app accessing a database on behalf of a user. It's also one of the most common privilege escalation paths in AD.

Unconstrained delegation is the dangerous one. Any computer with unconstrained delegation caches the full TGT of any user who authenticates to it. That TGT can then be reused to access any service in the domain as that user. If you find this enabled on anything other than your domain controllers, fix it.

Constrained delegation limits which target services (SPNs) the account can delegate credentials to. Better, but still needs monitoring.

Find everything with delegation configured:

# Objects with constrained delegation
Get-ADObject -Filter {msDS-AllowedToDelegateTo -like "*"} -Properties msDS-AllowedToDelegateTo |
    Format-Table Name, ObjectClass, msDS-AllowedToDelegateTo -AutoSize

# Computers with unconstrained delegation (EXCLUDING domain controllers)
# Uses PrimaryGroupID to reliably identify DCs (516) and RODCs (521)
Get-ADComputer -Filter {TrustedForDelegation -eq $true} -Properties TrustedForDelegation, PrimaryGroupID |
    Where-Object { $_.PrimaryGroupID -notin @(516, 521) } |
    Select-Object Name, DistinguishedName

Expected output (constrained delegation):

Name          ObjectClass    msDS-AllowedToDelegateTo
----          -----------    ------------------------
svc_web       user           {HTTP/app01.corp.local, HTTP/app01}
SQL01$        computer       {MSSQLSvc/sql02.corp.local:1433}

Expected output (unconstrained delegation — clean environment):

# No output = good. No non-DC machines with unconstrained delegation.

Expected output (unconstrained delegation — problem found):

Name          DistinguishedName
----          -----------------
YOURWEB01     CN=YOURWEB01,OU=Servers,DC=corp,DC=local
YOURPRINT01   CN=YOURPRINT01,OU=Servers,DC=corp,DC=local

What you're looking at: The first table shows objects with constrained delegation — they can only delegate to the specific SPNs listed. Review these to confirm the target services are still valid. The second query is the critical one: any computer that appears here has unconstrained delegation and is NOT a domain controller. That means if an attacker compromises that machine and coerces a Domain Admin to authenticate to it (trivial with tools like PrinterBug or PetitPotam), they capture the admin's full TGT. Web servers and print servers are common offenders — they were configured this way years ago and nobody ever fixed it.

If you find computers with unconstrained delegation that aren't domain controllers, that's a priority fix. Constrained delegation entries should be reviewed to make sure the target services are still valid and necessary.

Disabled accounts that still have the keys

At one client, I found a contractor's account that had been disabled three years ago. Nobody had removed it from Domain Admins because, well, it was disabled. Who cares, right? The SID was still sitting in the group membership. If that account got re-enabled (helpdesk mistake, social engineering, whatever) or if the old credentials got reused somewhere, it's instant domain admin. No escalation needed.

I check for this every time now:

# Find disabled accounts still in admin groups
$AdminGroups = "Domain Admins", "Enterprise Admins", "Schema Admins", "Administrators"
foreach ($Group in $AdminGroups) {
    Get-ADGroupMember -Identity $Group -Recursive |
        Where-Object { $_.objectClass -eq "user" } |
        Get-ADUser |
        Where-Object { $_.Enabled -eq $false } |
        Select-Object Name, SamAccountName, @{N='Group';E={$Group}}
}

Expected output (clean environment):

# No output = good. No disabled accounts in admin groups.

Expected output (problem found):

Name               SamAccountName    Group
----               --------------    -----
John Doe           jdoe_ext          Domain Admins
Old Service        svc_old           Administrators

What you're looking at: Disabled accounts that still sit in privileged groups. These are dormant backdoors. If anyone re-enables jdoe_ext (a helpdesk mistake, a social engineering call, a compromised admin), that account instantly has Domain Admin rights — no escalation needed. The fix is simple: remove them from the groups. Disabled accounts don't need group memberships.

If it returns anything, pull those accounts out of the groups. Disabled isn't the same as gone.

What to do first

If you're looking at all of this thinking "where do I even start," here's my priority list:

  1. Run PingCastle. It takes 5 minutes and gives you a security score with specific findings. Run this first — it'll tell you where your biggest gaps are and inform everything else on this list.
  2. Audit your admin groups. Know who's in Domain Admins and why. Remove anyone who doesn't need to be there.
  3. Check for unconstrained delegation. If it exists on non-DC machines, fix it. This is one of the most common escalation paths.
  4. Set up basic log monitoring. At minimum, alert on Event IDs 4625 (failed logins), 4672 (special privileges), and 4768/4769 (Kerberos ticket requests). Fair warning: 4768/4769 will be noisy in large environments. You'll need to tune thresholds or filter out known service accounts, otherwise you'll drown in legitimate traffic. Also worth adding: Event 4738 (user account changed) filtered for sIDHistory modifications. It's rare enough that any hit is worth investigating.
  5. Rotate KRBTGT. Twice. If you've never done it, you're overdue. Do this after your assessment is complete — rotating mid-assessment can tip off an attacker holding golden tickets.

Here's a starting point for filtering the 4768/4769 noise:

# Kerberos ticket requests grouped by account, sorted by volume
# Look for accounts you don't recognize near the top
Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4768,4769} -MaxEvents 10000 |
    Group-Object { ($_.Properties[0].Value -split '@')[0] } |
    Sort-Object Count -Descending |
    Select-Object -First 20 Count, Name

Expected output:

Count Name
----- ----
 4521 DC01$
 3892 DC02$
 1247 svc_exchange
  891 svc_sql
  443 SQL01$
  312 tgrome
  287 jsmith
   43 unknown_user
    2 admin.legacy

What you're looking at: Kerberos ticket requests grouped by account and sorted by volume. The top entries will be domain controllers (DC01$, DC02$) and service accounts (svc_exchange, svc_sql) — that's normal. Regular users like tgrome and jsmith will appear in the middle with moderate counts. What you're hunting for: accounts you don't recognize (unknown_user), accounts requesting an unusual volume of tickets relative to their role, or honeypot accounts that shouldn't be authenticating at all (admin.legacy with 2 requests is a red flag if it's a honeypot). An account suddenly appearing at the top that wasn't there last week could indicate Kerberoasting or lateral movement.

The top entries will almost always be your service accounts and computer accounts, which is normal. Scan for anything unexpected below those.

These are the things I see missed most often, and fixing them closes the most common attack paths.

Popular posts from this blog

Intune Log on Rights