We frequently encounter situations during our assessments (particularly red teams and internal network pentests) where an attack is theoretically possible but not practically possible during the engagement time window. A real world attacker, however, could take as much time as deemed proportionate for the payoff. In order to provide realistic adversarial simulations, we work outside of engagements to prepare for these scenarios.

In this four-post series, we present the results of one such avenue of research: a hardware-accelerated password recovery tool for Bitwarden servers. Bitwarden is an open-source password manager. It can sync data from various client applications—mobile, PC, browser—to the main Bitwarden server (vault.bitwarden.com) or to a self-hosted server instance. Researchers have published a significant amount of analysis and tooling for recovering passwords from a client application cache, but there is little to be found about post-compromise of a server. In theory, server files should be protected even more strongly than client application caches. In practice, we found that to be true, but we have still been able to extract all stored passwords from real-life server instances using the custom tooling covered in these posts.

To be clear, we have not discovered any vulnerability in Bitwarden, nor are we presenting any flaw in its security model. There is a known threat inherent to any application that stores all its data on the server: compromise of the server can allow attackers to brute-force user passwords. We are presenting tooling that realizes this threat for older versions of the Bitwarden server, which can be used by server operators to make realistic estimates of attacking difficulty.

In this post we’ll discuss the details of the Bitwarden server security model as they pertain to authentication and hashing. In the following two posts we will walk through our released code and provide commentary. The final post concludes with results and lessons learned.

Advice for Operators

First, what does this research mean for Bitwarden users who do not deploy their own servers, for example users of the cloud-hosted https://vault.bitwarden.com (or .eu)? Practically nothing. You should obviously continue to use a long, preferably random, Master Password.

If you or your IT department deploy a self-hosted Bitwarden vault, this research is more relevant, but it still doesn’t change much. Securing the filesystem backing the Bitwarden server is critically important. This research provides evidence that cracking a Bitwarden server database given full filesystem access to the server is not just theoretically possible but practically possible as well. We recommend double-checking the security protections of the server host, any backups, and any host that stores backups.

Security Model

Bitwarden uses a security model1 that is pretty standard for cloud-syncing password managers. Each user creates a Master Password, and all secrets are encrypted with that password. Client applications (browser plugin, phone app, etc.) cache encrypted password databases locally, but can also send encrypted passwords to a configured cloud server. A new client application can request the encrypted passwords from the server database to initialize a new local cache, but it has to authenticate first. Let’s walk through this process of logging in to and accessing a secret stored in a Bitwarden server:

  1. A user accesses the instance in a web browser (this could also be a mobile application or other client).
  2. The user enters their email and Master Password. The web browser computes the Master Password Hash (details later) and sends it to the server.
  3. The server computes another hash operation on the Master Password Hash and compares the result to the masterpassword field of the User table in the database (this value is “protected”, as discussed in more detail in a later post). If there’s a match, the server marks the session as authenticated and continues.
  4. The user requests a secret, and the server retrieves the corresponding entry from the database and serves it to the client. There are a few more cryptographic steps here that depend on the exact scenario, but the gist of it is that the Master Password is used as the lynchpin that decrypts everything else. See the Bitwarden Security Whitepaper for more details.

Our goal, taking an attacker’s mindset, is to recompute the Master Password for entries in the database. An attacker who compromises the server database will have a column of authentication hashes (as mentioned previously, there is an extra processing step required here) which can be brute-forced, just as any other password hash can be, to recover the Master Password. The Master Password can then be used to log in to the web interface and retrieve all the secrets or decrypt the entries from the database.

Diversion: what if an attacker compromises a database and has a user’s Master Password, but the user has 2FA enabled? Well, 2FA is not cryptographically relevant. One solution for the attacker is to run a modified server that doesn’t enforce 2FA. Another option is to just null the 2FA fields in the database.

Further diversion: why isn’t 2FA used to cryptographically protect secrets? The 2FA method that enjoys the most ubiquitous support is TOTP (HOTP shares the same properties). In TOTP, both the server and the device store an identical shared secret, and authentication consists of proving to the server that the device knows the same secret as it. This scheme can’t be used to cryptographically protect data on the server because it inherently requires the secret to be accessible to the server process as plaintext. There are other 2FA schemes that could be used to cryptographically protect a user’s secrets on the server (including the trivial scheme of having a phone app that transparently appends a stored secret to a user’s Master Password during authentication), but there are big trade-offs in terms of usability, adoption, and security.

