RSAT isn’t always available and importing PowerView raises flags. This post covers native PowerShell techniques for enumerating Active Directory - users, groups, SPNs, and trust relationships - using only built-in .NET classes and LDAP queries.

Why Native AD Enumeration

RSAT (Remote Server Administration Tools) is Microsoft’s administration toolkit for managing Windows infrastructure remotely. It includes tools such as:

  • Active Directory Users and Computers (ADUC)
  • Group Policy Management
  • DNS/DHCP management
  • PowerShell modules for AD

The RSAT component that is relevant in the context of this post is the PowerShell module: “Active Directory module for Windows PowerShell”. The ActiveDirectory module provides PowerShell cmdlets that communicate with domain controllers via LDAP, Kerberos, and AD Web Services, and includes such cmdlets as:

  • Get-ADUser
  • Get-ADComputer
  • Get-ADGroup
  • Get-ADDomain
  • Get-ADTrust and many more.

These cmdlets can be imported using the Import-Module ActiveDirectory command; their use simplifies enumeration and administration dramatically because it abstracts LDAP queries into easy cmdlets.

But what if RSAT is not installed in the environment you’re Pen-Testing?

The standard toolchain for AD enumeration is well-known: BloodHound, PowerView, SharpHound (and their varients). That’s also why these tools are flagged (by signatures) in most EDRs. When you need to be quiet, native .NET LDAP queries through System.DirectoryServices are far less likely to trigger. None of the following is a “novel technique”. The goal is a clean, minimal reference for common queries.

Go Native — PowerShell AD enumeration without RSAT

Connecting to LDAP

The DirectoryEntry and DirectorySearcher classes are in the GAC on every Windows machine. No imports needed.

# Connect to domain using current user's credentials
function Get-LDAPConnection {
    param([string]$Domain = $env:USERDNSDOMAIN)

    $dn = "DC=" + ($Domain -replace "\.", ",DC=")
    $entry = New-Object System.DirectoryServices.DirectoryEntry("LDAP://$dn")
    $searcher = New-Object System.DirectoryServices.DirectorySearcher($entry)
    $searcher.PageSize = 1000
    $searcher.SizeLimit = 0

    return $searcher
}

Enumerating Domain Users

function Get-DomainUsers {
    param(
        [string]$Domain = $env:USERDNSDOMAIN,
        [switch]$Enabled,
        [switch]$AdminsOnly
    )

    $searcher = Get-LDAPConnection -Domain $Domain
    $searcher.Filter = "(&(samAccountType=805306368))"

    if ($Enabled)    { $searcher.Filter = "(&(samAccountType=805306368)(!(userAccountControl:1.2.840.113556.1.4.803:=2)))" }
    if ($AdminsOnly) { $searcher.Filter = "(&(samAccountType=805306368)(adminCount=1))" }

    $searcher.PropertiesToLoad.AddRange(@(
        "samAccountName", "displayName", "mail",
        "userAccountControl", "lastLogonTimestamp",
        "memberOf", "description"
    ))

    $results = $searcher.FindAll()

    foreach ($result in $results) {
        $props = $result.Properties
        [pscustomobject]@{
            Username    = $props["samaccountname"][0]
            DisplayName = if ($props["displayname"].Count)  { $props["displayname"][0]  } else { "" }
            Email       = if ($props["mail"].Count)         { $props["mail"][0]         } else { "" }
            Description = if ($props["description"].Count)  { $props["description"][0]  } else { "" }
            UAC         = if ($props["useraccountcontrol"].Count) { $props["useraccountcontrol"][0] } else { 0 }
            LastLogon   = if ($props["lastlogontimestamp"].Count) {
                [datetime]::FromFileTime($props["lastlogontimestamp"][0])
            } else { $null }
        }
    }
}

Usage:

# All enabled users
Get-DomainUsers -Enabled | Format-Table Username, DisplayName, LastLogon

# Users with adminCount=1 (directly or indirectly in privileged groups)
Get-DomainUsers -AdminsOnly | Select-Object Username, Description

Enumerating Service Principal Names

SPNs are the first step toward Kerberoasting. Any authenticated user can query them.

function Get-Kerberoastable {
    param([string]$Domain = $env:USERDNSDOMAIN)

    $searcher = Get-LDAPConnection -Domain $Domain
    # Users with SPNs that aren't krbtgt or machine accounts
    $searcher.Filter = "(&(samAccountType=805306368)(servicePrincipalName=*)(!(samAccountName=krbtgt)))"
    $searcher.PropertiesToLoad.AddRange(@("samAccountName", "servicePrincipalName", "memberOf", "pwdLastSet"))

    $searcher.FindAll() | ForEach-Object {
        $props = $_.Properties
        $props["serviceprincipalname"] | ForEach-Object {
            [pscustomobject]@{
                Username   = $props["samaccountname"][0]
                SPN        = $_
                PwdLastSet = if ($props["pwdlastset"].Count) {
                    [datetime]::FromFileTime($props["pwdlastset"][0])
                } else { $null }
            }
        }
    }
}
Get-Kerberoastable | Sort-Object PwdLastSet | Format-Table -AutoSize

