> 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/anomalies.md).

# Anomalies

## A-LAPS-Not-Installed

#### What it means

Every Windows computer has a **built-in local Administrator account**. By default, this account often has the same password across all machines, or a password that never changes. If an attacker compromises one machine, they can reuse that password to access all the others.

**LAPS** (Local Administrator Password Solution) solves this by automatically setting a unique, randomly generated password on each computer and storing it securely in Active Directory. Only authorized administrators can retrieve it.

#### What the scan checks

The AD Scan looks for evidence that LAPS has been set up in your Active Directory. Specifically, it checks whether the necessary LAPS data fields exist in your AD configuration. If they don't, it means LAPS was never installed.

#### Why the alert may persist

* **LAPS managed through Intune only:** If you use Microsoft Intune to manage LAPS passwords in the cloud (without configuring the on-premises AD), the AD Scan cannot detect it. The scan only looks at your local Active Directory.
* **LAPS was partially installed:** The LAPS setup may have been started but not completed.

#### How to diagnose

```powershell
# Check if LAPS is set up in your Active Directory
Get-ADObject -SearchBase (Get-ADRootDSE).SchemaNamingContext `
  -Filter "name -like 'ms-Mcs-AdmPwd' -or name -eq 'ms-LAPS-Password'" |
  Select-Object Name, whenCreated
```

If this returns no results, LAPS is not installed.

#### How to fix

**For Windows Server 2019 and later (recommended):** Modern LAPS is built into Windows. Enable it via Group Policy:

1. Open `gpmc.msc` (Group Policy Management)
2. Edit or create a GPO linked to your computer OUs
3. Navigate to: `Computer Configuration > Administrative Templates > System > LAPS`
4. Configure the LAPS settings (enable, set password complexity, etc.)

**For older environments:** Download Legacy LAPS from Microsoft and run the schema extension:

```powershell
Update-LapsADSchema
```

> **Note:** If you manage local admin passwords through a different tool (CyberArk, Intune-only LAPS, etc.), contact Stoïk support to document this as an accepted exception.

## A-LAPS-Joined-Computers

#### What it means

When someone adds ("joins") a computer to the Active Directory domain, that person becomes the **creator** of the computer object in AD. By default, the creator gets special permissions on that object.

The problem is: when LAPS is installed, the local admin password is stored on that same computer object. If the creator still has the right permissions, they can **read the LAPS password**, even if they are not an administrator. This means a regular user who once joined a PC to the domain could retrieve the local admin password for that machine.

#### What the scan checks

For each enabled computer with LAPS deployed, the AD Scan checks **two things**:

1. **Is the person who joined the computer still the "owner" of the computer object?** Owners can grant themselves any permission, including reading the LAPS password.
2. **Does the creator still have explicit permissions** on the computer object such as `All Extended Rights` (which allows reading the LAPS password), `WriteDacl` (which allows modifying permissions), `WriteOwner` (which allows taking back ownership), or `GenericWrite`?

The alert triggers if **either** condition is true.

#### Why the alert may persist

This is the most common source of confusion: **changing the owner is only half the fix.**

Many administrators change the owner to "Domain Admins" and expect the alert to disappear. But the AD Scan also checks for leftover permissions (called ACEs, or Access Control Entries) that were granted to the original creator. These permissions stay in place even after changing the owner and must be removed separately.

#### How to diagnose

```powershell
# List computers where the creator still has risky permissions
Get-ADComputer -Filter "Enabled -eq `$true" `
  -Properties "mS-DS-CreatorSID", "nTSecurityDescriptor" |
  Where-Object { $_."mS-DS-CreatorSID" -ne $null } |
  ForEach-Object {
    $comp = $_
    $acl = $comp.nTSecurityDescriptor
    $creatorSid = $comp."mS-DS-CreatorSID"
    $owner = $acl.GetOwner([System.Security.Principal.SecurityIdentifier])

    $isOwner = ($owner.Value -eq $creatorSid.Value)

    $riskyAces = $acl.Access | Where-Object {
      -not $_.IsInherited -and
      $_.IdentityReference.Value -eq $creatorSid.Value -and
      ($_.ActiveDirectoryRights -match 'WriteDacl|WriteOwner|GenericWrite|ExtendedRight')
    }

    if ($isOwner -or $riskyAces) {
      [PSCustomObject]@{
        Computer       = $comp.Name
        CreatorIsOwner = $isOwner
        RiskyRights    = ($riskyAces | ForEach-Object { $_.ActiveDirectoryRights }) -join ", "
      }
    }
  } | Format-Table -AutoSize
```

#### How to fix

You need to do **both** of these for each affected computer:

