In this post I explore the impact of SQL Server 2025’s PBKDF2 hashing algorithm on password cracking and compare it with SQL Server 2022.
Spoiler: SQL Server 2025’s PBKDF2 hashing algorithm turns 45 seconds worth of password brute-forcing into an estimated 154008 seconds.
Intro
I’ve written previously about auditing or cracking SQL Server login passwords either online (inside the instance itself) or offline (exporting the hashes and using a specialized cracking tool).
Last week, Microsoft’s Pieter Vanhove published a blog post that covers What’s new in SQL Server 2025 security.
The thing that caught my attention was the point about RFC2898, also known as a password-based key derivation function (PBKDF), being introduced as a default for SQL logins in SQL Server 2025.
As per the blog post, the algorithm still uses SHA-512 but hashes the password multiple times (100,000 iterations), significantly slowing down brute-force attacks.
This applies to:
- CREATE LOGIN WITH PASSWORD
- CREATE USER WITH PASSWORD
- CREATE APPLICATION ROLE
So, naturally, I was curious about how this might impact attempts to crack SQL login password hashes online.
How the new hash looks
I’m creating the following SQL login on a SQL Server 2025 instance as well as a 2022 instance:
1 2 3 4 5 | USE [master] GO CREATE LOGIN [test_login] WITH PASSWORD = N'$up3R-S3Cur3P@22'; GO |
Then I run the following query to get the name and the password hash of the newly created login:
1 2 3 | SELECT [name], [password_hash] FROM sys.sql_logins WHERE [name] = N'test_login'; |

Note that the “header” of the hash is 0x0300, which indicates that this hash is version 3.
Meaning that it uses SQL Server 2025’s new PBKDF2 hashing algorithm.
For reference, SQL Server 2012-2022 SQL login hashes start with 0x0200, indicating version 2 of SQL Server’s hashing algorithm.
And the password hash was constructed as 0x0200+Salt+SHA-512(password + Salt)
.
Where the salt was a random 4 byte hexadecimal number, like what CRYPT_GEN_RANDOM(4) would return.
How to generate one
While I don’t yet have way to build the new PBKDF2 password hash from parts, the PWDENCRYPT function can be used to hash a clear text password.
1 | SELECT PWDENCRYPT(N'$up3R-S3Cur3P@22') AS [hashed_password]; |

Comparing cracking duration
Single login
All the instances involved have identical configuration (MAXDOP 4, CTP 50, Max Memory MB 4096).
The VM based instances are on a Windows Server 2025 VM with 4 CPU cores and 12GB of RAM.
While the container based instances are on a QNAP NAS with 4/8 CPU cores/threads and 32GB of RAM.
As for specific versions, the 2022 instances are running 16.0.4195.2, and the 2025 instances are on 17.0.800.3.
I’ve create the same test_login SQL login on all instance involved in this test.
1 2 3 4 5 | USE [master] GO CREATE LOGIN [test_login] WITH PASSWORD = N'$up3R-S3Cur3P@22'; GO |
And then I use PWDCOMPARE with time statistics to see how long it takes to hash the clear text string and compare it to the login’s hash for both the correct password and an incorrect password.
1 2 3 4 5 6 7 8 9 10 11 12 | SET STATISTICS TIME ON; SELECT [name], PWDCOMPARE(N'$up3R-S3Cur3P@22', [password_hash]) AS [password_match] FROM sys.sql_logins WHERE [name] = N'test_login'; SELECT [name], PWDCOMPARE(N' ', [password_hash]) AS [password_match] FROM sys.sql_logins WHERE [name] = N'test_login'; |
The result looks like this, with 1 meaning that the clear text password matches the existing password hash.

