Eliminate Compromised Passwords With One Identity Password Manager & Have I Been Pwned

Prevent the use of known breached passwords in your organization with the PwnedPasswords V3 API

Eliminate Compromised Passwords With One Identity Password Manager & Have I Been Pwned

Have I Been Pwned? Or rather, have you?

It's a good question, and if you're curious about whether any of your email addresses or passwords were included in a data breach, you can easily find out at HaveIBeenPwned.com. This entirely free service, managed by Microsoft Regional Director & MVP Troy Hunt, lets you enter an email address or password to see how many breaches it has been included in. For an email, it goes as far as informing you which breaches included your data, and you can check the "Who's been pwned" page to get the full list of breaches included in the repository. If you want to go really crazy, you can download the entire pwned passwords list as a torrent, in SHA-1 or NTLM formats.

It Gets Better

Have I Been Pwned also includes a set of APIs that you can use to programatically access data from the collection. The PwnedPasswords endpoint allows you to check if a Password appears in the pwned passwords list using a REST call. The best part is this is entirely free, with no rate limits or license requirements!

Can I trust this site?

Well, I'll leave that entirely up to you, but Troy is a highly respected security expert, and you can read all about the Have I Been Pwned service, how it handles privacy, and what all this stuff means directly on the website, as well as tons of detailed blog posts on Troy's website. The short answer is that this is a well-known, trusted, and secure service that's been used by organizations and government entities all around the world for years as another security layer to protect against attacks like Password Spraying and Credential Stuffing. In the end, I'll just reiterate Troy's own words from the FAQ:

The site is simply intended to be a free service for people to assess risk in relation to their account being caught up in a breach. As with any website, if you're concerned about the intent or security, don't use it.

How does this apply to me?

Glad you asked. The short answer is we can use this service with One Identity Password Manager to ensure that the passwords your users are setting have not been compromised in a data breach!

I've talked before about One Identity Password Manager, and how configurable and customizable it is. One of the configurable options is to enable the "Credential Checker on" setting, which runs all passwords through a Powershell script before allowing users to set them. See where I'm going with this?

By default, this script utilizes a service called CredVerify from VeriClouds, which requires authentication. If you have, or can obtain, a URL, Client Key, and Client Secret for this service, you can simply plug them into the script and get going.

However, we can use Have I Been Pwned as an alternative, which is totally free, and has no authentication requirements or rate limiting for the PwnedPasswords API!

The full script, and install instructions, are available on GitHub. You can simply replace the out-of-the-box script with the new one, enable the "Credential Checker on" setting, and you're good to go. Your Password Reset processes will now reject any passwords found in the HIBP database.

If that's all you need, head on over to GitHub and get going! If you're interested in the details of how this all works, keep reading.

Exploring the PwnedPasswords API

The first thing to get straight is that HIBP passwords are provided as SHA1 hashes. Now if you're thinking SHA1? Isn't that an unreliable algorithm these days?, I'll let Troy speak for himself:

It doesn't matter that SHA1 is a fast algorithm unsuitable for storing your customers' passwords with because that's not what we're doing here, it's simply about ensuring the source passwords are not immediately visible.

When the PwnedPasswords API was first launched, you could query the API with the full SHA1 hash of the password to return any hits. However, version 2 improved privacy (and speed) by introducing Cloudflare's k-Anonimity model. This model is still in use in the current version 3 of the API, and if you're still not convinced, have another Troy Hunt blog post explaining the model in depth.

How do we use this thing?

In practice, the model for this API works by accepting a 5 character substring of the SHA-1 hash for a password, and returning a list with all matches, only containing the remaining substring of the hash. Sounds more complicated than it is, let's walk through it.

Take my incredibly secure password, "Password123". First, I need to compute the SHA-1 Hash of that password. With Powershell, that's simple enough.

PS C:\> $PlaintextPassword = "Password123"
PS C:\> $PasswordStream = [IO.MemoryStream]::new([byte[]][char[]]$PlaintextPassword)
PS C:\> $HashedPassword = (Get-FileHash -InputStream $PasswordStream -Algorithm SHA1).Hash
PS C:\> $HashedPassword
B2E98AD6F6EB8508DD6A14CFA704BAD7F05F6FB1