1. **Change the owner** to Domain Admins
2. **Remove the creator's explicit permissions**

The following script does both automatically:

```powershell
#Requires -Modules ActiveDirectory

$affectedComputers = Get-ADComputer -Filter "Enabled -eq `$true" `
  -Properties "mS-DS-CreatorSID", "nTSecurityDescriptor" |
  Where-Object { $_."mS-DS-CreatorSID" -ne $null }

foreach ($comp in $affectedComputers) {
  $path = "AD:$($comp.DistinguishedName)"
  $acl = Get-Acl -Path $path
  $creatorSid = $comp."mS-DS-CreatorSID"
  $modified = $false

  # Step 1: Change owner to Domain Admins
  $domainAdmins = New-Object System.Security.Principal.NTAccount("$env:USERDOMAIN", "Domain Admins")
  $acl.SetOwner($domainAdmins)
  $modified = $true

  # Step 2: Remove explicit permissions granted to the creator (and orphaned SIDs)
  $toRemove = @($acl.Access | Where-Object {
    -not $_.IsInherited -and
    ($_.IdentityReference.Value -eq $creatorSid.Value -or
     $_.IdentityReference.Value -match '^S-1-5-21-')  # Orphaned SIDs (deleted accounts)
  })

  foreach ($ace in $toRemove) {
    $acl.RemoveAccessRuleSpecific($ace)
    Write-Host "$($comp.Name): Removed [$($ace.ActiveDirectoryRights)] for $($ace.IdentityReference)"
  }

  if ($modified) {
    Set-Acl -Path $path -AclObject $acl
    Write-Host "$($comp.Name): Fixed" -ForegroundColor Green
  }
}
```

> **Tip:** Run the diagnostic command first to review the list of affected computers before applying the fix.

## A-NoServicePolicy

#### What it means

**Service accounts** are accounts used by applications and services (e.g., SQL Server, backup agents) to authenticate in Active Directory. Unlike regular user accounts, service accounts are high-value targets: they often have elevated permissions and their passwords are rarely changed.

A **Fine-Grained Password Policy (PSO)** is a special password rule that you can apply to specific groups of accounts. The AD Scan expects that your service accounts are covered by a PSO requiring passwords of at least **20 characters**, making them much harder to crack.

#### What the scan checks

The AD Scan looks for at least one password policy (GPO or PSO) that enforces a minimum password length of **20 characters or more**.

#### Why the alert may persist

1. **The scan account doesn't have permission to read the PSO.** Password policies are stored in a protected area of AD that only Domain Admins can read by default. If the scan runs with a less-privileged account, it simply cannot see the PSO, even though it exists.
2. **The PSO exists but the minimum length is below 20** (e.g., 12 or 15 characters).
3. **The PSO exists but is not assigned to any group or user.**

#### How to diagnose

```powershell
# List all Fine-Grained Password Policies and their settings
Get-ADFineGrainedPasswordPolicy -Filter * |
  Select-Object Name, MinPasswordLength, @{N="AppliedTo"; E={$_.AppliesTo -join ", "}}, Precedence
```

If this returns nothing, either no PSO exists or the account running the command lacks permissions to see it. Try running with a Domain Admin account.

#### How to fix

**If the PSO exists but the scan can't see it:** Run the AD Scan with a Domain Admin account.

**If no PSO exists:** Create one and assign it to your service accounts:

```powershell
# Create a password policy for service accounts
New-ADFineGrainedPasswordPolicy -Name "Service Accounts - Strong Password" `
  -Precedence 10 `
  -MinPasswordLength 20 `
  -MaxPasswordAge "90.00:00:00" `
  -MinPasswordAge "1.00:00:00" `
  -PasswordHistoryCount 24 `
  -ComplexityEnabled $true `
  -ReversibleEncryptionEnabled $false `
  -LockoutDuration "00:30:00" `
  -LockoutObservationWindow "00:30:00" `
  -LockoutThreshold 5

# Assign it to a security group containing your service accounts
Add-ADFineGrainedPasswordPolicySubject `
  -Identity "Service Accounts - Strong Password" `
  -Subjects "SG_ServiceAccounts"
