Even if biometrics are enabled, the vault still has a main password (Bitwardencalls it master password), yousimply do not have to always enter it to unlock the vault. This begs thequestion how the vault is secured if you do not have to enter the masterpassword. Well, the vault is not really encrypted with the master password, butwith an account encryption key, which is itself stored in encrypted formwithin the vault. The key to decrypting the account encryption key is derivedfrom the main password chosen during vault creation.
In other words: When a user enters their main password, Bitwarden derives a keyfrom the password and this key is then used to decrypt the actual accountencryption key which in turn can decrypt the credentials stored in the vault.
The key derived from the main password will be called derived key from nowon and it is exactly where the biometric unlock comes into play. At this point,users can choose to add additional unlock mechanisms, which usually results in anencrypted copy of the derived key being stored at a (hopefully) safe locationfrom which it can be retrieved using biometrics, for example. As a result,whoever can retrieve and decrypt the derived key does not need a password toaccess the vault.
The already described Biometric unlock is implemented based on Windows Hello onWindows machines. Consequently, activating biometric login on Windows means thatthe derived key is encrypted locally using a secret which can be retrievedafter authentication via Windows Hello. So far so good, but as it turns out, itis very much worthwhile to take a closer look into how this is actuallyimplemented in Bitwarden.
After some digging, we found that Bitwarden stored the encrypted copy of thederived key using the Windows Credentials API by callingwindows::win32::Credentials::CredWriteWin the bitwarden/clients Rustcode.Unfortunately, the documentation on how these credentials are protected ismeager, to say the least, which makes it complicated to actually understand whatis happening when the API is used. It seems, as though CredWriteW createscredentials using the Credential Manager,which in turn seems to invoke the lower-level Data Protection API(DPAPI).DPAPI provides a convenient way to store datasecurelysuch that only the given user is able to retrieve the data at a later point intime. The differences between protecting credentials via the Credential Managerand DPAPI are somewhat unclear, however using the Credential Manager seems toprovide additional benefits over usingthe low-level API, for example UI visibility (you can test this yourself bysearching for the Credential Manager application in you Windows Start menu).
Since the workstation running Bitwarden was domain-joined and the domain wasalready compromised, we began hunting for DPAPI keys in order to exploit thismechanism. First, we need to obtain whatever DPAPI stores on the machine itself.
We quickly found that two directories were of particular relevance to us:%AppData%\Microsoft\Protect, which is used to securely store the DPAPIdecryption keys, and %AppData%\Microsoft\Credentials, where the protecteddata resides. Note, that we use some simplifications involving the creation ofsession keys in DPAPI since they are of no relevance to this blog post; detailsare available here.
The following code snippets are taken from our lab environment, but are closelybased on the actual penetration test. First, we obtained the required Bitwardendata file using smbclient from the Impacketproject:
So far we only have shown the derived/biometric key, which is sufficient todecrypt the vault. In practice, however, there is still a little legwork wehave to do. After all, our goal was not to demonstrate that we could decryptthe vault but to get actual credentials from the vault.
We first had to figure out how to decrypt the Bitwarden account encryptionkey, which protects all other information in the vault. Credentials are notalways encrypted with the account encryption key directly since Bitwardenalso supports usage scenarios where some credentials should be shared in anorganization, for example. This is solved by adding additional layers of keys(private and organizational keys), which are used to encrypt organizationcredentials, and are also protected using the account encryption key. Take alook at the figure below if you are starting to get confused about all the keysinvolved in this story.
We started by extracting the user object from the Bitwarden data filedata.json. The user object contains the encrypted credentials (in thedata section) and the encryption keys (in the keys section), in additionto (unencrypted) information about the user account like the email address(profile), settings like the online vault URL (settings) and more. Theuser section can be identified by searching for a UUID which is used as keyin the JSON object:
As indicated by the leading 2, our account encryption key was encryptedusing AES-CBC-256, and integrity-protected using an HMAC based on SHA-256.Before decrypting the account key with the biometric key, it has to beexpanded using HKDF-expand, though.
As can be inferred from the encryption type, this key was also encryptedusing AES-CBC. In our test setup, the private RSA key could be used todecrypt additional organizational keys, which were in turn required toaccess organization-specific credentials.
In the process of trying to understand this whole mechanism, we started playingaround with DPAPI in our lab setup. For example, we thought that using the APIitself is probably easier than cobbling together the key files from %AppData%.To familiarize ourselves with DPAPI, we wrote a tiny Go program to see whatcredentials are stored in DPAPI on the workstation using thewincred library. The full script is availablehere, but the only interesting lines are these:
This means that any process that runs as the low-privileged user session cansimply ask DPAPI for the credentials to unlock the vault, no questions asked andno PIN or fingerprint prompt required and Windows Hello is not even involved atall. The only caveat is that this does not work for other user accounts.
We contacted both Bitwarden and Microsoft about the details of this attack. Wealways make sure to follow industry best practices for responsible disclosure,even if it is unclear whether our findings are actual vulnerabilities. For theattack explained in this blog post, this was the case as it is not clear whetherthe attack is in scope of either Bitwarden (since the attack already assumesaccess to the workstation of the victim and the Windows domain) or Microsoft(who are only involved by providing DPAPI to store the decryption keys).Microsoft indeed responded to our report by stating that DPAPI and its backupmechanism were used exactly as intended, and that our attack therefore did notindicate any vulnerabilities on their side.
Bitwarden also responded, however they agreed that this behavior was unintended,and stated that they were already tracking a similar issue internally. As itturns out, we were not the first to discover this in March 2023, it had alreadybeen reported to Bitwarden throughHackerOne. However, we did not know thissince it was only disclosed in June 2023. Since then the vulnerability is knownas CVE-2023-27706.
Bitwarden has since made changes to their codebase to mitigate this particularscenario, which we will quickly summarize in the next section. They have alsochanged the default setting when using Windows Hello as login feature to requireentering the main password at least once when Bitwarden is started.
To prepare for this blog post, we also took a look at how credentials are storedin the current version of Bitwarden Desktopv2023.10.1.Bitwarden still makes use of Windows Hello and DPAPI, which are still accessibleusing the domain backup key. However the content of the secured data blobs haschanged:
Instead of storing the valuable derived/biometric key via DPAPI, Bitwarden nowstores two secured data blobs, which are encrypted and can no longer be used todecrypt the account encryption key directly (indicated by the encryption type0). The stored data blobs are now additionally encrypted using theKeyCredentialManagerAPIwhich actually requires interaction with Windows Hello to produce a decryptionkey. While it seems to us that this fixes the issue, we have not actually testedthe new implementation for other vulnerabilities, yet.
It seems that using "\," is still not considered correct by the GUI of MSIX packaging tool, but it does not complain when using the hex value for comma, i.e. "CN=TMurgent Technologies \2C LLP".
I don't have a test certificate at hand with a command in the publisher name to fully test it, but according to their docs it should work.
In addition, when there is documentation on all of this, the documentation should be clear about what to include in this field. There will be confusion on if OU= parts should be included. Just make it clear in the documentation, especially for people that don't deal in certificates regularly.
Entering the escape works to get past the UI dialog and makes a package if you don't sign it. The AppXManifest ends up with the \2C in the publishing field, but then the signtool errors (8007000b) against it. I'm not sure if the fault on that is the PackagingTool or signtool.
I would think that the least confusing solution for everyone is to let the PackagingTool accept the comma (and other escape-worthy characters) and place them in the Publishing field as is, and then fix the signtool to understand and escape if necessary.
Of course the best solution might be to let me point to the certificate earlier in the PackagingTool and have it extract what it needs rather than allow us to mess this all up. Probably still need to fix signtool, but the more of this that can be automated the better.
I have found that, at least on newer Packaging Tool and newer OSs, it is now possible to sign with this certificate. The key was to include the string in quotation marks, but also to include the entire subject line, and not just the CN= portion of it.
7fc3f7cf58