As part of a Red Team engagement I found myself looking for a way to bypass two-factor authentication (2FA) in Lastpass. Unfortunately this happened before Tavis Ormandy reported multiple 0-days in Lastpass. Would have saved us so much time! Anyway, 2FA is an additional layer of security to protect user accounts from attackers that have already compromised your password. I mention this because it is key to understand the purpose of this post.
When you login into a service using your username and password, you will get an additional challenge before access is granted. Usually it is a 6 digit temporary code that changes every 30 seconds. Google authenticator, Authy and Toopher are just a few of the 2FA solutions Lastpass supports that are based on RFC6238 and RFC4226. There are other types of 2FA but these are the most common.
Temporary codes are generated based on several variables, including a secret seed and a timestamp. The secret makes it secure and unique, the time makes it change in intervals of typically 30 seconds. The server needs to share the secret seed with the client to be able to setup and sync the code generation process. This is usually done by encoding the seed in a QR code that you scan with your 2FA app.
LastPass 2FA implementation
The first thing I checked when looking for ways to bypass 2FA was to see how the QR code was stored. In the end, this is what holds the secret to generate valid temp codes. If I can steal it somehow, I will be able to login (since I already had the password).
I enabled Google authenticator in my test account and when I was prompted to scan the QR code I checked the source code.
As you can see, the QR code is a img tag which source attribute points to:
I previously researched Lastpass and did a presentation at Blackhat EU. When I saw the parameter “passwordhash” I knew immediately what it was. passwordhash is the “login hash” LastPass uses for authentication. It consists of a SHA256-PBKDF2 of your password with the number of rounds indicated by the “iterations” parameter + 1. Basically, Lastpass derives the encryption key that protects the vault from the master password using PBKDF2. Because Lastpass does not want to know your encryption key, they apply an additional round of PBKDF2 and use that as the login hash (the passwordhash parameter).
What does this all mean? Lastpass is storing the 2FA secret seed under a URL that can be derived from your password. This literally beats the entire purpose of 2FA which, as mentioned above, is a layer of security to prevent attackers already in possession of the password from logging in. To put it in perspective, imagine that you have a safe in your house where you keep your most valuable belongings. Do you think it is a good idea to have the same lock for the door and the safe? Should the door key open the safe as well?
QR code retrieval requires authentication
While we know how to obtain the secret URL to get the QR code, it is an authenticated request. This means that we need to be authenticated in order to retrieve the QR code. Because we are not authenticated yet (we need the QR code for it) we cannot retrieve it. Chicken and egg problem… or not.
The request is authenticated but we can force the victim to make the request for us. This is known as a Cross-Site Request Forgery (CSRF) vulnerability. Usually, targets are “state changing requests” (actions that update, create or delete a resource) since Same Origin Policy (SOP) does not let the attacker see the response.
Unfortunately, Lastpass serves the QR code as a pure image file. The attacker can set an “img” tag on his domain with the “src” property pointing to the 2FA URL.
The image will load under the attacker’s domain context without any SOP limitations (read the update at the bottom).
It is also worth noting that it is not necessary for an attacker to lurk the victim into visiting his malicious website. Any XSS on sites trusted by the victim like Facebook or Gmail can be used by the attacker to add a payload to steal the QR Code and send it back to his server.
There are other consequences to Lastpass 2FA design. As explained above, the QR code is retrieved by passing the login hash as a URL parameter. This is (almost) the equivalent of passing the password in the URL. As mentioned above, the login hash is different from the master key that decrypts the vault. Specifically, 1 round of SHA256-PBKDF2.
Passing secrets in the URL is a bad security practice. URLs are logged by webservers. Lastpass may protect the database storing all users data but do they apply the same security standards to the logs? It is common for attackers to target logs looking for secrets.
URLs also leak in caches, proxies, referer headers and even the browser history.
Disabling 2FA with classic CSRF
The reason I thought I should write a post about my findings is because I wanted to highlight the importance of keeping passwords and 2FA as “separate entities”. Truth is, I found another, more straightforward way of disabling 2FA but less interesting technically.
Lastpass let’s you regenerate the 2FA secret seed (in case it was compromised for example). The implementation of this feature has 3 issues:
- Regenerating the seed does not ask you to verify the setup by asking for a temp code
- Regenerating the seed does automatically disable 2FA (instead of leaving it enabled but with the new seed)
- The request to regenerate the seed is vulnerable to CSRF
The combination of all these issues makes it possible for an attacker to disable the victim’s 2FA protection.
The request to disable 2FA looks like this:
GET /google_auth_regenerate_key.php HTTP/1.1
Classic CSRF. GET request used for a state-changing request with no CSRF token. A victim visiting an attacker controlled site can be forced to make this request and disable 2FA.
Lastpass pushed some initial fixes from day one and is working on identifying all CSRF vulnerable request (there were more). Disabling 2FA through the CSRF vulnerability was fixed by adding a CSRF token.
In terms of the the insecure 2FA design, they pushed a initial fix to check the Origin header. This will ensure that the request to obtain the QR Code can only come from lastpass.com. This is good as a immediate fix but does not work on older browser that don’t support the Origin header.
They also stopped using the login hash to retrieve the QR Code and use a “uidhash” instead. uidhash is “a SHA256 hash of the uid combined with a server secret combined with a random per-user salt (that we look up based on the User’s uid). We also store a hash of the value in our database so we can enforce expiry of the value after X minutes, and also to ensure that the user actually requested that the information be released“.
- 02/07/2017: Disclosure to Lastpass
- 02/08/2017: Bugs acknowledged. CSRF is fixed, origin check is added, password hash is not used anymore.
- 02/10/2017: Bounties issued
3 different people (a coworker, a reddit user and an anonymous person that commented below) noticed an error I made in this article that is key. I mention that “The image will load under the attacker’s domain context without any SOP limitations”. This is not true. CORS will prevent accessing the image data from the attacker’s domain.
Back in February when I contacted Lastpass about this issue, I wrote a PoC to test my findings. When I saw the comments I realized my mistake and went back to look for the code I wrote. While trying to get a QR decoder JS library working, I was testing pointing at a local image file. I never updated my PoC to point to Lastpass domain and made the wrong assumption based on the fact that the PoC worked.
While the 2FA implementation is still bad, it is not exploitable. Of course, you can still disable 2FA through standard CSRF as I explain in the article. I contacted Lastpass again to let them know that one of the 2 issues I reported is not exploitable.
Thanks to the 3 guys that reached out. The lesson learned today is, double-check the theory before making practical assumptions.