```

> **Note:** Creating a PSO does not immediately force password changes. Existing passwords will continue to work until they expire or are manually reset. At that point, the new 20-character minimum will apply.

## A-AuditDC

#### What it means

**Audit policies** determine what events are logged on your Domain Controllers. Without proper auditing, attacks like credential theft, unauthorized group changes, or replication abuse (DCSync) can happen without leaving any trace in the logs.

The AD Scan verifies that your Domain Controllers are configured to log the most important security events.

#### What the scan checks

The scan looks for **13 specific audit subcategories** that should be enabled via Group Policy on all Domain Controllers:

| Category                           | What it logs                                    |
| ---------------------------------- | ----------------------------------------------- |
| Kerberos Authentication Service    | Login attempts via Kerberos                     |
| Kerberos Service Ticket Operations | Service ticket requests (detects Kerberoasting) |
| Computer Account Management        | Computer joins/changes in AD                    |
| Security Group Management          | Group membership changes                        |
| User Account Management            | User creation, deletion, password resets        |
| DPAPI Activity                     | Data protection API usage (credential access)   |
| Process Creation                   | New processes started (detects malicious tools) |
| Logon / Logoff                     | Interactive and remote logins                   |
| Special Logon                      | Logins with elevated privileges                 |
| Authentication Policy Change       | Changes to authentication settings              |
| Sensitive Privilege Use            | Use of sensitive permissions                    |
| Security System Extension          | Security subsystem changes                      |

#### Why the alert may persist

1. **Settings applied via `auditpol` directly on the DC** are not detected. The AD Scan reads Group Policy files, not the local DC configuration. You must use a GPO.
2. **Conflicting GPOs:** One GPO enables the setting, but another GPO with higher priority disables it.
3. **GPO not linked to the Domain Controllers OU:** The GPO exists but is linked to a different OU.

#### How to diagnose

```powershell
# Check the effective audit policy on a Domain Controller
auditpol /get /category:*
```

If the local settings look correct but the alert persists, check that the settings come from a GPO (not a local override):

```powershell
# Verify which GPOs are linked to the Domain Controllers OU
Get-GPInheritance -Target "OU=Domain Controllers,$($(Get-ADDomain).DistinguishedName)"
```

#### How to fix

1. Open `gpmc.msc` (Group Policy Management Console)
2. Create or edit a GPO linked to `OU=Domain Controllers`
3. Navigate to: `Computer Configuration > Policies > Windows Settings > Security Settings > Advanced Audit Policy Configuration > Audit Policies`
4. Enable **Success and Failure** for all subcategories listed above
5. Run `gpupdate /force` on all Domain Controllers

Verify the fix:

```powershell
auditpol /get /category:"Account Logon"
auditpol /get /category:"Account Management"
auditpol /get /category:"Logon/Logoff"
```

## A-BadSuccessor

#### What it means

Windows Server 2025 introduced a new type of service account called **dMSA** (Delegated Managed Service Account). A security flaw was discovered where a regular user with certain permissions on an Active Directory folder (called an Organizational Unit, or OU) could create a dMSA and use it to gain full administrator access to the domain.

This attack is only possible when:

* You have at least one **Windows Server 2025** Domain Controller
* A **KDS Root Key** exists (required for managed service accounts)
* A non-admin user has the right to create objects in an OU

#### What the scan checks

The AD Scan looks for non-privileged users or groups that have permissions to create or control dMSA objects on any OU such as `CreateChild`, `GenericAll`, `WriteDacl`, or `WriteOwner`.

#### Why the alert may persist

* The fix must be applied on **every** affected OU, not just one.
* Permissions inherited from parent OUs still apply to child OUs.
* Custom admin groups may not be recognized as "privileged" by the scan.

#### How to diagnose

```powershell
# Check prerequisites
Get-KdsRootKey  # KDS Root Key must exist for the attack to be possible

Get-ADDomainController -Filter * |
  Where-Object { $_.OperatingSystem -like "*2025*" } |
  Select-Object Name, OperatingSystem  # At least one Win2025 DC

# Find OUs with risky delegations
Get-ADOrganizationalUnit -Filter * -Properties nTSecurityDescriptor |
  ForEach-Object {
    $ou = $_
    $ou.nTSecurityDescriptor.Access |
      Where-Object {
        -not $_.IsInherited -and
        $_.ActiveDirectoryRights -match 'GenericAll|CreateChild|WriteDacl|WriteOwner'
      } | ForEach-Object {
        [PSCustomObject]@{
          OU        = $ou.Name
          Principal = $_.IdentityReference
          Rights    = $_.ActiveDirectoryRights
        }
      }
  } | Format-Table -AutoSize
```

#### How to fix

For each flagged OU, remove the risky permissions from non-admin users:

```powershell
$ouDN = "OU=Servers,DC=yourdomain,DC=com"  # Replace with your OU
$path = "AD:$ouDN"
$acl = Get-Acl -Path $path

