Before I began comprehending the science of information security, it seemed that Two-Factor Authentication was a guaranteed way to protect your account. I thought hackers could, let’s say, steal my in-game currency to buy clothes for their characters in the game account. But over time, it has been proved empirically — two-factor authentication implementations may have many vulnerabilities.
In simple words, Two-Factor Authentication is a confirmation of the action by entering the generated code to increase security and throw sticks into the wheels of hypothetical hackers during the movement or before it starts.
In a good way, 2fa should be presented on cryptocurrency exchangers, banking systems, and all sites whose users’ accounts are of a particular value to make it difficult to hack an account to gain benefits or information.
The code verification system is widespread; it’s used everywhere on various sites and can be connected to both primary and secondary logins. But the use-case is not limited to this — the developers attach a code confirmation to the functionality of password recovery, verification of registration/subscription, additional confirmation of money transfers, password change, and change of personal data. Also, sometimes, 2FA can be used as a wall after “timing” logout instead of a password or other confirmations.
In this article, I have collected ways to test 2FA for vulnerabilities, their exploitation, and possible options for bypassing existing protection against specific attacks. Let’s look at the list of checks for vulnerabilities that apply to 2FA.
A lack of rate limit
The rate Limit algorithm checks whether a user session (or IP address) can be limited in attempts or speed and under what circumstances this happens. If the user has sent too many requests within a certain period, the web application can respond with a 429 code (many requests) or apply a rate limit without showing errors. The absence of a rate limit implies that during a typical enumeration, there will be no restrictions on the number of attempts and/or speed — it is allowed to iterate over codes any amount of times (at any rate) within the session/token validity period.
Quite often, I have to deal with a “silent” rate limit, — if you saw that there are no errors and the HTTP body/code doesn’t change in subsequent requests, it’s early to rejoice, and firstly, you need to check the final result of the attack using valid code.
Rate limit exists, but it can be bypassed
Here are the cases I had encountered before:
- Rate limit occurs after a certain speed, not the number of attempts.
Often, security analysts try to pick up code using two or more threads to make an attack faster (in Burp Intruder, the default number of threads is two without delay). But sometimes, a security system from brute force or a regular Load Balancer can only respond to this single factor. If you are trying to brute-force with two threads, it is worth reducing the number to 1 and then to 1 with a delay of one second. Earlier, I was lucky to observe such behavior, and precisely with the help of such manipulations, the successful hit of code occurred, which led to Account Takeover. If the 2FA code doesn’t have a specific expiration date, we have a lot of time to enumerate it. But if the validity period is presented, the attack’s success is reduced. However, the potential threat of vulnerability is still there since there is still a chance of guessing a correct code.
- The generated OTP code doesn’t change.
This doesn’t apply to dynamically-changing codes like in Google Authenticator, but only static ones that come by SMS, email, or as a message in the messenger. The essence of this bypass is that constantly or for some time, for example, 5 minutes, the same OTP code is sent in SMS, which is valid all this time. It is also worthwhile to ensure that no silent rate limit occurs. Report example: https://hackerone.com/reports/420163
Suppose the application generates a random code from 001 to 999 and sends it directly to the phone’s messages; within 10 minutes, when the “send again” functionality is enabled, we get the same code. But the rate limit is attached to the request, which limits the number of attempts per request token. We can constantly request a new code, generate a new request token, apply it to a subsequent request (using grep-match in a Burp Suite or using our script) and conduct brute force of the range of numbers from 001 to 999. Thus, constantly using a new request token, we will successfully pick up the correct code since it doesn’t change and is static for a certain period. The limitations of this attack are a long number or mixing letters with numbers as a confirmation code.
In case of such a complication, such a bug shouldn’t be considered “useless”. An attacker can try to enumerate through at least part of the brute force list, and there is a possibility that the generated code will appear in this part of the list since it is generated randomly. When trying, they need to rely on random, but still, there is a chance of getting into the right combination, proving that it’s a vulnerability that needs to be fixed.
- Rate-limit resetting when updating the code.
In a code verification request, a rate limit is presented. Still, after activating the functionality of code re-sending, it resets and allows you to continue the brute force of the code. Examples of reports:
- https://hackerone.com/reports/149598 — theory;
- https://hackerone.com/reports/205000 — a practical exploit based on a previous ticket.
- Bypassing the rate limit by changing the IP address. Many rate limit implementations are based on the restriction of accepting requests from IP, which has reached the threshold of a certain number of attempts upon sending a request. If the IP address is changed, then there is an opportunity to bypass the restriction. To check this method, change your IP using the Proxy server or VPN and see if the block depends on the IP.
Ways to change IP:
- AWS gateway IP usage via IP Rotator extension for Burp Suite https://github.com/RhinoSecurityLabs/IPRotate_Burp_Extension. This is the best choice because it gives us ~unlimited brute force attempts and IP addresses that allow you to conduct a brute-force attack without 42x errors and interruptions.
Since IP rotate tool sends requests using AWS IP addresses, requests might be blocked if the web application is behind the Cloudflare firewall.
In this case, you need to additionally find the IP of the original web server or find a method that doesn’t use AWS IP addresses.
- A good option might be a python script with a proxy requests module, but first, you need to buy many valid proxies or obtain them from public lists.
- X-Forwarded-For header’s support is turned on in the application.
The built-in header X-Forwarded-For can be used to change IP. If the application has built-in processing for this header, send
X-Forwarded-For: desired_IPto replace the IP and bypass the restriction without additional proxies. The web server will think that our IP address matches the value transmitted through the header whenever a request is sent using X-Forwarded-For. Related materials:
Substitution of values from the session of another account
If a parameter with a specific value is sent to verify the code in the request, try sending the value from the request of another account. For example, when sending an OTP code, the form ID, user ID, or cookie is checked, which is associated with sending the code. If we apply the data from the parameters of the account on which you want to bypass code verification (Account 1) to a session of a completely different account (Account 2), receive the code, and enter it on the second account, we can bypass the protection on the first account. After reloading the page, 2FA should disappear.
MFA bypass using the “memorization” feature.
Many sites that support MFA have a “remember me” functionality. It is useful when the user doesn’t want to enter a 2FA code on subsequent login attempts. From the security perspective, it is crucial to identify how 2FA is “remembered.” This can be a cookie, a value in session/local storage, or simply attaching 2FA to an IP address.
If 2FA is attached using a cookie, the cookie value must be unguessable. If a cookie consists of a set of numbers that increased for each account, it is possible to apply a brute-force attack for cookie value and bypass 2FA. In addition, developers should supply the cookie (along with the session cookie and CSRF token) with the HttpOnly attribute so that it cannot be stolen using XSS and used to bypass 2FA.
If 2FA is attached to an IP address, you can try to replace your IP address. To identify this method, log in to your account with the 2FA “memorization” feature, switch to another browser or incognito mode of the current browser, and log in again. If 2FA is not requested, then 2FA was attached to the IP address. To replace the IP address, use the X-Forwarded-For header when entering the login and password if the web application supports such a header. Using this header, you can bypass the “IP address white-list” feature if one is present in the account settings. It can be used in the chain with 2FA as additional account protection or may not even request 2FA if the IP address matches the white-listed one). Thus, even without abusing the “memorization” feature, in some cases, 2FA can be bypassed with the help of bypassing the associated protection methods.
Attaching 2FA to an IP address is not a safe way of protection since if you are on the same network or connected to the same VPN or ISP with a static IP address, 2FA can be avoided.
Improper access control bug on the 2FA dialog page
Sometimes a dialog page for entering 2FA is presented as a URL with parameters. Accessing such a page with parameters in the URL with session data that do not match the data used to generate the page or without session data is unsafe. But if the developers decide to accept the risks, then you need to go through a few important points:
- Does the link for the 2FA dialog page expire?
- Is the link indexed by search engines? If the link has a long period of existence and/or the search engines indexed valid links for 2FA input (there are no restrictions in robots.txt/meta tags), then impact is presented. It’s possible to use the 2FA bypass mechanism on the 2FA input page, in which we can skip the requirement of knowing login and password and gain access to someone else’s account.
insufficient censorship of personal data on the intermediate 2FA page
On the intermediate 2FA page, data such as email, phone number, nickname, etc., should be hidden. However, developers don’t always hide personal data; it can be fully disclosed in the API endpoints and other requests for which we have enough rights at the 2FA stage. If an application exposes data that initially wasn’t known, for example, we entered only a login without knowing the phone number; this is considered an “Information Disclosure” vulnerability. Knowledge of the phone number/email can be used for subsequent phishing, brute force attacks, or in a chain with other vulnerabilities.
Here’s an example of exploiting a vulnerability using Credentials Stuffing. There is a publicly accessible database with logins and passwords for site A. Attackers can use the data from this database on website B:
- First, they check whether the user exists in the database of site B using the “Accounts Enumeration” bug in registration/password recovery functionality. Typically, many application developers do not consider this vulnerability and accept risks. “Vulnerability” lies in the presence of an error that discloses user registration on the site. Ideally, a secure message on the password recovery page should be as follows:
- Attackers spray passwords for the accounts after confirming that users exist in the database.
- If they encounter 2FA, they are at a dead end. But in case of an absence of user data censorship, they can supplement their database with additional users’ data if they were not in the original database.
2FA is being ignored under certain conditions
When accomplishing some actions that lead to automatic login to your account, 2FA may not be requested.
- 2FA ignoring when recovering a password. Many services apply auto-login to the account after completing a password recovery procedure. Since access to the account is provided instantly, when you log in to your account, 2FA can be skipped and completely ignored.
Here’s an impact of a similar report on HackerOne I sent recently:
If an attacker gains access to the victim’s email (an account can be hacked using phishing, brute-force attacks, credentials stuffing, etc.), they can bypass 2FA, although in this case, 2FA should protect the account. Currently, for 2FA, there is a check of the TOTP code or the backup code, but not the code from the email, so this bypass makes sense.
- 2FA is ignored when logging in through a social network. You can attach a social network to your account to log in quickly in the future and, at the same time, set up 2FA. When you log in to your account via social networks, 2FA can be ignored. If the victim’s email is hacked, it is possible to recover the password to the social network account if it allows you to do this and enter the desired application without entering 2FA. Additionally, access to the social network account can be retrieved without hacking an email using credentials stuffing, applying most used passwords, using a vulnerability in social networks, and abusing a lack of 2FA forcing.
Impact of one of the reports:
- A chain with other vulnerabilities, such as the previously sent OAuth misconfiguration #577468, to completely takeover the account, overcoming 2FA.
- If an attacker hacked the user’s email, they could try to gain access to the social network account and log into the account without additional verification.
- If an attacker once hacked into a victim’s account, they could connect the social network to the account and log into it in the future, completely ignoring 2FA and entering the login/password.
- 2FA is ignored in an older version of the application. Developers often add staging versions of a web application to domains/subdomains to test specific functionalities. Interestingly, if you log in using your username and password, 2FA may not be requested. Perhaps the developers are using an older version of the application in which there is no protection for 2FA, 2FA itself is disabled or it was intentionally disabled for testing.
You can also check other vulnerabilities simultaneously — registering a new user on a staging server whose email exists in the production version database can give you a user’s data from production. It is also worth checking the absence of a rate limit in crucial features, for example, password recovery. An example of such a vulnerability is a [$15,000 Facebook bug](https://www.freecodecamp.org/news/responsible-disclosure-how-i-could-have- hacked-all-facebook-accounts-f47c0252ae4d/), which allowed hacking an account via the brute-force password recovery code at beta.facebook.com.
- In the case of cross-platforming, 2FA is skipped. Implementations of 2FA in mobile, SDK, or desktop versions may differ from the web version of the application. 2FA may be weaker than in the web version or completely absent.
When disabling 2FA, the current code or password is not requested
When disabling 2FA, additional confirmation is not requested, such as the TOTP code, or the code from email/phone, then this behavior has certain risks. With a request without additional checks, a CSRF attack is possible even if a password confirmation is used. If a bypass vector of CSRF protection is found, 2FA can be disabled by visiting a link in the web application or a specially crafted page on the attacker’s website. You can also use the clickjacking vulnerability — after a couple of clicks from the unsuspecting user, 2FA will be disabled. Validation of the previous code will add additional 2FA protection, considering potential CSRF / XSS / Clickjacking attacks and CORS misconfigurations.
An example of the best and recommended protection that should be used is on hackerone.com, — when you turn off 2FA in the form, you need to enter two values simultaneously, — the current code from the google authenticator app and the password.
Previously created sessions remain valid after activation of 2FA
When 2FA is enabled, parallel sessions on the same account should end, and a 2FA dialog should appear. Same with a password change. If the account was compromised and the first response of the victim will be to turn on 2FA, then the attacker’s session will be disabled, and the following login with login and password will require to pass the 2FA check. In general, this is the best practice to be followed. I often notice how cryptocurrency exchanges add such protection — here’s an example of the report on HackerOne.
A lack of Rate-limit in the user’s account
Developers often add 2FA to the various features of the user’s account to increase security. It can be a change of email address, password, confirmation of financial transactions, etc. The rate limit inside of the account may differ from that in the 2FA window upon entering the account. I had often encountered similar cases when it was possible to freely brute-force 2FA code in my account, while at the entrance, a “Strict” rate limit was set up.
If the developers initially added protection against unauthorized data changes, then this protection must be supported, and all possible bypasses should be fixed or mitigated. If a bypass is found, this is considered a security feature bypass that was implemented by the developers, which is often a vulnerability.
Manipulation of API versions
If you see something like
/v*/ in the application’s request, where * is a number, then it’s likely that you can switch to an older version of the API. In the old API version, there may be weak protection, or it may not be presented at all. It is a relatively rare occurrence and occurs in the case when developers forget to remove the old version of the API in the production/staging environment.
/api/v4/login endpoint is responsible for a login request - checking the username and password. If 2FA is activated on the account, this request should be followed by
/endpoint/api/v4/2fa_check endpoint. If we replace the API version before encountering 2FA, in some cases, we can avoid it. Endpoint
/api/v3/login can lead to
/api/v3/login_successful?code=RANDOM, which gets an active user session because an endpoint
2fa_check was not implemented in this API version.
Another example is a rate limit was introduced in the request
/endpoint/api/v3/2fa_check allows you to brute-force the codes without any restrictions.
Improper Access Control in the backup codes request
Backup codes are generated promptly after 2FA is enabled and are available on a single request. After each subsequent call to the request, backup codes can be regenerated or remain unchanged (static codes).
Suppose there are CORS misconfigurations/XSS vulnerabilities and other bugs that allow you to “pull” backup codes from the
/api/backup-codes/show endpoint. In that case, the attacker could steal the codes and bypass 2FA if the username and password are known.
Bonus: Developers of some companies create applications for generating their 2FA codes (examples are Salesforce and Valve). Due to the lack of security, this emphasis on independence from other authentication applications gives attackers an additional attack point for 2FA bypassing. The range of independent 2FA applications is quite extensive and gives those wishing to avoid 2FA protection more opportunities, space for creativity and increases the number of variations of 2FA bypasses.
In general, 2FA and social engenering awareness are one of the most reliable ways to protect your account unless developers store the current 2FA codes of all application users in the admin panel (and this has happened in my practice). If 2FA is implemented securely, there’s a lower chance that the attacker will get past it. I hope the information in the article was helpful for information security researchers, bug bounty hunters, and developers, and it’ll help to minimize the number of vulnerabilities in developing applications. Stay safe!