Enumerating Group Membership

function Get-GroupMembers {
    param(
        [Parameter(Mandatory)]
        [string]$GroupName,
        [string]$Domain = $env:USERDNSDOMAIN,
        [switch]$Recurse
    )

    $searcher = Get-LDAPConnection -Domain $Domain
    $searcher.Filter = "(&(objectClass=group)(samAccountName=$GroupName))"
    $searcher.PropertiesToLoad.AddRange(@("member", "distinguishedName"))

    $group = $searcher.FindOne()
    if (-not $group) {
        Write-Warning "Group '$GroupName' not found"
        return
    }

    $members = $group.Properties["member"]
    $output  = @()

    foreach ($memberDN in $members) {
        $memberSearcher = Get-LDAPConnection -Domain $Domain
        $memberSearcher.Filter = "(distinguishedName=$memberDN)"
        $memberSearcher.PropertiesToLoad.AddRange(@("samAccountName", "objectClass", "distinguishedName"))

        $memberObj = $memberSearcher.FindOne()
        if (-not $memberObj) { continue }

        $mp = $memberObj.Properties
        $objType = $mp["objectclass"] | Select-Object -Last 1

        $entry = [pscustomobject]@{
            Name  = $mp["samaccountname"][0]
            Type  = $objType
            DN    = $mp["distinguishedname"][0]
        }
        $output += $entry

        # Recursively expand nested groups
        if ($Recurse -and $objType -eq "group") {
            $output += Get-GroupMembers -GroupName $mp["samaccountname"][0] -Domain $Domain -Recurse
        }
    }

    $output | Sort-Object Type, Name -Unique
}
# Direct members of Domain Admins
Get-GroupMembers -GroupName "Domain Admins"

# All members including nested groups
Get-GroupMembers -GroupName "Domain Admins" -Recurse | Format-Table Name, Type

Domain Trust Enumeration

function Get-DomainTrusts {
    param([string]$Domain = $env:USERDNSDOMAIN)

    $searcher = Get-LDAPConnection -Domain $Domain
    $searcher.SearchRoot.Path = "LDAP://CN=System," + (
        "DC=" + ($Domain -replace "\.", ",DC=")
    )
    $searcher.Filter = "(objectClass=trustedDomain)"
    $searcher.PropertiesToLoad.AddRange(@(
        "name", "trustDirection", "trustType", "trustAttributes"
    ))

    $directionMap = @{ 1 = "Inbound"; 2 = "Outbound"; 3 = "Bidirectional" }
    $typeMap      = @{ 1 = "Downlevel"; 2 = "Uplevel"; 3 = "MIT"; 4 = "DCE" }

    $searcher.FindAll() | ForEach-Object {
        $p = $_.Properties
        [pscustomobject]@{
            TrustedDomain = $p["name"][0]
            Direction     = $directionMap[[int]$p["trustdirection"][0]]
            Type          = $typeMap[[int]$p["trusttype"][0]]
            Attributes    = $p["trustattributes"][0]
        }
    }
}

Putting It All Together — Quick Recon Script

$domain = $env:USERDNSDOMAIN
$ts     = Get-Date -Format "yyyyMMdd_HHmmss"
$outDir = "$env:TEMP\adrecon_$ts"
New-Item -ItemType Directory -Path $outDir -Force | Out-Null

Write-Host "[*] Domain: $domain"
Write-Host "[*] Output: $outDir"

# Admins
Get-DomainUsers -AdminsOnly |
    Export-Csv "$outDir\admin_users.csv" -NoTypeInformation
Write-Host "[+] Admin users written"

# Kerberoastable accounts
Get-Kerberoastable |
    Export-Csv "$outDir\kerberoastable.csv" -NoTypeInformation
Write-Host "[+] Kerberoastable accounts written"

# DA members
Get-GroupMembers -GroupName "Domain Admins" -Recurse |
    Export-Csv "$outDir\domain_admins.csv" -NoTypeInformation
Write-Host "[+] Domain Admin members written"

# Trusts
Get-DomainTrusts |
    Export-Csv "$outDir\trusts.csv" -NoTypeInformation
Write-Host "[+] Trust relationships written"

Write-Host "`n[+] Done. Review files in $outDir"

Opsec Notes

  • These queries go over LDAP (port 389) or LDAPS (636) to a domain controller. They look like normal Windows traffic but generate event IDs 4662 (directory service access) on DCs with advanced auditing enabled.
  • PageSize = 1000 issues multiple requests for large directories. If you need to be stealthier, reduce page size and add Start-Sleep between pages.
  • The .NET classes are loaded in-process in your PowerShell runspace — no child processes, no files on disk.

None of this replaces tools such as BloodHound for visualizing the full attack path graph; but for a quick, quiet check of the basics, native LDAP queries keep the noise down.