And the time statistics output in the messages tab looks like this on the 2022 instance:
SQL Server parse and compile time:
CPU time = 7 ms, elapsed time = 7 ms.(1 row affected)
SQL Server Execution Times:
CPU time = 0 ms, elapsed time = 0 ms.(1 row affected)
SQL Server Execution Times:
CPU time = 0 ms, elapsed time = 0 ms.
Ignore the parse and compile time since that’s just the time SQL Server takes to parse the T-SQL and compile the plan, not the actual duration of PWDCOMPARE.
Instance | Elapsed time (ms) – correct password | Elapsed time (ms) – incorrect password |
---|---|---|
2022 VM | 0 | 0 |
2025 VM | 147 | 149 |
2022 Container | 0 | 0 |
2025 Container | 152 | 150 |
It looks like the PBKDF2 hashing algorithm used in SQL Server 2025 does incur some additional time.
Around 150 milliseconds in this case.
Which makes sense since it’s an iterative algorithm and hashes the password 100,000 times in an attempt to make brute-force attacks even slower and less feasible* than they are now.
*If you’re using passwords that aren’t like Summer2025 or [CompanyName][Year].
Multiple logins with generated password list
For this, I’ll try to do something similar to an actual audit/online cracking.
Since the number of generated candidates will be fairly large, I’ll only create a few logins (besides sa, and test_login).
I’ll also create a new database for a login that will have its password derived from the database name.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | CREATE DATABASE [AdventureWorks2022] GO CREATE LOGIN [sa1] WITH PASSWORD = N'Welcome123??'; GO ALTER SERVER ROLE [sysadmin] ADD MEMBER [sa1]; GO /*Login with a database name + special character as password*/ CREATE LOGIN [AppAdmin] WITH PASSWORD = N'AdventureWorks2022$'; GO ALTER SERVER ROLE [serveradmin] ADD MEMBER [AppAdmin]; GO /*Login that uses a password derived from a provided base word*/ CREATE LOGIN [Dev1] WITH PASSWORD = N'(Contoso22)'; GO ALTER SERVER ROLE [dbcreator] ADD MEMBER [Dev1]; GO /*The kind of password that the ID Admin team at my former job used to provide*/ CREATE LOGIN [Dev2] WITH PASSWORD = N'SUMMER20!', CHECK_POLICY = OFF; GO ALTER SERVER ROLE [dbcreator] ADD MEMBER [Dev2]; GO |
This means that with sa, test_login and the newly created logins, there are 6 SQL logins on each instance.
And then I use my QuickSQLPassAudit.sql script with the following options:
1 2 3 4 5 6 7 8 9 10 11 12 | /*List of comma separated custom words spaces are not required*/ SET @BaseWordsList = N'contoso'; /*Change this to 1 to use database names, logins and instance name for password candidates*/ SET @UseInstInfo = 1; /* This is useful if you want to run the script on a production server and not expose the passwords. If you set this to 0, the script will return only the logins for which passwords have been found. If you set this to 1, the script will return the logins and their identified passwords. Speeds up execution at the cost of not knowing the actual password */ SET @ReturnFoundPasswords = 1; /*Set this to 1 to not drop the #WordList table at the end of the execution*/ SET @KeepPassCandidatesTbl = 1; |
Execute it on the target instances and take note of the Duration value specified in the Messages tab.
This excludes the time needed to generate the password candidates, and only accounts for the actual password “cracking”.
Here’s how the result looks for one of the 2022 instances:

Note that neither sa nor test_login show up here since they both have strong passwords that are beyond the scope of this script.
The results
So… here’s the deal: after executing in a decent amount of time on both the 2022 instances, when the time came to run it on the 2025 instances I cancelled the execution around the 60 minutes mark.
But I do have an average duration of 150 milliseconds per password candidate from the previous (single login) test.
I also know that the current execution of the QuickSQLPassAudit.sql script generates 171121 password candidates for each instance.
So, I should be able to estimate the number of seconds this would take to compare the 171121 password candidates against the password hashes of the 6 logins using the new PBKDF2 algorithm.
1 2 3 4 5 6 7 8 | DECLARE @pass_candidates INT = 171121, @single_candidate_ms INT = 150 SELECT COUNT(*) AS [sql_logins_count], CAST(( COUNT(*) * @pass_candidates * @single_candidate_ms ) / 1000. AS DECIMAL(23, 2)) AS [estimated_seconds_pbkdf2] FROM sys.[sql_logins] WHERE [name] NOT LIKE N'##%'; |

So, for 6 logins, with 171121 password candidates at 150 milliseconds per candidate, it would take 154008.90 seconds or 42.8 hours.
The side-by-side comparison:
Instance | Duration (seconds) |
---|---|
2022 VM | 45 |
2025 VM | 154008.90 (estimated) |
2022 Container | 71 |
2025 Container | 154008.90 (estimated) |
Note that the duration for the 2022 instance running in a container is higher, so using the same estimate for its 2025 counterpart as for the VM may be a bit optimistic.
But SQL Server 2025’s new hashing algorithm makes password brute-forcing way less feasible than it was before.
Does throwing more CPU at it help?
Not really.
As far as I can tell, the hashing process is single-threaded.
And I can’t imagine it working if it would be multi-threaded since it’s iterative and the next hash is built “on top” of the previous one in a loop that spans 100k iterations.
The portion of the script that checks password candidates against the password hashes ends up also being single-threaded (due to the hashing) and row by row, because that’s what you generally get when you join on a function.
I’ve doubled the CPU core count on my VM and increased MAXDOP accordingly on the 2025 instance.
But the average PWDCOMPARE time still hovers in the 150 milliseconds area.
This is an obstacle which external tools such as hashcat can help overcome, since they can split password candidates and/or hashes across multiple threads.
Speaking of password cracking tools…
Can SQL Server 2025’s new PBKDF2 hashes be cracked offline?
Nope. At least not at the moment.
The current version of hashcat only supports the previous versions of SQL Server hashing algorithms, and the same goes for John The Ripper (which kinda feels like abandonware at this point).
Can PBKDF2 be used in SQL Server 2022?
Yes, it can. As long as you’re running SQL Server 2022 CU12 or newer and you’re ok with enabling a trace flag.
Read more about it here and make sure you take note of the caveats if you might need to revert.
Can a login hash migrated from SQL Server 2022 work in SQL Server 2025?
A while ago I wrote a blog post about migrating sa’s password hash, from one instance to another, without knowing the clear text password.
For this test, I’ll use a modified version of the query from that post to target the test_login login on the SQL Server 2022 instance.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | DECLARE @SQL NVARCHAR(500), @LineFeed NVARCHAR(5) = CHAR(13) + CHAR(10); SELECT @SQL = N'ALTER LOGIN ' + QUOTENAME([name]) + N' WITH CHECK_POLICY= OFF,' + @LineFeed + N' PASSWORD = ' + CONVERT(NVARCHAR(256), [password_hash], 1) + N' HASHED;' + @LineFeed + N'GO' + @LineFeed + CASE WHEN [is_policy_checked] = 1 THEN N'ALTER LOGIN ' + QUOTENAME([name]) + N' WITH CHECK_POLICY= ON;' + @LineFeed + N'GO' ELSE N'' END FROM sys.[sql_logins] WHERE [name] = N'test_login'; PRINT @SQL; |

