Ryan Bolger

Ryan Bolger

Adventures In Tech

Auditing Active Directory Passwords With PwnedPassCheck

Make sure users aren't using compromised passwords.

Ryan Bolger

In a previous post, I introduced a new PowerShell module called PwnedPassCheck. It can be used to check passwords and hashes against a list of over half a billion compromised passwords exposed in data breaches thanks to Troy Hunt’s incredibly useful haveibeenpwned.com. In this post, I’ll demonstrate how to use the module in conjunction with Michael Grafnetter’s amazing DSInternals module to quickly audit existing passwords in Active Directory against the compromised list.

Doesn’t DSInternals Already Support This?

Awkward Monkey Puppet

Yes. Since version 3 of the DSInternals module, the Test-PasswordQuality function has been able to use the monolithic NTLM hash file from Have I Been Pwned as a source for weak password hashes. The function also includes a bunch of other password quality indicators and is a fantastic utility all by itself. It was pretty slow in that version because it was scanning the file line by line and the file is huge. Version 3.3 implemented a binary search option if you had a sorted hash file that was significantly faster.

PwnedPassCheck has two potential advantanges over DSInternals. The first is the “Seen Count” which is the number of times a password was seen in breaches. While it could be argued that any password seen in a breach even once is bad and should be changed, I don’t think anyone would argue that a password seen over 100,000 times is worse than one only seen once. The other advantage is if you’re already hosting or have access to a third party hosting a copy of the NTLM API, you can skip having to deal with downloading the monolithic file to your audit system.


As I mentioned in the previous post, Have I Been Pwned doesn’t currently host an NTLM API version of the data set. So you need to download the NTLM data set file ordered by hash, split the file into the format the API uses, and host it somewhere. Check the previous post for details on that process.

Ultimately, you need to know what to set for the -ApiRoot parameter. Here are some possibilities depending on where you decide to host the files.

# web server
# file share
# local filesystem

Querying NTHash with DSInternals

DSInternals supports two different ways to get AD’s NTHash values that you’ll be checking; an offline method that reads ntds.dit files and an online method that uses AD’s own replication protocol to query a DC directly. In my opinion, the online method is way easier to deal with, so that’s what we’ll describe here. But if you want or need to go with the offline route, here’s a DSInternals blog post describing the process.

For the online query method, we’ll be using the Get-ADReplAccount function. In order for the query to work, you’ll need the Replicating Directory Changes and Replicating Directory Changes All permissions on the domain’s root object. By default, members of “Domain Admins” have these permissions as well as a few other domain controller related groups. But in case you want to setup a dedicated account for this purpose, here’s a quick way to add them for a particular group or account.

$context = (Get-ADRootDSE).defaultNamingContext
$userOrGroup = 'EXAMPLE\myuser'
&dsacls.exe "$context" /G "$($userOrGroup):CA;Replicating Directory Changes;"
&dsacls.exe "$context" /G "$($userOrGroup):CA;Replicating Directory Changes All;"

There are a number of different ways to call Get-ADReplAccount. If you want to get every account-like object in the domain (computers, managed service accounts, etc.), you can call it like this.

$context = (Get-ADRootDSE).defaultNamingContext
$dc = (Get-ADDomainController).HostName
$accounts = Get-ADReplAccount -All -NamingContext $context -Server $dc

Getting everything tends to be overkill for this purpose because things like computers and managed service accounts are highly unlikely to have compromised passwords since they auto-rotate with random long passwords under normal circumstances. Instead, I like to do an initial query with Get-ADUser from the ActiveDirectory PowerShell module. You can use whatever filter and search base options make sense for the audit you want to perform, but here’s a basic example.

$dc = (Get-ADDomainController).HostName
$accounts = Get-ADUser -Filter * | Select ObjectGuid | Get-ADReplAccount -Server $dc

The Select ObjectGuid portion is important because the parameter binding in Get-ADReplAccount gets confused if you send it the raw output of Get-ADUser. Now you should have a set of “DSInternals.Common.Data.DSAccount” objects in the $accounts variable that are ready to be sent to PwnedPassCheck.

Checking The Hashes

Each of the DSAccount objects in the $accounts variable you created in the previous step have an NTHash property that contains the hash as a byte array which we can use with Test-PwnedHashBytes. The NTHash property can be null on some of the default domain objects like Guest and DefaultAccount, so we’ll filter those out first.

$accounts | Where-Object { $_.NTHash } |
    Test-PwnedHashBytes -ApiRoot 'http://pwned.example.com/range/' |
    Select Label,SeenCount

The -Label parameter in Test-PwnedHashBytes has an alias of SamAccountName and so parameter binding picks up the value from each DSAccount object automatically and puts it in the output for us. I also purposefully excluded the Hash property from the output because in this case, the Label is likely all you care about. The output might look something like this.

Label         SeenCount
-----         ---------
krbtgt                0
Administrator         0
baduser            1177
gooduser              0

So baduser is the only account that had a password found in the compromised data set and it was seen a total of 1177 times which means that user should probably be required to change their password.

What’s Next?

Audit All The Passwords Meme

A one-time audit for compromised passwords is a great idea for any organization who isn’t currently doing any password auditing beyond AD’s native complexity enforcement. Setting up a regularly scheduled audit that can report to admins and/or take action like forcing a password change on compromised users is an even better idea. Changed passwords since the last audit will be caught and newly compromised passwords will be caught as new versions of the compromised data set are released and you update your local copy.

It’s also a good idea to prevent compromised passwords from being set it the first place by integrating the check with your existing password change system. How to do this will vary widely on how passwords are currently managed in your organization.

If you’re using an entirely on-prem Active Directory, there are a variety of free and paid password filter drivers that get installed on your domain controllers and are able to reject passwords that are compromised or don’t meet other requirements you configure. A few that I’ve run across recently include Lithnet Password Protection, PwnedPasswordsDLL-API, and safepass.me.

If your on-prem Active Directory is just a sync’d replica from another source or you’re using one of the many cloud federated identity providers, you may need to implement it at the source layer. Azure AD’s feature called Password Protection is the main one I know about. But I’m sure other cloud identity providers have something similar.

The NIST password policy guidelines released in 2017 are pretty clear that the conventional wisdom regarding complex passwords and forced rotation were a bad idea. The new guidelines suggest that length is the most important factor in password strength in addition to other caveats like not using dictionary words or repeating characters. And forced rotation should only be necessary in cases of breach or known compromise.

Thanks again to Michael Grafnetter for his incredible work on DSInternals and Troy Hunt for everything related to Have I Been Pwned. You guys are awesome and we’re all safer because of your efforts.

Recent Posts