Troy Hunt’s incredibly useful haveibeenpwned.com is a great way to check whether your email address and other personal information was exposed in a data breach. But it also allows you to separately check if a specific password was exposed in a breach. As of version 5, the data set contains over half a billion compromised passwords and the number of times they’ve been seen in data breaches. My PwnedPassCheck module lets you query that data easily via PowerShell.
This Sounds Sketchy
If you’re thinking that sending your password or even just its hash to a random website on the Internet just to check if it has been compromised seems like a good way to become compromised, you’re absolutely right. But since version 2, Troy with the help of Junade Ali from CloudFlare came up with a way to let you query the data safely and still remain anonymous using a model called k-Anonymity.
Essentially, you never send your password or its full hash to the API server. Instead you send only the first 5 characters of the hash and the server sends back a list of hashes with a matching prefix. You can then check the list locally for the actual result. Not only does this prevent the server operator from getting your password/hash, it’s also significantly faster to query than a single multi-gigabyte text file.
If you’re still unconvinced, you’ll be happy to know that this module supports checking against alternative API sources or even filesystem paths. So you can host your own internal copy of the API or even just toss the files on a server somewhere and query that. The official API only supports SHA-1 hashes, so this is actually a requirement if you want to check NTLM hashes unless someone else decides to host a public NTLM version.
Show Me The Code Already
First grab the module from the PowerShell gallery. It requires a minimum of PowerShell 3.0, but will happily run cross-platform on PowerShell Core as well. If you’re running an ancient version of PowerShell that doesn’t include
Install-Module, see the readme for alternative instructions.
The most basic thing you can do is pass a string to
Test-PwnedPassword. Don’t use a real password this way because it can be saved in your command history.
PS C:\> Test-PwnedPassword 'password' Label Hash SeenCount ----- ---- --------- 5BAA61E4C9B93F3F0682250B6CF8331B7EE68FD8 3730471
The output will display the hash of that string along with the “seen count” which is how many times that password has been seen across all of the data breaches. So popular/insecure passwords like ‘password’ are inevitably going to have a huge seen count. Ignore the Label column for now.
When you’re ready to test a real password, use something like
Get-Credential to enter it interactively so it doesn’t show up in your command history.
Test-PwnedPassword will accept String, SecureString, and PSCredential objects. You can also pass a bunch of them at once via the pipeline.
$pass = Read-Host -Prompt 'Password' $secPass = Read-Host -Prompt 'Secure Password' -AsSecureString $cred = Get-Credential # The username in a credential is ignored $pass,$secPass,$cred | Test-PwnedPassword
Test-PwnedPassword is fine when you know the clear text for the passwords you want to check. But for admins who want to audit the passwords of an existing userbase, they (hopefully) only have access to the password hashes. That’s where
Test-PwnedHashBytes come in handy.
The only restriction on checking hashes at the moment is that they must be either unsalted SHA-1 or NTLM hashes. The official API only supports SHA-1 and both SHA-1 and NTLM versions are available for download to check against locally. It’s possible there may be other hash versions in the future, but those are the limitations for now.
Test-PwnedHashBytes will accept hashes as a hex-encoded string (case-insensitive) or byte array respectively. If you don’t have an existing set of password hashes to use, the module has
Get-NTLMHash helper functions you can test with.
$hashHex = Get-SHA1Hash 'P@ssword1' Test-PwnedHash $hashHex $hashBytes = Get-SHA1Hash 'P@ssword1' -AsBytes Test-PwnedHashBytes $hashBytes
Querying Locally and NTLM
For those who can’t or won’t send even the first 5 characters of a password hash to a third party website, it’s fairly easy to host your own local copy. This is also necessary if you want to check NTLM hashes because the official API only supports SHA-1.
The Pwned Passwords page has links to download the 7-zip compressed files. Download the one ordered by hash for hash type you want to query against and extract it. The version 5 files are roughly 20 GB once extracted so make sure you have enough disk space.
Splitting The File
This is the largest drawback to querying locally. But you only have to do it once. 5 hexadecimal characters have a total of 16^5 (1,048,576) combinations which makes splitting the file by hand impractical. Each line is in the form
<hash>:<count> except the occasional line that has the string “NULL”. So for each line you need to make sure it’s not NULL, split the line into its prefix (first 5 characters) and suffix (the rest of the line), then append the suffix to a file named with the prefix. It’s not a super difficult scripting exercise, but the size of the original file and the quantity of resulting files makes it a fairly storage I/O intensive process. So if your disk is slow, it may take a while.
I’ve added a Split-Scripts folder to the module’s GitHub repository that contains a couple scripts I used to split the file for my own purposes on various systems. If you end up writing your own script that would be useful to others, feel free to contribute to the project and send a pull request.
Hosting The Files
When you finish splitting the file, you should have a folder with 1,048,576 files named
FFFFF that are each roughly 20 KB in size. You can either leave them on the filesystem if you’re doing the checks from the same system, move them to a file share, or host them on an internal web server. If they’re on a web server, make sure it’s configured to serve the files as
text/plain MIME type. You should be able to go to a URL like
http://pwned.example.com/range/00000 in a browser and see the contents of the file.
Running Local Queries
All of the
Test-* functions take an optional
-ApiRoot parameter that will override the default pwnedpasswords.com URL. The parameter will accept URLs or filesystem/UNC paths. So any of the following examples would work. Just make sure to include the entire path except for the 5 character hash prefix.
# These work the same way using Test-PwnedHash and Test-PwnedHashBytes Test-PwnedPassword 'password' -ApiRoot 'C:\temp\pwned\' Test-PwnedPassword 'password' -ApiRoot '\\server\share\pwned\' Test-PwnedPassword 'password' -ApiRoot 'http://pwned.example.com/range/'
For NTLM API sources, you’ll need to set the
-HashType NTLM parameter with
Test-PwnedPassword in addition to the
-ApiRoot parameter. The
Test-PwnedHash* functions assume the hash is already of the correct type. So no change is needed for those other than setting
Test-PwnedPassword 'password' -ApiRoot 'C:\temp\pwned\' -HashType 'NTLM' Get-NTLMHash 'password' | Test-PwnedHash -ApiRoot 'http://pwned.example.com/range/'
Huge thanks to Troy Hunt for his tireless work maintaining haveibeenpwned.com and the rest of his contributions to the larger security community making people safer online. I hope this module helps add to that in some small way. I’m also planning on adding another post soon that demonstrates how to easily do a password audit against Active Directory using this module, so be on the lookout for that.