Overview
In a modern Active Directory environment, identifying insecure LDAP authentication is critical for maintaining directory security. This script performs a centralized audit of insecure LDAP bind events (Event ID 2889) across all Domain Controllers in the forest. It validates network connectivity, remotely queries the Directory Service event log using WMI, extracts relevant client and authentication data, and exports the results into a consolidated CSV report for analysis.
The script is designed for environments where WinRM may be restricted, leveraging RPC/WMI instead.
Why Monitor Insecure LDAP Binds?
Event ID 2889 is generated when a client performs:
- Unsigned LDAP bind
- Simple bind over non-SSL connection
These authentication methods expose credentials to interception and are commonly seen with:
- Legacy applications
- Misconfigured appliances
- Hardcoded LDAP integrations
- Non-secure service accounts
Identifying these binds is the first step toward enforcing LDAP signing and channel binding policies.
Script Architecture
The script follows a structured workflow:
- Accept a configurable time window (default: 24 hours).
- Discover all Domain Controllers dynamically.
- Validate network connectivity (ICMP and RPC).
- Query Directory Service logs for Event ID 2889.
- Parse and normalize event data.
- Export a consolidated CSV report.
This ensures reliability, consistency, and safe execution across multiple domain controllers.
Event Data Parsing and Normalization
Each event contains embedded metadata within InsertionStrings. The script extracts:
- Domain Controller name
- Client IP address
- Source port
- Username used for bind
- Bind type (Unsigned or Simple)
- Reverse DNS resolution (if possible)
The bind type is normalized from numeric values to readable labels, improving clarity in reporting.
Reverse DNS Resolution
If a client IP address is available, the script attempts a reverse DNS lookup to identify the hostname. While optional, this significantly improves traceability when investigating insecure applications or legacy systems.
Failures in DNS resolution are handled gracefully and do not interrupt execution.
Practical Use Cases
This script is valuable in several scenarios:
- Pre-hardening assessment before enforcing LDAP signing
- Security compliance audits
- Identifying legacy systems before domain modernization
- Incident response involving credential exposure concerns
- Preparing for secure LDAP policy enforcement
It is especially useful in hybrid environments or during modernization projects where legacy applications are still present.
# Requires Active Directory module
Param (
[parameter(Mandatory=$false,Position=0)][Int]$Hours = 24
)
Import-Module ActiveDirectory
function Test-TcpPortWithTimeout {
param (
[string]$ComputerName,
[int]$Port = 135,
[int]$TimeoutSeconds = 15
)
try {
$tcpClient = New-Object System.Net.Sockets.TcpClient
$iar = $tcpClient.BeginConnect($ComputerName, $Port, $null, $null)
$wait = $iar.AsyncWaitHandle.WaitOne($TimeoutSeconds * 1000, $false)
if (!$wait) {
$tcpClient.Close()
return $false
}
$tcpClient.EndConnect($iar)
$tcpClient.Close()
return $true
} catch { return $false }
}
$outputDir = "D:\LDAP"
if (!(Test-Path -Path $outputDir)) {
New-Item -ItemType Directory -Path $outputDir
}
$dateString = Get-Date -Format "yyyy-MM-dd"
$DomainControllers = Get-ADDomainController -Filter *
$AllInsecureLDAPBinds = @()
$since = (Get-Date).AddHours(-$Hours)
foreach ($DC in $DomainControllers) {
$ComputerName = $DC.HostName
Write-Host "Checking connectivity to $ComputerName..."
# Ping check
$pingReply = Test-Connection -ComputerName $ComputerName -Count 1 -Quiet -ErrorAction SilentlyContinue
if (-not $pingReply) {
Write-Warning "$ComputerName is not responding to ping. Skipping."
continue
}
# RPC port 135 check
if (-not (Test-TcpPortWithTimeout -ComputerName $ComputerName -Port 135 -TimeoutSeconds 15)) {
Write-Warning "Cannot connect to RPC (port 135) on $ComputerName. Skipping."
continue
}
Write-Host "Network check passed for $ComputerName. Querying WMI for event logs..."
# Pull events via WMI
try {
$wmiQuery = "SELECT * FROM Win32_NTLogEvent WHERE Logfile='Directory Service' AND EventCode=2889"
$Events = Get-WmiObject -ComputerName $ComputerName -Query $wmiQuery -ErrorAction Stop
$FilteredEvents = $Events | Where-Object {
$eventDate = $_.TimeGenerated
$dt = [System.Management.ManagementDateTimeConverter]::ToDateTime($eventDate)
$dt -ge $since
}
} catch {
Write-Warning "Failed to query WMI for $ComputerName : $_"
continue
}
foreach ($Event in $FilteredEvents) {
$Client = $Event.InsertionStrings[0]
$User = $Event.InsertionStrings[1]
$BindTypeRaw = $Event.InsertionStrings[2]
$IPAddress = $null
$Port = $null
if ($Client -and $Client.LastIndexOf(":") -gt 0) {
$IPAddress = $Client.Substring(0, $Client.LastIndexOf(":"))
$Port = $Client.Substring($Client.LastIndexOf(":")+1)
}
switch ($BindTypeRaw) {
"0" { $BindType = "Unsigned" }
"1" { $BindType = "Simple" }
default { $BindType = "Unknown" }
}
# Optional: Resolve client computer name (reverse DNS lookup)
$ClientName = $null
if ($IPAddress) {
try {
$ClientName = ([System.Net.Dns]::GetHostEntry($IPAddress)).HostName
} catch {
$ClientName = $null
}
}
$Row = "" | select DCName,ClientName,IPAddress,Port,User,BindType
$Row.DCName = $ComputerName
$Row.ClientName = $ClientName
$Row.IPAddress = $IPAddress
$Row.Port = $Port
$Row.User = $User
$Row.BindType = $BindType
$AllInsecureLDAPBinds += $Row
}
Write-Host "Events collected from $ComputerName."
}
$outputFile = Join-Path $outputDir "InsecureLDAPBinds_WMI_AllDC_$dateString.csv"
if ($AllInsecureLDAPBinds.Count -gt 0) {
Write-Host "$($AllInsecureLDAPBinds.Count) records saved to $outputFile"
$AllInsecureLDAPBinds | Export-Csv -NoTypeInformation -Path $outputFile
} else {
Write-Host "No records found."
}

