The Importance of Testing Multi-Factor Authentication

Fingerprint Icon


Multi-Factor Authentication (MFA), also known as Two-Factor Authentication (2FA) or Two-Step Verification (2SV), is widely accepted as one of the key security controls that should be implemented to protect user and administrative accounts, especially for externally facing systems.

Although the absence of MFA will frequently be flagged as a security issue (and can constitute a failure in some standards such as PCI DSS or Cyber Essentials), it is also crucial that the security and strength of the MFA deployment is thoroughly tested to ensure that it cannot be bypassed, and that it provides the expected level of protection. However, this is an area that can often be overlooked when carrying out penetration testing, particularly for applications that have their own bespoke MFA implementations.

In order to help address this gap, CODA has helped to produce a new guide on testing multi-factor authentication, which is included in the excellent OWASP Web Security Testing Guide (WSTG).

Avoiding MFA

The most important thing when testing MFA is to determine exactly what it covers, and more importantly, what is does not cover.

There will often be specific user accounts that are exempted from MFA, for various reasons. These can be service accounts or break-glass accounts but can also be people who are “too important” to bother with MFA. These are exactly the kinds of accounts that are an attractive target for attackers, as they’re likely to have elevated privileges and access to sensitive data.

MFA is also often not consistently applied across all systems and applications, particularly in heterogeneous environments. This can include things like third-party web applications that are not using SSO, DMZ systems that are not joined to the main Active Directory domain, or odd Linux servers in predominately Windows environments. Other systems that are frequently overlooked are hypervisors, network devices, physical appliances such as UPSs, and out-of-band management cards.

Where MFA is configured for a system, it’s important that this is done in a comprehensive manner. Requiring MFA when users connect to a system over remote desktop is good, but if they can access the files over SMB, execute commands with impacket and connect to the SQL Server database without that second factor, then the protection provided is minimal. Equally, if the MFA is only enforced on the main login form of a web application, then it may be possible to bypass it using other API endpoints, such as those used by an associated mobile application. Alternatively, there may be functionality that is specifically designed to bypass the MFA in order to allow users to recover access to their accounts.

Breaking MFA

Once it’s been established that the MFA deployment covers all of the required accounts, systems and services, the implementation itself needs to be reviewed and tested to ensure that it is appropriately robust. Where a third-party service is relied upon, this should include the usual dependency management processes, to ensure that the solution is securely developed, supported and patched.

It is also important to ensure that the MFA implementation includes the same security controls implemented in other steps of the authentication process, particularly around account lockout, auditing and logging. Most applications will lock out a user account after multiple failed passwords (and record these failed attempts in an audit log) - but these same controls are often missing on the second step of the authentication. If the second factor is relatively short (such as the six digit codes uses by most TOTP implementations), this can leave it vulnerable to brute-force attacks, which can be surprisingly easy to carry out, as discussed in the example below. And if these attempts aren’t logged and monitored, there is likely to be little evidence that the attack has taken place.

Case Study - Brute-Forcing TOTP

Time-based One-Time Passwords (TOTP) are one of the most common ways to implement MFA on an application, and are widely supported across many applications, libraries and frameworks. It’s most often used with a mobile app, which generates a six-digit code based on a shared secret and the current time, with a new code being generated every 30 seconds. This code would be submitted after the user has entered the correct username and password. Many implementations will allow not just the current code, but also the code(s) on either side (i.e, the previous and next codes), in order to accommodate slight differences in the system clocks of the devices and the time required to submit the request.

Most applications implement some form of account lockout, in order to protect against brute-force or password guessing attacks. However, this is often only done on the initial stage of the login (the username and password), and not one the second stage (the TOTP code). If an attacker is able to brute-force a TOTP code, then they can effectively bypass the MFA, and gain access to the victim’s account with just the username and password.

At first glance, calculating how hard it is to brute-force this seems quite complicated, with the codes changing every 30 seconds and multiple codes being valid at once. However, it’s actually relatively simple.

Each attempt that we make has a 3 in 1,000,000 chance of being correct, as there are 1,000,000 possible six-digit codes, and the previous, current and last codes are accepted. This means that the chance of a code being correct is 0.000003, and thus the chance of a code being incorrect is 0.999997.

Because of the relatively small number of codes we can try in a 30 second window, we can assume that each attempt is independent from every other attempt. And this means that we can calculate the chance of multiple incorrect attempts by multiplying the probabilities together: so the chance of three attempts all being incorrect is 0.999997 * 0.999997 * 0.999997 (or 0.999997^3), which is 0.999991 - meaning that the chance of one of these codes being correct is 0.000009 or 0.0009%.

Based on this, we can use the following (Python) code to calculate the chance of a correct code when we make large numbers of requests over a period of hours:

(1 - (valid_codes / 1000000))**(3600 * attempts_per_second * hours)

If we set valid_codes to 3 (the previous, current and next codes), attempts_per_second to 10 (which is easily achievable from a single system), and hours to 4, then we get a result of 0.649, meaning that there’s a 65% chance that every single attempt we made is incorrect. Or to put it another way, there’s a 35% chance that we guessed the correct TOTP code after four hours.

The table below (also included in the WSTG MFA Testing Guide) shows how the success rate changes with different numbers of valid codes and attack lengths:

Valid Codes Success rate after 1 hour Success rate after 4 hours Success rate after 12 hours Success rate after 24 hours
1 4% 13% 35% 58%
3 10% 35% 72% 92%
5 16% 51% 88% 99%
7 22% 63% 95% 99%

It quickly becomes apparent that, without additional security controls such as account lockout or rate limiting, TOTP codes are not particularly difficult for an attacker to brute-force. As such, it is crucial that those controls are implemented, and that there is appropriate logging and monitoring to allow suspicious activity like this to be detected.