Powershell : Detecting Password Reuse (avoiding policy)


This script and article requires you to have not the ntds.dit (this is the active directory database) or even the system registry file - this particular article requires you to have already processed these files with secretsdump.py.

This particular script as the name suggests will output usernames, and hashes and it also requires the last couple of files in a password protected 7zip archive where the password must be kept separately (This means you need to know the password to run the script due to the sensitive data)

The Password Policy Loophole

Many organizations implement mandatory password rotation policies that require users to change their passwords every 60-90 days. However, there's a common compliance loophole that users exploit: they simply change their password... back to the same one they had before. This practice undermines security policies and creates significant vulnerabilities, but it often goes undetected.

You may be reading this and thinking that is a policy that governs this and you would be right, this particular policy is called “Remembered last passwords” alternatively, other names can be “password history requirements” - this is an integer value that is set to 20 for example will remember the last 20 password you’ve used and not allow you to reuse them.

Unfortunately, as of many things, there are certain administrative conditions where this policy can be completely bypassed and ignored.

This post outlines a PowerShell script that can analyze multiple NTDS password audit extracts to identify users who maintain the same password across multiple audits, revealing who is circumventing password change requirements.

How The Script Works

The script operates on encrypted 7-Zip archives containing NTDS password extracts from multiple audits (e.g., 2023, 2024, 2025). This ensures sensitive password data remains protected during analysis.


Warning : The password for ntds-extracts.7z is never stored with the script, but in a password manager this is not automated for security reasons, as you can see below a user needs to enter the password:


# Define encrypted archive path and expected files

$encryptedArchive = ".\ntds-extracts.7z"
$ntdsFiles = @{
    "April2024" = "ntlm-extract.2024.1"
    "September2024" = "ntlm-extract.2024.2"
    "April2025" = "ntlm-extract.2025"
}

# Extract files with password protection
function Expand-EncryptedArchive {
    param (
        [string]$ArchivePath,
        [string]$OutputPath,
        [string]$Password
    )
    
    $arguments = @(
        "x",                    # Extract command
        "`"$ArchivePath`"",    # Archive path
        "-o`"$OutputPath`"",   # Output path
        "-y"                   # Assume Yes
    )
    
    if ($Password) {
        $arguments += "-p$Password"
    }
    
    # Execute extraction with proper error handling
    $process = Start-Process $7zipPath -ArgumentList $arguments -NoNewWindow -Wait -PassThru
    # ...
}

Key security features:

  • Password-protected 7-Zip archives with header encryption
  • Temporary working directory that's securely cleaned up after analysis
  • No plaintext passwords - only analyses NTLM hashes

Account Filtering

Rather than analyzing all accounts, the script uses intelligent filtering to focus on genuine user accounts:

function Test-AccountShouldBeExcluded {
    param ([string]$Username)
    
    # Remove domain prefix if present
    $accountName = if ($Username -match "^[^\\]+\\(.+)$") {
        $matches[1]
    } else {
        $Username
    }
    
    # Check for exclusion criteria
    # 1. Computer accounts (ending with $)
    if ($accountName -match '\$$') {
        return $true
    }
    
    # 2. Accounts starting with zz_
    if ($accountName -like 'arc_*') {
        return $true
    }
    
    # 3. Specific account names to exclude
    $excludedNames = @('Bear', 'ADDS')
    if ($excludedNames -contains $accountName) {
        return $true
    }
    
    return $false
}

This ensures the analysis focuses on human users who would be subject to password policies.

Hash Consistency Analysis

The script performs the following analysis:

function Get-PasswordHashes {
    param ([string]$NTDSFilePath)
    
    $ntdsData = Get-Content $NTDSFilePath
    $userHashes = @{}
    
    # Parse each line of the NTDS file
    foreach ($line in $ntdsData) {
        if ($line -match '^([^:]+):(\d+):([^:]+):([^:]+):::') {
            $user = $matches[1]
            $ntlmHash = $matches[4]
            
            # Skip the LM hash and empty passwords
            if ($matches[3] -eq "aad3b435b51404eeaad3b435b51404ee" -and 
                $ntlmHash -ne "31d6cfe0d16ae931b73c59d7e0c089c0") {
                $userHashes[$user] = $ntlmHash
            }
        }
    }
    
    return $userHashes
}

The analysis identifies users whose hash value remains identical across multiple audit scans:

# Gather all unique usernames from all years
foreach ($year in $yearHashData.Keys) {
    foreach ($username in $yearHashData[$year].Keys) {
        # Check if account should be excluded
        if (Test-AccountShouldBeExcluded -Username $username) {
            $excludedAccounts++
            continue
        }
        
        if (-not $allUsers.ContainsKey($username)) {
            $allUsers[$username] = @{
                HashesByYear = @{}
            }
        }
        $allUsers[$username].HashesByYear[$year] = $yearHashData[$year][$username]
    }
}