Algorithms

So what exactly are the hashing algorithms? Let’s build it up piece by piece.

The password a user types in is their Master Password.

The Bitwarden client calculates their Master Key with Master Key = PBKDF2-SHA256(password=Master Password, key=Email Address, rounds=600000), where PBKDF2-SHA256 is Password-Based Key Derivation Function 2 using SHA256 as the underlying hash function. The number of rounds is configurable by the user, but the default is 600000. The user can also switch from PBKDF2-SHA256 to Argon2, but we didn’t analyze or write code to support that. The Master Key is the basis of all secret encryption and decryption, which we won’t cover in this post (see Bitwarden’s whitepaper).

The Bitwarden client calculates the Master Password Hash with Master Password Hash = PBKDF2-SHA256(password=Master Key, key=Master Password, rounds=1)2.

To authenticate to a cloud server, the Bitwarden clients sends the Master Password Hash encoded in base64. The server treats this incoming value as a password and uses ASP.NET Core’s PasswordHasher3 functionality to further hash the value. The specific algorithm varies between versions, but the version we looked at used PBKDF2-SHA256(password=base64(Master Password Hash), key=Email Address, rounds=10000). The resultant value is stored in the database using ASP.NET Core’s Data Protection, which is covered in detail later.

Putting it all together, the hash stored on the server looks like this:

Server Password Hash(Master Password, Email Address):
    return PBKDF2-256(
        password=base64(PBKDF2-SHA256(
            password=PBKDF2-SHA256(
                password=Master Password,
                key=Email Address,
                rounds=600000
            ),
            key=Master Password,
            rounds=1
        )),
        key=Email Address,
        rounds=10000
    )

Diversion: Modern Bitwarden clients cache yet another hash type, used to verify the Master Password and unlock the local database. Instead of storing the Master Password Hash, they take the Master Key and calculate PBKDF2-SHA256(password=Master Key, key=Master Password, rounds=2) instead of rounds=1 to get the Master Password Hash. Even though this isn’t documented in the whitepaper1, a user sent an email and got a response saying that the rationale is so that the cached value can’t simply be sent to a cloud server to authenticate. This password hash is what the existing Bitwarden hashcat plugin (hash mode 23400) cracks. Prior to 2021, clients cached the Master Password Hash.

Versioning

We developed the proof of concepts covered in this post series a while ago. Bitwarden and its underlying libraries have released updates that support stronger hashing algorithms, leading to better security for users. We are only publishing code that works with Bitwarden servers running version 2024.2.2 or older, and will only work for users who have not switched to Argon2 in their settings (Argon2 has been available since version 2023.2.0). The breaking change in 2024 was this commit, which upgraded the server library from .NET 6 to .NET 8, which included this pull request, which changed changed the PRF from hmac-sha256 to hmac-sha512.

To recap, our hashcat plugin supports:

  • Client-side hash: PBKDF2-SHA256, any number of rounds
  • Server-side hash: PBKDF2-SHA256, any number of rounds

Our plugin does not support:

  • Client-side hash: Argon2
  • Server-side hash: PBKDF2-SHA512

Next Up

In this post we explained how Bitwarden calculates and stores password hashes. In the next post, we’ll release our hashcat plugin and dive into its technical details. After that, we’ll take a detour into a technical analysis and re-implementation of ASP.NET Core’s Data Protection functionality, which was necessary for a pure Python hash extractor tool. Finally, we’ll wrap up with our lessons learned.

  1. https://bitwarden.com/help/bitwarden-security-white-paper/  2

  2. The whitepaper calls this PBKDF-SHA256 twice, even though it uses the term PBKDF2-SHA256 for other parts of the algorithm. As far as I can tell, this is misleading or incorrect. Bitwarden code uses PBKDF2 for everything and never uses PBKDF1

  3. https://github.com/dotnet/aspnetcore/blob/main/src/Identity/Extensions.Core/src/PasswordHasher.cs