> For the complete documentation index, see [llms.txt](https://docs.stoik.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.stoik.io/cyber-best-practices/vulnerability-remediation-guides/ad-scan-vulnerability-remediation-guide/privileged-accounts.md).

# Privileged accounts

## P-Kerberoasting

#### What it means

In Active Directory, some accounts are registered to run services such as a SQL Server or a web application. These accounts have a **Service Principal Name (SPN)**, which is essentially a label that says "this account runs a network service."

The security risk is: **any authenticated user** in the domain can request a Kerberos ticket for any account with an SPN. That ticket is encrypted with the account's password, and the attacker can then try to crack it offline, without triggering any alert.

This is called a **Kerberoasting** attack. It's especially dangerous when the account with the SPN is also a **Domain Admin**, because cracking that password means full control of the domain.

#### What the scan checks

The AD Scan flags accounts that meet **all three** conditions:

1. The account is a member of a **privileged group** (Domain Admins, Enterprise Admins, etc.)
2. The account has a **Service Principal Name (SPN)**
3. The account's **password hasn't been changed in over 40 days** (giving attackers more time to crack it)

#### Why the alert may persist

You need to address **all three** conditions. Common incomplete fixes:

* **Changed the password but kept the SPN:** The alert clears temporarily, but returns after 40 days.
* **Removed the SPN but the account is still in Domain Admins:** The alert clears, but the underlying risk remains.

#### How to diagnose

```powershell
$privilegedGroups = @("Domain Admins", "Enterprise Admins", "Schema Admins", "Administrators")
$threshold = (Get-Date).AddDays(-40)

foreach ($group in $privilegedGroups) {
  Get-ADGroupMember -Identity $group -Recursive |
    Where-Object { $_.objectClass -eq "user" } |
    ForEach-Object { Get-ADUser $_ -Properties ServicePrincipalName, PasswordLastSet } |
    Where-Object { $_.ServicePrincipalName.Count -gt 0 -and $_.PasswordLastSet -lt $threshold } |
    Select-Object SamAccountName,
      @{N="PasswordAge (days)"; E={[math]::Round(((Get-Date) - $_.PasswordLastSet).TotalDays)}},
      @{N="SPNs"; E={$_.ServicePrincipalName -join "; "}}
}
```

#### How to fix

**Best option: Remove the account from privileged groups.** Service accounts rarely need to be Domain Admins. Create a dedicated, non-privileged account with only the permissions the service actually needs:

```powershell
Remove-ADGroupMember -Identity "Domain Admins" -Members "svc_account" -Confirm:$false
```

**If you can't remove it from the admin group:** At minimum, reset the password regularly (every 30 days or less):

```powershell
Set-ADAccountPassword -Identity "svc_account" `
  -NewPassword (ConvertTo-SecureString "YourNewStr0ngP@ssw0rd!" -AsPlainText -Force) -Reset
```

**Best long-term solution:** Convert to a **Group Managed Service Account (gMSA)**, which rotates its password automatically every 30 days:

```powershell
New-ADServiceAccount -Name "gMSA_SQLService" `
  -DNSHostName "gmsa_sqlservice.yourdomain.com" `
  -PrincipalsAllowedToRetrieveManagedPassword "SQLServersGroup"
```

## P-AdminLogin

#### What it means

Every Active Directory domain has a **built-in Administrator account** that is created automatically when the domain is set up. This account has full control over the entire domain and cannot be locked out.

Because of its power and predictability (attackers know it always exists), it should be reserved for **emergencies only**, such as recovering from a disaster. Day-to-day administration should use personal admin accounts (e.g., `adm_john`) so that actions can be traced to individuals.

#### What the scan checks

The AD Scan checks whether the built-in Administrator account has logged in to any Domain Controller in the **last 35 days**. If it has, the alert triggers, indicating the account is being used for routine tasks instead of being kept for emergencies.

#### Why the alert may persist

* **Renaming the account does not help.** The scan identifies the built-in Administrator by its internal identifier (RID-500), not by its name.
* **Changing the password does not help.** The scan checks login activity, not password age.
* **Automated scripts or scheduled tasks** running as the built-in Administrator will keep triggering the alert.
* **You just need to wait.** After you stop using the account, the alert clears automatically after 35 days.

#### How to diagnose

```powershell
# Check when the built-in Administrator last logged in on each DC
$domainSID = (Get-ADDomain).DomainSID
$adminSID = "$domainSID-500"

Get-ADDomainController -Filter * | ForEach-Object {
  $dc = $_
  $admin = Get-ADUser -Identity $adminSID -Server $dc.HostName -Properties LastLogon
  $lastLogon = [DateTime]::FromFileTime($admin.LastLogon)
  $daysAgo = [math]::Round(((Get-Date) - $lastLogon).TotalDays, 1)

  [PSCustomObject]@{
    DC        = $dc.HostName
    LastLogon = $lastLogon
    DaysAgo   = $daysAgo
    Status    = if ($daysAgo -le 35) { "ALERT - Used recently" } else { "OK" }
  }
} | Format-Table -AutoSize
```

Also check for scheduled tasks that might be using the built-in Administrator:

```powershell
Get-ADDomainController -Filter * | ForEach-Object {
  Invoke-Command -ComputerName $_.HostName -ScriptBlock {
    Get-ScheduledTask | Where-Object {
      $_.Principal.UserId -match "Administrator"
    } | Select-Object TaskName, @{N="RunAs"; E={$_.Principal.UserId}}
  }
}
```

#### How to fix

1. **Create personal admin accounts** for each administrator (e.g., `adm_john`, `adm_jane`)
2. **Update any scheduled tasks** to use a dedicated service account instead of the built-in Administrator
3. **Stop logging in** with the built-in Administrator for routine tasks
4. The alert will clear automatically **35 days** after the last login

## P-AdminNum

#### What it means

The more accounts with administrator privileges in your domain, the larger the attack surface. Each admin account is a potential entry point for an attacker. Many organizations end up with excessive admin accounts over time: temporary access that was never revoked, service accounts added to Domain Admins for convenience, etc.

#### What the scan checks

The AD Scan counts the total number of **unique users** across all privileged groups (Domain Admins, Enterprise Admins, Administrators, Account Operators, Server Operators, Backup Operators, etc.). If the same user is in multiple groups, they are counted only once.

The alert triggers if:

* More than **10%** of active users are privileged, OR
* More than **50** privileged accounts exist

> This check is skipped for small organizations with fewer than 100 active users.

#### How to diagnose

```powershell
$privilegedGroups = @(
  "Domain Admins", "Enterprise Admins", "Schema Admins", "Administrators",
  "Account Operators", "Server Operators", "Backup Operators", "Print Operators"
)

$uniqueMembers = @{}
foreach ($group in $privilegedGroups) {
  try {
    Get-ADGroupMember -Identity $group -Recursive |
      Where-Object { $_.objectClass -eq "user" } |
      ForEach-Object { $uniqueMembers[$_.DistinguishedName] = $_.SamAccountName }
  } catch {}
}

$activeUsers = (Get-ADUser -Filter "Enabled -eq `$true" | Measure-Object).Count
$pct = [math]::Round(($uniqueMembers.Count / $activeUsers) * 100, 1)

Write-Host "Privileged accounts: $($uniqueMembers.Count)"
Write-Host "Active users: $activeUsers"
Write-Host "Ratio: $pct% (alert if >10% or >50 accounts)"
Write-Host ""
$uniqueMembers.Values | Sort-Object | ForEach-Object { Write-Host "  - $_" }
```

#### How to fix

Review the list and remove accounts that don't truly need admin access:

```powershell
Remove-ADGroupMember -Identity "Domain Admins" -Members "username" -Confirm:$false
```

Best practices:

* Use **separate admin accounts** (`adm_john`) that are different from daily-use accounts
* **Remove service accounts** from admin groups, granting only the specific permissions they need
* **Review privileged group membership** on a regular basis

## P-ProtectedUsers

#### What it means

The **Protected Users** group is a special security group introduced in Windows Server 2012 R2. Members of this group get extra protections:

* They can **only authenticate with Kerberos** (the more secure protocol), not with the older NTLM protocol
* Their **credentials are not cached** on machines they log into: an attacker who compromises a workstation cannot steal their credentials from memory
* **Delegation is disabled:** their account cannot be impersonated by services

These protections are critical for administrator accounts, which are the primary targets in most attacks.

#### What the scan checks

The AD Scan verifies that all **enabled admin accounts** (members of Domain Admins, Enterprise Admins, etc.) are also members of the **Protected Users** group.

#### Why the alert may persist

* **Users added via nested groups** may not be detected. Add users **directly** to the Protected Users group.
* **Replication delays** between Domain Controllers can cause temporary inconsistencies.
* **Managed Service Accounts (gMSA)** are excluded from this check: they don't need to be in Protected Users.

#### How to diagnose

```powershell
$protectedUsers = (Get-ADGroupMember -Identity "Protected Users").DistinguishedName
$privilegedGroups = @("Domain Admins", "Enterprise Admins", "Schema Admins", "Administrators")

$notProtected = foreach ($group in $privilegedGroups) {
  Get-ADGroupMember -Identity $group -Recursive |
    Where-Object { $_.objectClass -eq "user" } |
    ForEach-Object { Get-ADUser $_ -Properties Enabled } |
    Where-Object { $_.Enabled -and $_.DistinguishedName -notin $protectedUsers }
}

$notProtected | Select-Object -Unique SamAccountName, Name | Format-Table -AutoSize
```

#### How to fix

```powershell
Add-ADGroupMember -Identity "Protected Users" -Members "adm_john", "adm_jane"
```

> **Important:** Adding accounts to Protected Users enforces **Kerberos-only authentication**. Some legacy applications that require NTLM may stop working for these accounts. **Test with one account first** before adding all administrators.
>
> Best practice: admin accounts should be used **only for administration tasks** (not for email, web browsing, or running business applications), so NTLM compatibility is rarely a concern.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.stoik.io/cyber-best-practices/vulnerability-remediation-guides/ad-scan-vulnerability-remediation-guide/privileged-accounts.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
