How to use and defend against MFA Fatigue Attacks
WRITTEN BY JULIK KEIJER (BLUE TEAM) AND BART ROOS (RED TEAM)
Multi Factor Authentication (MFA) is a method often used in cyber security to verify the legitimacy of a user login. In Multi-factor authentication more than one device is needed to access the account. This decreases the chances of a malicious party logging in with stolen, guessed or leaked credentials. Often, this extra authentication step is done through filling out a 6-digit code.
Though MFA lowers chances of malicious parties logging in, it must be used in the correct way to actually work.
Three years ago, one of Northwave’s ethical hackers was working on an adversary simulation. In this simulated attack, common attack techniques are used to test the security of a company. The ethical hacker of the Red Team sent out phishing e-mails to try and gain a valid username and password to get access to the remote workplace of the customer. As with most phishing attacks, this one was also successful. Using the obtained username and passwords, the ethical hacker was able to get access to the remote workplace of the organisation and continue his attack from there.
Strangely, the ethical hacker noticed that the attempts at logging in to the remote workplace did not always work. Sometimes he could log in immediately, sometimes with a slight delay and sometimes after multiple times of using the same credentials. At the end of the project, we discussed these findings with the customer and discovered that the organisation implemented Multi Factor Authentication (MFA) that made use of push notifications. At the time, this MFA method was relatively new, and users did not understand the effect of pushing the “Approve” button. It turned out they were approving our malicious logins, sometimes immediately and sometimes after they received a couple push notifications.
What is an MFA Fatigue Attack?
Nowadays, using push notifications is a common way to enable Multi Factor Authentication. It is probably one of the most used options in Microsoft based environments where Microsoft Authenticator is being used. Accepting a push notification is also considered more user friendly compared to manually entering a time based 6-digit authentication code.
However, there is a hidden risk involved in using push notifications. When an attacker has obtained a valid username and password, several push notification can be triggers on the victim’s phone. This could result in a so called MFA Fatigue attack. In an MFA fatigue attack, the attacker will continue triggering push notifications on the victim’s phone until the user is fed up with all the notifications. Hoping to stop the annoying notifications, the user eventually pushes the “Approve” button. This then gives the attacker access to the victim’s systems. An MFA fatigue attack is also how the Lapsus$ group hacked Uber and Microsoft 
Educating users on how to respond when receiving an unexpected MFA notification is a good preventive measure, but that doesn’t guarantee that the MFA fatigue attack will fail. The good news is that another layer of protection against MFA fatigue attack is possible. This is done by detecting successful attacks.
Detecting MFA Fatigue Attacks
Triggered by the behavioural bypass of the MFA technology, the Northwave Security Operations Center (SOC) researched the artefacts generated during an MFA fatigue attack. What chain of events is visible in a successful MFA fatigue attack? Imagine that the attacker has a valid username and password. In this case, the first event would be a successful password authentication, so we can’t rely on checking on failed authentications. From the attack artefacts we can see that users often ignore or deny the first few MFA notifications. We therefore look for failed authentications where the MFA request is ignored or denied.
The problem is that an MFA request being ignored happens very often in a normal organisation, or someone is simply too late to respond. Therefore, we must check the reason it failed. There are several reasons why an MFA request would fail. For example, because the MFA device could not be reached to serve the request, or the server was not reachable for a response. These are not events we are interested in, so they can be filtered out.
Even after filtering out irrelevant events, there are still too many to investigate. To further reduce our set of events we can disregard scenarios that we deem safe. For example, when a user logs in from the same location, an onboarded device and IP address every day, but one day ignores the MFA request, we can assume that it was simply forgotten. By disregarding login attempts from known locations, IP addresses and devices we reduce our events even more.
But what if an attacker tries to evade one of these measures?
In the previous section, our goal was to reduce the number of login events to the point where we can analyse them for malicious attempts. Once our set is down to a manageable size, we can expand it with more interesting events. As described earlier in this blog, it might happen that a user correctly ignores or denies an MFA request and the attacker tries multiple times to login, then that is something we would like to see regardless of other factors.
By combining multiple methods, we can detect a broader range of attacks. The first method lets us detect single attempts with unfamiliar features by filtering out all attempts where the features are familiar (a known location, an onboarded device and IP address). The second method lets us detect attacks, even though they have familiar features, by alerting on multiple failed attempts.
Detecting MFA Fatigue Attacks using Azure Active Directory sign-in logs
One of the log sources that the Northwave SOC monitors are Azure Active Directory sign-in logs. This section uses these logs to give an impression of the practical implementation of MFA Fatigue Attack detection.
Any authentication attempt is logged in the Azure Active Directory SigninLogs table. Failed MFA requests can be recognised by the ResultType field (Table 1), where Result Type 50074 indicates a failed MFA request.
|500121||MFA explicitly denied or ignored|
Table 1: Azure Active Directory Result Types 
In the earlier section we described that we are not looking for general error messages and only want to see events where the MFA is explicitly denied or ignored. Therefore, we look at Result Type 500121.
SigninLogs | where AuthenticationRequirement == "multiFactorAuthentication" | where ResultType == 500121 | extend MFA_denied = Status has "user declined the authentication" | extend MFA_ignored = Status has "user did not respond to mobile app notification" | where MFA_denied or MFA_ignored
In order to reduce the number of events that we could alert on, we would like to exclude previous locations and IP addresses. This can be done by filling our table previous_ips with all combinations of users and IP addresses and our table previous_locations with all combinations of users and locations. The tables are then joined with the failed MFA requests to enrich the information.
Let lookback = 1h; let previous_ips = SigninLogs | where TimeGenerated between(ago(14d) .. ago(lookback)) | extend PreviousIp = true | distinct IPAddress, UserPrincipalName, PreviousIp; let previous_locations = SigninLogs | where TimeGenerated between(ago(14d) .. ago(lookback)) | extend LocationDetails = todynamic(LocationDetails) | extend City = LocationDetails.city, Country = LocationDetails.countryOrRegion | extend PreviousLocation = true | distinct tostring(City), tostring(Country), UserPrincipalName, PreviousLocation; SigninLogs | where TimeGenerated between(ago(lookback) .. now()) …. | join kind=leftouter previous_ips on IPAddress, UserPrincipalName | join kind=leftouter previous_locations on City, Country, UserPrincipalName
Since we do not want to disregard all logins with a previous location or IP address, we keep track of failed MFA attempts that have a precious location or IP address with the PreviousLocation and PreviousIP variables. Then we can confirm that the failed MFA attempts are not coming from a previous location or IP address, or that there are more than 10 MFA attempts.
Let MFA_threshold = 10; SigninLogs ... | where not(PreviousLocation or PreviousIP) or MFA_count >= MFA_threshold
The entire rule can be found as an attachment to this blog.
Northwave approaches her Intelligent Detection and Response Service (IDRS) in a risk-based fashion. This means we ensure that the detections we generate, result from an organisation’s risk analysis, which incorporates our practical knowledge of threats and risks from our Red Team, Blue Team and CERT. In this way, we make sure that the risk analysis, and therefore the monitoring, is based on real threats that are relevant to the target organisation.
We use the MITRE ATT&CK Framework  to map a series of these attack techniques against our detection capabilities, functioning as a coverage matrix. Opportunities to cover gaps in this matrix will be prioritised and new detection rules are developed and put in place. As such, findings from our Red Team about MFA fatigue attacks ended up in a detection rule that is present at all our IDRS customers, which ultimately reduces the time it takes to uncover an ongoing attack.
name: MFA fatigue attack detected. description: | 'This query looks for failed MFA actions with suspicious attributes and MFA fatigue attacks' requiredDataConnectors: - connectorId: AzureActiveDirectory dataTypes: - SigninLogs tactics: - CredentialAccess relevantTechniques: - T1110 query: | let MFA_threshold = 10; let lookback = 1h; let previous_ips = SigninLogs | where TimeGenerated between(ago(14d) .. ago(lookback)) | extend PreviousIP = true | distinct IPAddress, UserPrincipalName, PreviousIP; let previous_locations = SigninLogs | where TimeGenerated between(ago(14d) .. ago(lookback)) | extend LocationDetails = todynamic(LocationDetails) | extend City = LocationDetails.city, Country = LocationDetails.countryOrRegion | extend PreviousLocation = true | distinct tostring(City), tostring(Country), UserPrincipalName, PreviousLocation; SigninLogs | where AuthenticationRequirement == "multiFactorAuthentication" | where TimeGenerated between(ago(lookback) .. now()) | where ResultType in (500121, 50074) | extend MFA_denied = Status has "user declined the authentication" | extend MFA_ignored = Status has "user did not respond to mobile app notification" | extend City = tostring(LocationDetails.city), Country = tostring(LocationDetails.countryOrRegion) // Filter out all recoreds with IPs or locations from which this user logged in. | join kind=leftouter previous_ips on IPAddress, UserPrincipalName | join kind=leftouter previous_locations on City, Country, UserPrincipalName | summarize TimeStamps=make_set(TimeGenerated), IPAddresses=make_set(IPAddress), Locations=make_set(LocationDetails), Apps=make_set(AppDisplayName), MFA_denied=max(MFA_denied), MFA_ignored=max(MFA_ignored), PreviousLocation=max(PreviousLocation), PreviousIP=max(PreviousIP), MFA_count=count() by UserPrincipalName | where ((MFA_denied or MFA_ignored) and not(PreviousLocation or PreviousIP)) or MFA_count >= MFA_threshold entityMappings: - entityType: Account fieldMappings: - identifier: FullName columnName: UserPrincipalName version: 1.0.0 kind: Scheduled