Parsing modern ASP.NET Core Identity password hashes to Hashcat
While this would not be anything out of the ordinary, the environment certainly was. So where do we expect ASP.NET Core Identity hashes? Some obscure cloud application, local Windows application, ... but surely not on an embedded linux system!?
While the vulnerability itself was discovered at this point - exposing password hashes without any authentication is not exactly considered best practice - it was decided to investigate the password hashes further in an attempt to showcase password cracking and detect weak passwords.
Despite numerous tools on the internet advertising the capability of converting ASP.NET Core Identity hashes into Hashcat format, all attempts at cracking the extracted password hashes remained unsuccessful despite knowing some pairs of plaintext passwords and ASP.NET Core Identity hashes.
Why? It was time to dig in!
No Country for Old Hashes: Cracking ASP.NET Core Identity with Hashcat
Before we look at why all of the current tools fail to correctly convert ASP.NET Core Identity hashes into Hashcat format, let us establish some background on the ASP.NET Core Identity Hashes.
ASP.NET Core Identity Hashes
Offical documentation from Microsoft on the structure is sparse. Luckily some amazing blog posts have been written by Filip W. and Rui Figueiredo. To avoid regurgitating their entire works and opinions here, we are going to provide a brief overview of ASP.NET Core Identity password hashes:
ASP.NET Core Identity hashes are base64-encoded byte structures split into multiple sets of magic length and values.
The magic marker starts at the first byte, notated in the following as [0], called the format marker (mostly referred to as version) where we must differentiate between ASP.NET Core Identity v2 and v3 hashes.
The old v2 hashes have a non-configurable structure: The format marker is followed by the salt [1,16] consisting of 16 bytes and the subkey [17,49] with 32 bytes. The key derivation PRF (pseudo-random function) is PBKDF2+HMAC-SHA1 with 1,000 iterations, taking the password and salt as input and producing the 32 bytes long subkey.
With the introduction of ASP.NET Core Identity v3 hashes, the key derivation PRF, number of iterations, length of the salt, and length of the subkey can be customized. The PRF [1,4] can be chosen based on the KeyDerivationPrf enum [3]: either HMACSHA1, HMACSHA256, or HMACSHA512.
The number of iterations (ITER) [5,8] in combination with the chosen PRF defines the work factor.
The length of the salt (SALTLEN) [9,12] is the input of a random number generator which generates the salt. The interval of the salt [13,(13+SALTLEN-1)] therefore depends on this length of the salt.
After the salt, the actual subkey follows. The length of the subkey is not stored inside the ASP.NET Core Identity hash. Therefore, the only way to determine the length of the subkey is to read the byte structure until the end of the input [(13+SALTLEN-1), EOL]. Here, EOL represents the index of the last byte of the structure.
Diving into the implementation in .NET, the PasswordHasher class provides two methods of creating v3 hashes by overloading the HashPasswordV3 method [3]. The first method is a convenience wrapper of the second using static magic values. Here, PRF is set to HMAC-SHA512 (HMAC-SHA256 for .NET versions <7.0), ITER to 100,000 (10,000 for .NET versions <7.0), and SALTLEN to 16. Additionally, 32 bytes are requested back from the PRF (length of the subkey). These values are most often referenced as "default values".
The non-wrapped method allows unrestricted configuration of these values. There is no apparent limit on ITER, SALTLEN, and number of requested subkey bytes. Only the PRF is constrained by the enum Microsoft.AspNetCore.Cryptography.KeyDerivation [4].
While we did not encounter any example that deviates from these "default values", simply assuming static magic values when reading a v3 hash will result in incorrect handling for the "non-default" ASP.NET Core Identity v3 hashes.
The following Listing shows the above described structure of ASP.NET Core Identity hashes:
| [0] | ... | '--> 0x00 ASP.NET Core Identity hashing logic version v2 | | [0] | [1,16] | [17,49] | | | 0x00 | SALT | SUBKEY | | '--> 0x01 ASP.NET Core Identity hashing logic version v3 | [0] | [1,4] | [5,8] | [9,12] | [13,(13+SALTLEN-1)] | [(13+SALTLEN),EOL] | | 0x01 | PRF | ITER | SALTLEN | SALT | SUBKEY | | | | ' --> 10,000 (HMAC-SHA256 .NET < 7.0 default) | ' --> 100,000 (HMAC-SHA512 .NET >= 7.0 default) | ' --> 0x00 HMAC-SHA1 ' --> 0x01 HMAC-SHA256 ' --> 0x02 HMAC-SHA512 ASP.NET_IDENTITY_HASH_... SALT (32 bytes) --> Password salt used in PBKDF2+HMAC-SHA{1,256,512} function SUBKEY (32 bytes) --> Result of PBKDF2+HMAC-SHA{1,256,512} (only v2 -- method HashPasswordV2 in [#1]_) Number of iterations: 1,000 (static) Length of the salt: 16 bytes (static) Length of the subkey: 32 bytes (static) (only v3 -- method HashPasswordV3 in [#1]_) PRF (uint32BE) --> Pseudo-random function used for key derivation (see [#2]_) ITER (uint32BE) --> Number of iterations the hashing algorithm is applied SALTLEN (uint32BE) --> Length of the salt (saltSize)
ASP.NET Core Idenity Hash to Hashcat Format Convertion
To generate valid Hashcat format from ASP.NET Core Identity hashes, the PRF, number of iterations, salt, and subkey must be brought into the following structure:
Let us do this process for the following ASP.NET Core Identity password hash:
Base64-decoded and structured:
01 := (0x) ASP.NET Core Identity v3 hash 00000002 := (0x) HMAC-SHA512 000186a0 := (0x) 0d100,000 iterations f7 2b bc 38 a1 b2 8b d9 34 7c 51 16 bb f5 cb 78 := salt b8 a9 f8 ca b1 79 fe 27 32 83 ec 03 88 97 17 b9 61 26 ff b3 eb 4c 5c 95 50 79 4d 7f fd 54 17 a3 := subkey
ASP.NET Core Identity v3 hash PRF, iterations, salt, and subkey converted to Hashcat format:
When attempting to break the ASP.NET Core Identity hashes with Hashcat, we need to ensure that Hashcat correctly identifies the hash type or set it manually with the hash-mode (aka hash-type). This also means that we need to use multiple Hashcat runs when encountering differing PRFs. Ideally, the hashes with differing PRFs are split into multiple files.
ASP.NET version |
Hash type |
Hashcat hash-mode |
|---|---|---|
v2, v3 |
PBKDF2+HMAC-SHA1 |
12000 |
v3 |
PBKDF2+HMAC-SHA256 |
10900 |
v3 (.NET > 7.0) |
PBKDF2+HMAC-SHA512 |
12100 |
Current Tooling
From what we have seen so far, the process of converting ASP.NET Core Identity hashes seems a bit convoluted due to their magic structure, but otherwise relatively straightforward.
So what occurred during initial attempts of converting the hash with one of the available tools? The tried tools being:
ASP.NETIdentity2hashcat and identity-to-hashcat both lack the support for ASP.NET Core Identity v3 hashes from .NET versions 7.0 or higher. Instead, they wrongfully default to a statically set SHA256 as the PRF.
Further, ASP.NETIdentity2hashcat uses the wrong byte interval for the number of iterations, returning the encoded PRF enum instead.
ASP.NET Identity seems to use a more solid approach by using the AspNetIdentityHashInfo class from the NetDevPack.Utilities namespace instead of using their own implementation. Yet similarly, this falls short of .NET version 7.0 and higher compatibility. In this case it was necessary to dig deeper to find out why the resulting Hashcat hashes cannot be broken. The answer lies in the NetDevPack repo on commit b3106b8. Therefore, all three tools do not handle configured ASP.NET Core Identity v3 hashes: Only SHA256 support as well as the default values for the length of the salt and length of subkey are statically assumed as 16 and 32 bytes respectively.
Instead of displaying any warnings or errors, the tools report back false PRFs, salts, and subkeys.
TL;DR. The tested tools lack support for the PRF HMAC-SHA512 which was introduced with .NET version 7.0 (which was released in 2022). We need a tool that supports all currently supported ASP.NET Core Identity hashes and .NET versions.
Contributions
Instead of extending one of the existing tools, a ground-up approach was taken to reach the following goals:
Clear type system for the structured components of ASP.NET Core Identity hashes. Errors are thrown if parsed content does not fit type system. Type system can be extended for future .NET releases.
- Input of ASP.NET Core Identity hashes:
Capability to parse large numbers of ASP.NET Core Identity hashes at once. This allows us to parse entire excerpts of real-world ASP.NET Core Identity databases which may hold hundreds of user entries.
Allow annotations of the hashes which can be used for usernames, suspected passwords, or other remarks.
- Output of Hashcat formatted hashes:
Structured output split by PRF due to Hashcat expecting only a single hash type in its input file.
Annotations from input file can be prepended to the formatted hashes for compatibility with the Hashcat
--usernameoption.
Documentation on the ASP.NET Core Identity structure, conversion process, and supported versions/PRFs. At time of writing v2 and v3 (PBKDF2+HMAC-SHA{1,256,512}) with default or custom configuration hashes are supported.
Test cases for all "default variants" of ASP.NET Core Identity hashes to confirm proper conversion.
Verbose output options to allow debugging.
Recommendation for ASP.NET Core Identity Production Use
If you are using ASP.NET Core Identity hashes for authentication, we strongly recommend reviewing the work effort of all stored hashes. OWASP publishes and continuously updates their recommendations for the work effort of PBKDF2. The .NET "defaults" do not meet these recommendations at all:
Key derivation and hash |
.NET "defaults" |
OWASP recommendations |
|---|---|---|
PBKDF2+HMAC-SHA1 |
1,000 |
1,400,000 iterations |
PBKDF2+HMAC-SHA256 |
10,000 |
600,000 iterations |
PBKDF2+HMAC-SHA512 |
100,000 |
220,000 iterations |
To the best of our knowledge, newly created "default" ASP.NET Core Identity v3 hashes for .NET version 7.0+ use PBKDF2-HMAC-SHA512 which only lags behind the OWASP recommendations by a factor of two.
.NET versions below 7.0 ASP.NET Core Identity v3 "default" hashes use PBKDF2-HMAC-SHA256 with work effort an order of magnitude lower than recommended by OWASP.
Regardless of the .NET version: If ASP.NET Core Identity v2 hashes are generated, PBKDF2-HMAC-SHA1 with 1,000 iterations will always be used. This is multiple magnitudes below current OWASP recommendations. It cannot be customized. This poses direct risk by allowing attackers to break hashed and salted passwords without major computational and time effort. Therefore, v2 hashes are always a bad sign.
Perhaps it would be a good time for Microsoft to set their own standards or mandate those of others, rather than passing the buck down to developers and customers.