# Identify users with consistent hashes across multiple years
foreach ($username in $allUsers.Keys) {
    $userInfo = $allUsers[$username]
    
    # Only consider users found in at least 2 audit files
    if ($userInfo.HashesByYear.Count -ge 2) {
        $distinctHashes = $userInfo.HashesByYear.Values | Select-Object -Unique
        
        # Check if all hashes are identical
        if ($distinctHashes.Count -eq 1) {
            $usersWithConsistentHashes += [PSCustomObject]@{
                Username = $username
                Hash = $distinctHashes[0]
                YearsPresent = $userInfo.HashesByYear.Keys -join ', '
                NumberOfYears = $userInfo.HashesByYear.Count
            }
        }
    }
}

What The Script Reveals

When a user has the same password hash across multiple audits, it means they:

  • Never changed their password despite policy requirements, OR
  • Changed their password but reverted back to a previous password, OR
  • Are using a pattern that results in identical hash values

Output and Reporting

The script generates comprehensive reports showing:

# Group users by hash to find shared passwords
$usersByHash = @{}
foreach ($user in $usersWithConsistentHashes) {
    if (-not $usersByHash.ContainsKey($user.Hash)) {
        $usersByHash[$user.Hash] = [System.Collections.ArrayList]@()
    }
    $null = $usersByHash[$user.Hash].Add($user)
}

# Display users grouped by hash value
Write-Host "`nGrouping by password hash:" -ForegroundColor Yellow
foreach ($hashEntry in $usersByHash.GetEnumerator() | 
         Sort-Object -Property { $_.Value.Count } -Descending) {
    $users = $hashEntry.Value
    $hash = $hashEntry.Key
    
    Write-Host "`nPassword hash: $hash" -ForegroundColor Magenta
    Write-Host "Used by $($users.Count) users:" -ForegroundColor Magenta
    
    foreach ($user in $users) {
        $isAdminAccount = $user.Username -match "\\ADM"
        $accountType = if ($isAdminAccount) { "(Admin account)" } 
        else { "(Regular account)" }
        Write-Host "  - $($user.Username) $accountType - Same hash in: $($user.YearsPresent) 
        ($($user.NumberOfYears) audits)" -ForegroundColor White
    }
}

CSV Export

The script also exports results to CSV format for deeper analysis:

# Export results to CSV
$csvOutput = @()

foreach ($user in $usersWithConsistentHashes) {
    $isAdminAccount = $user.Username -match "\\ADM"
    
    # Check if this user shares password with others
    $sharedWith = @()
    $usersWithSameHash = $usersByHash[$user.Hash]
    if ($usersWithSameHash.Count -gt 1) {
        $sharedWith = ($usersWithSameHash | 
                      Where-Object { $_.Username -ne $user.Username } | 
                      ForEach-Object { $_.Username })
    }
    
    $csvOutput += [PSCustomObject]@{
        Username = $user.Username
        PasswordHash = $user.Hash
        IsAdminAccount = $isAdminAccount
        YearsPresent = $user.YearsPresent
        NumberOfAuditsWithSameHash = $user.NumberOfYears
        SharedWith = ($sharedWith -join "; ")
        NumberOfAccountsSharing = if ($sharedWith.Count -gt 0) { 
            $usersWithSameHash.Count 
        } else { 1 }
    }
}

$csvPath = "FilteredUsers-Consistancy__$(Get-Date -Format 'yyyyMMdd_HHmmss').csv"
$csvOutput | Export-Csv -Path $csvPath -NoTypeInformation

Why This Matters : Do you need to ask?

This analysis exposes several critical security issues:

  1. Policy Circumvention: Users who simply reuse old passwords to satisfy "change password" requirements
  2. Shared Passwords: Multiple accounts using identical passwords (especially concerning for admin accounts)
  3. Static Passwords: Accounts that have never actually changed passwords despite audit dates
  4. Admin Risk: Administrative accounts using the same password as regular accounts

Usage

  1. Create a password-protected 7z archive containing your NTDS extracts
  2. Run the script: .\Hashcat-ConsistantHashes.ps1
  3. Choose option 2 to analyze existing archive
  4. Enter the archive password
  5. Review the console output and CSV report

Security Considerations

  • All sensitive data is handled in memory
  • Temporary files are securely deleted
  • Password archives use 7-Zip's strongest encryption
  • No plaintext passwords are ever displayed or stored
# Secure cleanup function
function Remove-WorkingData {
    param ([string]$WorkingDirectory)
    
    if (Test-Path $WorkingDirectory) {
        Write-Host "Cleaning up temporary files in $WorkingDirectory" -ForegroundColor Cyan
        
        # Remove all files in the working directory
        Remove-Item -Path (Join-Path -Path $WorkingDirectory -ChildPath "*") -Recurse -Force
        
        # Remove the directory itself
        Remove-Item -Path $WorkingDirectory -Force
        
        Write-Host "Temporary files removed" -ForegroundColor Green
        return $true
    }
    
    return $false
}

Conclusion

This script provides a powerful tool for detecting password compliance evasion that traditional password policies miss. By analyzing password hashes across multiple security audits, organizations can identify users who circumvent password policies and address this critical security gap.

Remember: The goal isn't to catch users but to improve organizational security posture by identifying and fixing weaknesses in password policy enforcement.

Previous Post Next Post

نموذج الاتصال