Now to..."k-Anonymize". We need to send the first 5 characters of the hash to HIBP, so let's call that the $hashPrefix and the remainder of the hash the $hashSuffix.

PS C:\> $hashPrefix = $HashedPassword.Substring(0,5)
PS C:\> $hashPrefix
B2E98

Great, now we can send that to the PwnedPasswords API to get a list of matches. But first, we need to ensure we are using at least TLS 1.2

PS C:\> [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

Now we can make our REST Web Request:

PS C:\> $url = "https://api.pwnedpasswords.com/range/$hashPrefix"
PS C:\> $HIBP = Invoke-RestMethod -Uri $url -Method GET -UseBasicParsing

If we inspect the $HIBP response, we'll see that it's a large string of newline-delimited results. Each of these results contains a hash suffix for a matching password beginning with our prefix, followed by a colon (:) and the count of how many times that password has appeared.

PS C:\> $HIBP
004FAC2FA6779235D4D835A4B518D563F3F:3
00540184B698E09B3977E74434933FF1A6D:8
0078FBA31E1A8034F55AAB06C61337D377A:1
00D24723161C97D6C16B9139B1279DD7B53:2
01183CA5E4C40A960A98EC3F9DB4A705007:1
011969830985B5E941F7285B22393130EA0:1
0123A5B3069D0CA31210A184BB79927B9C9:64
012A94FB50CFC1267DEC3EE8C3826AA2405:3
01A148B7911D34BC9697D1FB203706D00BC:1
01E8A15747D1289B6E8D960008BC990C798:1
01FB7A423C3AD369B173E4F88EC836B1BA9:2
0283F368CB5A6850390AF940A4D79097637:4
02A5DB2458D4235579AED00A5D6304F6043:2
032BF92F00C1C365999BCE1F6B8A74403BD:1
045EB79206C8C42998D632FB0619542ABEB:1
...

For our purposes, we really only care about 1 thing. Is our original password hash in this list of breached passwords? To check, we need to standardize this response.

We first need to split this giant string into an array of strings, one item for each line. We can split on the newline character with $HIBP.Split("`n"). Then, we can loop through each of these lines and do 2 things.

  1. Split it on the colon (:) so we can parse out the $hashSuffix in the first index [0].

  2. Append the original $hashPrefix to the beginning of the string.

Finally, store the result of this in a nice $HashMatches array.

$HashMatches = ForEach ($result in $HIBP.Split("`n")) {
        $hashSuffix = $Result.Split(":")[0]
        "$hashPrefix$hashSuffix".ToUpper()
}

PS C:\> $HashMatches
B2E98004FAC2FA6779235D4D835A4B518D563F3F
B2E9800540184B698E09B3977E74434933FF1A6D
B2E980078FBA31E1A8034F55AAB06C61337D377A
B2E9800D24723161C97D6C16B9139B1279DD7B53
B2E9801183CA5E4C40A960A98EC3F9DB4A705007
B2E98011969830985B5E941F7285B22393130EA0
B2E980123A5B3069D0CA31210A184BB79927B9C9
B2E98012A94FB50CFC1267DEC3EE8C3826AA2405
B2E9801A148B7911D34BC9697D1FB203706D00BC
B2E9801E8A15747D1289B6E8D960008BC990C798
B2E9801FB7A423C3AD369B173E4F88EC836B1BA9
B2E980283F368CB5A6850390AF940A4D79097637
B2E9802A5DB2458D4235579AED00A5D6304F6043
B2E98032BF92F00C1C365999BCE1F6B8A74403BD
B2E98045EB79206C8C42998D632FB0619542ABEB
B2E98049670AA6A430AE0911C189E8E456FF92E0
...

Great, we have a lovely array filled with hashes that begin with the same 5 characters as our password's hash. Now we just need to see if our password is in that array, and if so, return $true.

$PasswordLeaked = ($HashedPassword.ToUpper() -in $HashMatches)
Return $PasswordLeaked

And of course, we can wrap all this up in a cmdlet, let's call it Confirm-PwnedPassword. As part of that, it should also be able to support both a plaintext password and a SHA1 hash as options, just for flexibility. If it receives a plaintext password, it can perform the hashing function before processing.

function Confirm-PwnedPassword {

    [cmdletbinding()]
    param(
        [parameter(position=1,mandatory,ParameterSetName="Plaintext")]
        [string]
        $PlaintextPassword,

        [parameter(position=1,mandatory,ParameterSetName="Hash")]
        [string]
        $HashedPassword
    )

    if ($PSCmdlet.ParameterSetName -eq "Plaintext") {
        # Get & Hash the entered password
        $PasswordStream = [IO.MemoryStream]::new([byte[]][char[]]$PlaintextPassword)
        $HashedPassword = (Get-FileHash -InputStream $PasswordStream -Algorithm SHA1).Hash
    }

# the rest of the cmdlet here

The full function can be found in a standalone Confirm-PwnedPassword.ps1 script in the associated GitHub Repo, along with all the other files for this project.

Password Manager's Built-in Credential Checker

We now have a nice cmdlet to check if a password appears in the PwnedPasswords database, so how do we get Password Manager to run that against a user's about-to-be-set password?

The out-of-the-box Password Manager Credential Checker runs a Powershell Script that comes included with the Password Manager installation. This script runs 2 functions with specific data passed in for the parameters. We just need to modify this script appropriately.

The Script

With "Credential Checker On", any time a user tries to set a new password, it will first run this script against their username & password to determine if that should be accepted.

Get-HashAlgorithm returns a string for the hashing algorithm that Password Manager should use to hash the user's password. This defaults to SHA1, which is what we need for the PwnedPasswords API. Password Manager will hash the password using this algorithm before passing it into the following function.

Test-IsCompromisedCredential takes the username of the user resetting their password, and the SHA-1 hash provided by Password Manager, and runs some code with those parameters. If this code returns $true, Password Manager will reject the password the user entered.

To make this work with the PwnedPasswords API, all we need to do is plug in our Confirm-PwnedPassword cmdlet, and call that inside Test-IsCompromisedCredential, instead of the default API call to CredVerify.

Putting this all together, first Password Manager will run Get-HashAlgorithm, which will return the string SHA1. Next, Password Manager will hash the about-to-be-set password using the SHA-1 algorithm. Finally, Password Manager will call Test-IsCompromisedCredential, passing in the username to the $username parameter, and the SHA1 hash for the $passwordHash parameter. If that returns $true, the password will be rejected, and the user will see an error message. Otherwise, the password will be accepted, allowing the workflow to continue.

Some solutions, like the default CredVerify, check the combination of username and password against their database. Since HaveIBeenPwned only needs the password, we can simply ignore the $username parameter.

Conclusion

You now have another method of increasing your password security by ensuring your users can't set passwords that have been released or compromised as the result of a breach. If you're following NIST best practices for passwords, you'll know that this is one of the key principles:

Compare the set password against the list of breached passwords (commonly used passwords).

Take this a step further and combine this with a Password Policy that prioritizes length over complexity, restricts usage of particular AD Attributes in the password, and utilizes a curated dictionary. Even better, make sure the authentication process for your forgotten password process includes MultiFactor Authentication.

As always, you can keep up with the latest code changes on my personal GitHub or grab the latest stable release on the One Identity GitHub. Refer to the README for instructions.

One Identity GitHub Note

One Identity open source projects are supported through One Identity GitHub issues and the One Identity Community. This includes all scripts, plugins, SDKs, modules, code snippets or other solutions. For assistance with any One Identity GitHub project, please raise a new Issue on the One Identity GitHub project page. You may also visit the One Identity Community to ask questions. Requests for assistance made through official One Identity Support will be referred back to GitHub and the One Identity Community forums where those requests can benefit all users.

External Links

View this article elsewhere: