Managing Active Directory (AD) security can be challenging, particularly when identifying accounts sharing passwords or using weak authentication practices. Enter LazyAdminFinder, a powerful, lightweight PowerShell tool that quickly reveals shared password usage and improves your organization's AD security posture.
What is LazyAdminFinder?
LazyAdminFinder is a PowerShell script leveraging the DSInternals module to:
- Detect and report shared NTLM password hashes.
- Archive and compare historical hash data to identify password reuse.
- Provide comprehensive reports that pinpoint risky account behaviors.
Designed specifically for Active Directory administrators, LazyAdminFinder provides quick, actionable insights into password security.
Why Use LazyAdminFinder?
-
Security Improvement: Identify weak or compromised passwords.
-
Compliance: Maintain AD integrity for audit and compliance purposes.
-
Efficiency: Quickly audit accounts without manually reviewing data.
How Does LazyAdminFinder Work?
Fetch and Filter User Accounts
The script collects AD user accounts, filtering out disabled, machine, and service accounts to focus only on active users.
NTLM Hash Extraction and Archiving
LazyAdminFinder extracts NTLM hashes from accounts and securely archives their SHA-256 hashes for historical comparison.
Password Reuse Detection
By comparing the current run to previous archives, the tool detects password reuse, flagging accounts with potential security risks. This helps identify:
- Admins or privileged users synchronizing passwords between standard and administrative accounts.
- Users, especially admins, repeatedly resetting their passwords to previously used values.
Shared Password Grouping
The script identifies and groups accounts sharing identical NTLM hashes, which can indicate either intentional password sharing or multiple users independently choosing the same password—often due to weak password habits. For example, if several users select a common password like "Summer2025!", LazyAdminFinder will group these accounts together, allowing you to quickly spot both potential shared passwords and risky parallel thinking in password creation.
Recommended Frequency of Use
Consider running LazyAdminFinder regularly based on your organization's password reset policy. For instance, if your policy mandates a password reset every 90 days, schedule the script to run approximately every 100 days. This timing ensures you capture instances where admins or privileged users might reset their passwords back to previously used ones. If your organization doesn't enforce a password reuse policy, running LazyAdminFinder periodically becomes even more crucial to identify and mitigate potential security risks.
Installing LazyAdminFinder
Step 1: Preparation
- Ensure PowerShell is installed on your system.
- Install the required module:
Install-Module DSInternals
Step 2: Configure the Script
Customize these essential parameters within the provided script:
- Domain Controller:
$dc = "your-domain-controller.example.com"
- Naming Context:
$nc = "DC=yourdomain,DC=com"
- Output Report Path:
$outputReport = "C:\Path\To\SharedPasswordsReport.csv"
- Archive Directory:
$archiveDir = "C:\Path\To\HashArchives"
Step 3: Run the Script
Execute the script as a Domain admin:
powershell.exe -ExecutionPolicy Bypass -File "C:\Path\To\LazyAdminFinder.ps1"
Reviewing the Results
- A detailed CSV report is generated, clearly highlighting accounts sharing passwords or independently choosing weak passwords.
- Historical archives are stored securely for future comparisons, ensuring robust password management.
Use Cases for LazyAdminFinder
- Security Audits: Regularly identify and remediate shared, weak, or compromised passwords.
- Incident Response: Quickly detect compromised accounts following security incidents.
- Routine Compliance Checks: Ensure ongoing compliance by maintaining robust account security practices.
- Privileged Account Management: Specifically target administrators or privileged users who may reuse or synchronize passwords across multiple accounts.
Customizing LazyAdminFinder
Feel free to adjust the script according to your environment and needs:
- Modify filtering logic to include/exclude specific account types.
- Adjust archival and reporting paths for your storage preferences.
- Integrate additional reporting capabilities as required.
Benefits of Using LazyAdminFinder
-
Rapid Security Assessments: Automates complex AD security checks.
-
Enhanced Visibility: Clearly identifies potential security risks within AD.
-
Proactive Defense: Enables administrators to swiftly address vulnerabilities before exploitation.
LazyAdminFinder simplifies the critical task of password auditing in Active Directory, empowering administrators to maintain a secure, compliant, and efficient IT environment.
Discover shared passwords, remediate security risks, and fortify your AD security effortlessly with LazyAdminFinder.
#Another /\_[]_/\
# fine |] _||_ [|
# ___ \/ || \/
# /___\ ||
# (|0 0|) ||
# __/{\U/}\_ ___/vvv
# / \ {~} / _|_P|
# | /\ ~ /_/ []
# |_| (____)
# \_]/______\ Barberion
# _\_||_/_ Production
# (_,_||_,_)
#
Import-Module DSInternals
# === Parameters ===
$dc = "your-domain-controller.example.com"
$nc = "DC=yourdomain,DC=com"
$outputReport = "C:\Path\To\SharedPasswordsReport.csv"
$archiveDir = "C:\Path\To\HashArchives"
# Ensure archive directory exists
if (-not (Test-Path $archiveDir)) {
New-Item -ItemType Directory -Path $archiveDir | Out-Null
}
$timestamp = (Get-Date).ToString("yyyyMMdd_HHmmss")
# === 1. Fetch & filter AD user accounts ===
Write-Host "[+] Fetching AD user accounts (skipping machines, service & disabled)..."
$filteredAccounts = Get-ADReplAccount -All -Server $dc -NamingContext $nc |
Where-Object { $_.NTHash -and $_.SamAccountName } |
Where-Object { $_.SamAccountName -notmatch '\$$' } |
Where-Object { $_.SamAccountName -notmatch '^(?i)(svc|service)' } |
Where-Object {
if ($_.PSObject.Properties.Match('IsDisabled').Count -gt 0) {
-not $_.IsDisabled
}
else {
# bit 2 = disabled
( ($_.UserAccountControl -band 2) -eq 0 )
}
}
if (-not $filteredAccounts) {
Write-Host "[-] No matching user accounts found. Exiting."
exit
}
# === 2. Convert NTLM byte array to hex string ===
$filteredAccounts | ForEach-Object {
$hex = [BitConverter]::ToString($_.NTHash) -replace '-', ''
$_ | Add-Member -MemberType NoteProperty -Name NTLMHashString -Value $hex -Force
}
# === 3. Archive SHA-256 of each NTLM hash ===
Write-Host "[+] Archiving SHA-256 of each NTLM hash..."
$sha256 = [System.Security.Cryptography.SHA256]::Create()
$currentArchive = $filteredAccounts | ForEach-Object {
$shaBytes = $sha256.ComputeHash($_.NTHash)
$shaHex = [BitConverter]::ToString($shaBytes) -replace '-', ''
[PSCustomObject]@{
SamAccountName = $_.SamAccountName
HashSha256 = $shaHex
}
}
$sha256.Dispose()
$archiveFile = Join-Path $archiveDir "UserHashArchive_$timestamp.csv"
$currentArchive | Export-Csv -Path $archiveFile -NoTypeInformation
# === 4. Compare to previous archive ===
$allArchives = Get-ChildItem -Path $archiveDir -Filter "UserHashArchive_*.csv" |
Sort-Object LastWriteTime -Descending
if ($allArchives.Count -gt 1) {
$previousFile = $allArchives[1].FullName
Write-Host "[+] Comparing this run to previous archive: $previousFile"
$prevData = Import-Csv $previousFile
$reused = Compare-Object `
-ReferenceObject $prevData `
-DifferenceObject $currentArchive `
-Property SamAccountName, HashSha256 `
-IncludeEqual |
Where-Object { $_.SideIndicator -eq '==' } |
Select-Object SamAccountName, HashSha256
if ($reused) {
$reuseReport = Join-Path $archiveDir "PasswordReuse_$timestamp.csv"
Write-Host "[!] $($reused.Count) accounts reused their old hash; exporting to $reuseReport"
$reused | Export-Csv -Path $reuseReport -NoTypeInformation
}
else {
Write-Host "[+] No password‑reuse detected this run."
}
}
else {
Write-Host "[+] Only one archive present; skipping reuse comparison."
}
# === 5. Shared‑password grouping & report ===
Write-Host "[+] Grouping accounts by shared NTLM hash..."
$sharedGroups = $filteredAccounts |
Group-Object -Property NTLMHashString |
Where-Object { $_.Count -gt 1 }
if (-not $sharedGroups) {
Write-Host "[-] No shared hashes found."
exit
}
$reportResults = @()
Write-Host "[+] Generating shared‑password report:"
foreach ($g in $sharedGroups) {
$h = $g.Name
$users = $g.Group | ForEach-Object { $_.SamAccountName }
$reportResults += [PSCustomObject]@{
SharedHash = $h
UserCount = $users.Count
Users = $users -join ', '
}
Write-Host '----------------------------------------------'
Write-Host "Shared Hash: $h"
Write-Host "User Count : $($users.Count)"
Write-Host "Users : $($users -join ', ')"
Write-Host '----------------------------------------------`n'
}
Write-Host "[+] Saving shared‑password report to $outputReport"
$reportResults | Export-Csv -Path $outputReport -NoTypeInformation
Write-Host "[Summary] Shared groups: $($reportResults.Count); Total archives: $($allArchives.Count)"