SEKurity GmbH Logo
adPEAS

adPEAS v2 Episode 2: Under the Hood - Anatomy of a Scan

What happens when adPEAS scans an Active Directory? From authentication and LDAP queries to context-dependent severity ratings and caching -- a look under the hood.

Alexander Sturz

Founder & Red Team Lead

11 min read
Share:

What Actually Happens After You Hit Enter?

Episode 1 showed how to launch adPEAS. Invoke-adPEAS -Domain contoso.com — Enter — and then a whole lot happens. But what exactly? This episode covers what goes on under the hood when adPEAS performs a scan.

Don’t worry, this won’t get too technical. But having a basic understanding of the internal workflow is enormously helpful for troubleshooting when something doesn’t work as expected. And that will happen — Active Directory wouldn’t be Active Directory if something unexpected didn’t come up eventually.


Phase 1: The Connection

Everything starts with authentication. Depending on the parameters provided, adPEAS chooses the appropriate path:

+--------------------------------------------------------------------+
|                          Connect-adPEAS                            |
|                      (The Club's Bouncer)                          |
+---------------------------------+----------------------------------+
                                  |
     +--------------+-------------+-------------+--------------+
     |              |             |              |              |
     v              v             v              v              v
+----------+  +----------+  +-----------+  +------------+  +---------+
| Windows  |  | Kerberos |  |   NTLM    |  |   Simple   |  | Ticket  |
|  Auth    |  |  Native  |  | Imperson. |  |    Bind    |  | Import  |
| (Current |  | (TGT->TGS|  | (runas    |  | (Username  |  | (Kirbi/ |
|  User)   |  |  ->PTT)  |  | /netonly) |  | +Password) |  | Ccache) |
+----------+  +----------+  +-----------+  +------------+  +---------+

Fallback with credentials: Kerberos -> NTLM Impersonation -> SimpleBind

The most common case is probably Windows Auth — logging into the domain with the current account. That’s straightforward: adPEAS uses the Windows credential cache of the current session.

Things get more interesting with explicit credentials. In that case, adPEAS first tries native Kerberos:

  1. Get a TGT — Request a Ticket Granting Ticket from the KDC
  2. Get a TGS — Use the TGT to request a Service Ticket for LDAP
  3. PTT — Import the ticket into the Windows session (Pass-the-Ticket)
  4. Connect — Establish the LDAP connection, Kerberos is used automatically

Why all this effort? Because Kerberos is the standard in enterprise environments. Many DCs have SimpleBind disabled or only allow it over LDAPS. With Kerberos, you’re on the safe side.

If Kerberos doesn’t work — port 88 blocked, DNS issues, etc. — adPEAS automatically falls back to NTLM Impersonation (supports LDAP Signing). If that also fails, SimpleBind is attempted. A warning is displayed, but the scan continues regardless. (Note: PTT does not require admin privileges — adPEAS uses LsaConnectUntrusted.)


Phase 2: The LDAPContext Object

Once the connection is established, adPEAS gathers basic information about the domain. This is stored in $Script:LDAPContext — the notebook that adPEAS uses throughout the entire scan:

$Script:LDAPContext = @{
    Domain                     = "contoso.com"
    DomainDN                   = "DC=contoso,DC=com"
    DomainSID                  = "S-1-5-21-3623811015-..."
    Server                     = "DC01.contoso.com"
    DefaultNamingContext       = "DC=contoso,DC=com"
    ConfigurationDN            = "CN=Configuration,DC=contoso,DC=com"
    SchemaNamingContext        = "CN=Schema,CN=Configuration,DC=contoso,DC=com"
    RootDomainNamingContext    = "DC=contoso,DC=com"
    # ... and quite a bit more (Protocol, Port, AuthMethod, etc.)
}

Why does this matter? Because these values are needed everywhere. When a check module wants to know which SIDs belong to “Domain Admins,” it needs to know <DomainSID>-512. For ACL analysis, the Schema DN is needed to look up attribute GUIDs. And so on.

The LDAPContext object is adPEAS’s memory for the current session. Once populated, it’s shared across all modules.


Phase 3: The LDAP Engine

Now it gets technically interesting. At the heart of adPEAS sits Invoke-LDAPSearch — a single function that every LDAP query flows through. Whether it’s users, computers, or ACLs — everything goes through this channel:

# A typical call looks like this (simplified):
$result = Invoke-LDAPSearch -Filter "(objectClass=user)" -Properties "sAMAccountName","memberOf"

Why a centralized function instead of direct LDAP calls scattered throughout the code?

Error handling in one place: If the connection drops or a timeout occurs, it only needs to be handled in one spot. Every module benefits automatically.

Paging: LDAP has a limit on how many results are returned per request. In large domains with thousands of objects, paging is required — meaning multiple requests are made and the results are stitched together. Invoke-LDAPSearch handles this automatically.

Logging: With -Verbose enabled, every single LDAP filter that gets executed is displayed. Very helpful for debugging.

# With Verbose:
Invoke-adPEAS -Domain "contoso.com" -Verbose

# Output (truncated):
# VERBOSE: [Invoke-LDAPSearch] Filter: (objectClass=user)
# VERBOSE: [Invoke-LDAPSearch] Returned 1523 results
# VERBOSE: [Invoke-LDAPSearch] Filter: (objectClass=group)
# VERBOSE: [Invoke-LDAPSearch] Returned 342 results

Phase 4: The Check Modules

Now comes the actual scan. adPEAS has over 40 check modules that are executed sequentially (or in groups). Each module is responsible for a specific aspect of AD security.

The workflow is always the same:

+------------------------------------------------------------+
|                        Check Module                        |
|             (e.g. Get-KerberoastableAccounts)              |
+-----------------------------+------------------------------+
                              |
        +---------------------+---------------------+
        v                     v                     v
  +------------+        +----------+         +------------+
  | 1. Ensure  |        | 2. Query |         | 3. Analyze |
  | Connection |        | Data     |         | & Return   |
  +------------+        +----------+         +------------+

Step 1: Ensure Connection

Every check module starts with the same code:

if (-not (Ensure-LDAPConnection @PSBoundParameters)) {
    return $null
}

This verifies whether a valid session exists. If not, one is automatically established (when credentials were provided for a standalone call) or the check is skipped.

Step 2: Query Data

Now the module retrieves the required data via the Get-Domain* functions from the core modules:

# Example: Finding Kerberoastable accounts
$users = Get-DomainUser -LDAPFilter "(servicePrincipalName=*)"

Step 3: Analyze and Return

The retrieved data is analyzed and enriched with severity information:

foreach ($user in $users) {
    # Determine severity
    $user | Add-Member -NotePropertyName '_adPEASObjectType' -NotePropertyValue 'Kerberoastable' -Force

    # Pass to reporting pipeline
    Show-Object -Object $user
}

The Severity System

Every finding that adPEAS discovers gets a severity rating. This sounds simple at first, but it’s not — the rating is context-dependent.

The base categories:

SeveritySymbolColorMeaning
Finding[!]RedSecurity issue, action required
Hint[+]YellowInteresting, should be investigated
Note[*]GreenInformation, no risk
Secure[#]Red on YellowGood configuration

What makes this special is the context-dependent rating. Example: “Password Never Expires”:

# For a regular user: Hint (yellow)
# Interesting, but not a major risk

# For a Domain Admin: Finding (red)
# This is a real risk!

# For a gMSA (Managed Service Account): Note (green)
# This is normal and expected for gMSAs

Technically, this works through the Get-AttributeSeverity function, which evaluates multiple factors:

  1. Which attribute? — Some attributes are inherently more critical than others
  2. What value? — Some values are more problematic than others
  3. What context? — Is the account privileged? Which groups? What type?
# Simplified example:
function Get-AttributeSeverity {
    param($Attribute, $Value, $Object)

    # Check if the account is privileged
    $isPrivileged = Test-IsPrivileged -Object $Object

    if ($Attribute -eq 'PasswordNeverExpires' -and $Value -eq $true) {
        if ($isPrivileged) {
            return "Finding"  # Red for Domain Admins
        }
        return "Hint"  # Yellow for regular users
    }
    # ... further logic
}

SID-Based Checks

An important aspect that shouldn’t be overlooked: adPEAS performs all permission checks based on SIDs, not names.

Why? Because Active Directory is used internationally. The group “Domain Admins” is called “Domänen-Admins” on a German DC and something entirely different in other languages. Name-based checks would fail on localized systems. That’s why adPEAS checks the SID:

# WRONG - Only works on English systems:
if ($group.Name -eq "Domain Admins") { ... }

# RIGHT - Works everywhere:
if ($group.SID -match "-512$") { ... }  # Domain Admins always have SID *-512

Phase 5: Reporting

At the end of a scan (or in parallel), all findings are collected and formatted. Internally, adPEAS uses a Unified RenderModel Pipeline: when a check calls Show-Object $user, Get-RenderModel builds a renderer-agnostic data model — severity classification, attribute ordering, and transformer logic are determined once. From there, Render-ConsoleObject (ANSI colors) and Render-HtmlObject (HTML with tooltips) each produce their respective output. Details on this in Episode 5: Output & Reports.

Depending on the selected output options:

Console Output (Default)

Results are displayed directly in the console — color-coded and structured. Ideal for quick checks when you need to see at a glance what’s going on.

HTML Report

Using -Outputfile, an interactive HTML report is generated with:

  • Security Score (overall rating)
  • Finding Cards (expandable details)
  • Tooltips with explanations and remediation steps
  • Search and filter functionality
  • Dark mode

The Caching System

Caching deserves its own section — though it’s important to understand what adPEAS caches and what it doesn’t.

What is NOT cached: LDAP query results. Every call to Get-DomainUser, Get-DomainComputer, or Get-DomainGroup goes fresh to the DC. If two different check modules both call Get-DomainUser -LDAPFilter "(servicePrincipalName=*)", two separate LDAP queries are actually made. This is intentional — the checks always work with current data.

What IS cached: The expensive side operations that occur during analysis. First and foremost, SID-to-name resolution. ConvertFrom-SID has its own cache:

$Script:SIDResolutionCache = @{
    'S-1-5-21-3623811015-...-512' = 'CONTOSO\Domain Admins'
    'S-1-5-21-3623811015-...-519' = 'CONTOSO\Enterprise Admins'
    # ... populated during the scan
}

In large domains with thousands of ACLs, without caching adPEAS would have to resolve the same SID hundreds of times. With caching, it happens once and the result is stored.

Beyond the SID cache, there are several others:

  • Global Catalog Connection ($Script:GCConnection) — When adPEAS encounters SIDs from foreign domains in the forest during trust analysis or ACL checks, ConvertFrom-SID automatically establishes a connection to the Global Catalog (port 3268, or 3269 for LDAPS). This connection is cached for the entire session and cleaned up during Disconnect-adPEAS. If the GC is unreachable, adPEAS falls back to a RID-based format (e.g., FOREIGN\DomainAdmins (RID:512)).
  • Privileged-Check Cache — Remembers whether a SID belongs to a privileged group. Since the privilege check resolves recursive group memberships, this saves a significant number of queries.
  • Group-Membership Cache — Stores recursive group memberships resolved via the LDAP Matching Rule filter.
  • DNS Cache — Hostnames to IP addresses, so DNS isn’t queried on every operation.

The result: significantly faster scans, less load on the DC — and still always current data for the actual LDAP queries.


Troubleshooting: What to Do When Things Get Stuck

To wrap up, here are some tips in case the scan doesn’t run as expected:

“No LDAP connection available”

The connection to the DC isn’t possible. Things to check:

  • Is port 389 (LDAP) or 636 (LDAPS) reachable?
  • Is the domain name correct?
  • Are the credentials valid?
# Quick test:
Test-NetConnection dc01.contoso.com -Port 389

“The server is not operational”

A classic. The DC isn’t responding or DNS resolution isn’t working.

# Check DNS:
Resolve-DnsName contoso.com
Resolve-DnsName _ldap._tcp.dc._msdcs.contoso.com -Type SRV

Kerberos isn’t working

If native Kerberos auth doesn’t work, adPEAS first tries NTLM Impersonation. If that also fails, SimpleBind is attempted. Possible reasons for failure:

  • Port 88 (Kerberos) is blocked
  • Restrictive security policies (LSA access restricted)
  • DNS issues with KDC resolution

Use -ForceSimpleBind to skip Kerberos and NTLM, or -ForceNTLM to force NTLM Impersonation:

# SimpleBind - send credentials directly to the DC
Connect-adPEAS -Domain "contoso.com" -Credential $cred -ForceSimpleBind

# NTLM Impersonation - similar to "runas /netonly", preserves existing Kerberos tickets
Connect-adPEAS -Domain "contoso.com" -Credential $cred -ForceNTLM

The scan is slow

With very large domains (100k+ objects), a scan can take a while. Some measures:

  • Run only specific modules: -Module Domain,Accounts
  • OPSEC mode (fewer queries): -OPSEC
  • Specify a DC instead of auto-discovery: -Server DC01.contoso.com

Summary

Here’s a recap of the phases:

  1. Connection — Authentication and session setup
  2. Context — Gathering domain information
  3. Checks — Modules perform their analyses
  4. Severity — Context-dependent rating of findings
  5. Reporting — Formatting and output

Understanding these phases helps with troubleshooting and with properly interpreting the results.

In the upcoming episodes, we’ll dive deep into authentication. We’ll look at how adPEAS implements Kerberos natively, what Pass-the-Hash and Pass-the-Key mean, and how certificate-based authentication works via PKINIT.


← Episode 1: Introduction | Episode 3: Authentication — coming soon

About the Author

Alexander Sturz

Founder & Red Team Lead

Active Directory Ninja and offensive security expert specializing in enterprise infrastructure compromise and post-exploitation techniques.

Related Articles