# Remove risky rights for a specific group
$identity = New-Object System.Security.Principal.NTAccount("YOURDOMAIN", "HelpDeskTeam")
$toRemove = $acl.Access | Where-Object {
  $_.IdentityReference -eq $identity -and
  $_.ActiveDirectoryRights -match 'CreateChild|GenericAll'
}

foreach ($ace in $toRemove) {
  $acl.RemoveAccessRuleSpecific($ace)
}
Set-Acl -Path $path -AclObject $acl
```

## A-AdminSDHolder

#### What it means

When a user is added to a privileged group in Active Directory (like Domain Admins), the system automatically marks their account with a flag called `adminCount = 1`. This flag is part of a built-in protection mechanism.

The problem is: **when you remove that user from the admin group, the flag is NOT automatically cleared.** It stays forever, unless you explicitly remove it.

The AD Scan flags accounts that have this leftover `adminCount` marker but are no longer actual administrators. This can indicate:

* A user who was temporarily made admin and the cleanup was incomplete
* A potential security concern: these accounts may still have elevated permissions in certain areas

#### What the scan checks

The scan finds enabled user accounts where `adminCount = 1` but the account is not currently a member of any privileged group.

#### How to diagnose

```powershell
# Build a list of all current privileged users
$privilegedMembers = @()
@("Domain Admins", "Enterprise Admins", "Schema Admins", "Administrators",
  "Account Operators", "Server Operators", "Backup Operators", "Print Operators"
) | ForEach-Object {
  try { $privilegedMembers += (Get-ADGroupMember $_ -Recursive).DistinguishedName } catch {}
}

# Find accounts with the leftover adminCount flag
Get-ADUser -LDAPFilter "(&(admincount=1)(!(userAccountControl:1.2.840.113556.1.4.803:=2)))" `
  -Properties adminCount |
  Where-Object { $_.DistinguishedName -notin $privilegedMembers } |
  Select-Object SamAccountName, Name
```

#### How to fix

After confirming that these accounts should no longer be administrators, clear the flag:

```powershell
# Clear adminCount for a single user
Set-ADUser -Identity "username" -Clear adminCount

# Or clear it for all non-privileged users at once
Get-ADUser -LDAPFilter "(&(admincount=1)(!(userAccountControl:1.2.840.113556.1.4.803:=2)))" |
  Where-Object { $_.DistinguishedName -notin $privilegedMembers } |
  ForEach-Object {
    Set-ADUser -Identity $_ -Clear adminCount
    Write-Host "Cleared adminCount for $($_.SamAccountName)"
  }
```

## A-NoGPOLLMNR

#### What it means

**LLMNR** (Link-Local Multicast Name Resolution) is a protocol that Windows uses to find other computers on the network when DNS doesn't have the answer. It works by broadcasting a question to all nearby machines: "Does anyone know where `servername` is?"

The problem is: an attacker on the same network can **respond to these broadcasts pretending to be the target server**, tricking the victim into sending their credentials to the attacker. This is called an **LLMNR poisoning** attack and is one of the most common techniques used during internal network attacks.

LLMNR is a legacy protocol that modern networks don't need (DNS handles everything). It should be disabled via Group Policy.

#### What the scan checks

The AD Scan looks for an **enabled and linked GPO** that disables LLMNR by setting the `EnableMulticast` registry value to `0`.

#### Why the alert may persist

1. **GPO exists but is not linked** to any OU.
2. **GPO is disabled**, even if linked.
3. **LLMNR was disabled manually** (via registry or `netsh`) but not through Group Policy. The AD Scan reads GPO files, not local machine settings.

#### How to diagnose

```powershell
Import-Module GroupPolicy
Get-GPO -All | ForEach-Object {
  [xml]$r = Get-GPOReport -Guid $_.Id -ReportType Xml
  if ($r.InnerXml -match "EnableMulticast") {
    Write-Host "GPO '$($_.DisplayName)' - Status: $($_.GpoStatus) - Contains LLMNR setting"
  }
}
```

#### How to fix

```powershell
$gpoName = "Disable LLMNR"
$gpo = Get-GPO -Name $gpoName -ErrorAction SilentlyContinue
if (-not $gpo) { $gpo = New-GPO -Name $gpoName }

Set-GPRegistryValue -Name $gpoName `
  -Key "HKLM\Software\Policies\Microsoft\Windows NT\DNSClient" `
  -ValueName "EnableMulticast" -Type DWord -Value 0

# Link to the domain root so it applies to all computers
New-GPLink -Name $gpoName `
  -Target (Get-ADDomain).DistinguishedName `
  -LinkEnabled Yes
```

After linking the GPO, run `gpupdate /force` on machines or wait for the next Group Policy refresh cycle (typically 90 minutes).


---

# 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/anomalies.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.