Note the 0x0200 header indicating that this is a pre-2025 hash.
I execute the resulting T-SQL in the SQL Server 2025 instance, and it doesn’t error out, so that’s a first good sign.
Let’s see how PWDCOMPARE handles it:
1 2 3 4 5 6 7 | SELECT [name], PWDCOMPARE(N'$up3R-S3Cur3P@22', [password_hash]) AS [password_match], [password_hash], SERVERPROPERTY('ProductVersion') AS [instance_version] FROM sys.sql_logins WHERE [name] = N'test_login'; |

That’s a yes, it’s able to match the clear text password with the hash even if the hash is for a previous version of SQL Server.
Does authentication still work?
While still connected as sa, I grant the following permission to test_login so that it’s able to view the contents of the password_hash column:
1 | GRANT VIEW ANY CRYPTOGRAPHICALLY SECURED DEFINITION TO [test_login]; |
I connect via sqlcmd as test_login and run the following query:
1 2 3 4 5 6 7 | :setvar SQLCMDMAXVARTYPEWIDTH 20 :setvar SQLCMDMAXFIXEDTYPEWIDTH 20 SELECT SERVERPROPERTY('ProductVersion') AS [instance_version], [name],[password_hash] FROM sys.sql_logins WHERE [name] = SUSER_NAME(); GO |

Well, that’s interesting. It looks like test_login’s password hash was updated to the last version during login.
But just to confirm this and show you that there’s nothing up my sleeve, I reset the hash to the 2022 one, and re-check what happens.
I use a little trick to be able to reconnect as test_login as soon as I disconnect from my connection as sa:
1 2 | sqlcmd -S WINSRV2K25\SQL2025_2 -U sa ;` sqlcmd -S WINSRV2K25\SQL2025_2 -U test_login -P '$up3R-S3Cur3P@22' |
Once the connection as sa is established, I paste the following code all in one go:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | :setvar SQLCMDMAXVARTYPEWIDTH 20 :setvar SQLCMDMAXFIXEDTYPEWIDTH 20 SELECT GETDATE() AS [now], SUSER_NAME() AS [whoami], SERVERPROPERTY('ProductVersion') AS [instance_version], [name], [password_hash], [modify_date] FROM sys.sql_logins WHERE [name] = N'test_login' GO EXIT :setvar SQLCMDMAXVARTYPEWIDTH 20 :setvar SQLCMDMAXFIXEDTYPEWIDTH 20 SELECT GETDATE() AS [now], SUSER_NAME() AS [whoami], SERVERPROPERTY('ProductVersion') AS [instance_version], [name], [password_hash], [modify_date] FROM sys.sql_logins WHERE [name] = N'test_login' GO EXIT |
This does the following:
- Sets column widths to something that’s more human-friendly.
- Returns information about my current login, as well as details about test_login.
- Exits the first sqlcmd connection (the one using sa).
- At this point, the sqlcmd connection as test_login will be established due to chaining the two sqlcmd commands with a semicolon.
- Checks the same information as the first query.

Notice how the password hash version changes between the two connections (arrow 1).
And test_login’s password hash is updates as soon as the connection is initiated, judging by the 27 milliseconds difference between the modify_date timestamp and the now one (arrow 2).
So, SQL Server 2012-2022 password hashes can be migrated to SQL Server 2025 without issues, but they are updated to SQL Server 2025’s PBKDF2 hashing algorithm at the first successful authentication.
I’m guessing that this process was actually implemented in case of an in-place upgrade from 20022 or 2019 to 2025.
Where all the old password hashes will be updated as soon as the pre-upgrade logins will authenticate to the upgraded instance.
Conclusion
SQL Server 2025’s new PBKDF2 hashing algorithm improves security by increasing the time needed for brute-force attacks against the password hashes.
I’m really curios to see if hashcat will be updated to support the new hashing algorithm, since offline cracking tends to be way more efficient and use more/specialized resources.
Keep in mind that this isn’t the same as a login brute-force attack where a user just tries various passwords for a login from SSMS/sqlcmd or any other client/script.