Skip to content
Field Detail
Platform PortSwigger Web Security Academy
Type Authentication — 2FA Brute-Force with CSRF-Bound Session Macro
Difficulty Expert
Objective Brute-force the 2FA code and access Carlos's account page
Note Victim credentials carlos:montoya. The verification code resets periodically during the attack, so it may need repeating if the new code happens to be one already attempted.

2FA Bypass Using a Brute-Force Attack

I logged in as carlos:montoya on /login and got prompted for a 4-digit security code.

Screenshot

Trying a couple of random guesses like 9999 or 6767 bounced me back to /login after two attempts.

Screenshot

Tracking the full flow in Burp revealed two separate CSRF-bound stages:

POST /login HTTP/2
Host: 0a2500db048a4055807e5db7001000cf.web-security-academy.net

csrf=Zg2vIVIJ2fzscMK2JihzkuN0hmE0q5c7&username=carlos&password=montoya

That leads to a GET /login2, then:

POST /login2 HTTP/2

csrf=i2Y1289dGVC8l5H78MfxOhsglWagwAK2&mfa-code=miku

Reusing the same csrf value on a second attempt returned "Invalid CSRF token (session does not contain a CSRF token)."

Screenshot

The csrf token rotates with every GET /login, and a second, separate csrf gets issued when /login2 is reached. Two separate, rotating CSRF tokens make naive brute-forcing of mfa-code impossible without re-deriving fresh tokens for every attempt. The full sequence needed per attempt is:

  1. GET /login — get a fresh csrf
  2. POST /login — submit credentials with that csrf
  3. GET /login2 — get the second-stage csrf
  4. POST /login2 — submit the brute-forced mfa-code with the second csrf

Step 4 is the only one Intruder needs to vary; steps 1-3 need to run automatically before every single attempt. Burp's session handling rules with a recorded macro handle exactly this.

In Settings -> Sessions -> Session Handling Rules, I added a rule named teto scoped to all URLs, then recorded a macro covering the three setup requests: GET /login, POST /login, GET /login2.

Screenshot
Screenshot

With the rule in place, I sent the POST /login2 request to Intruder. The macro replays steps 1-3 fresh before each request fires, so every mfa-code guess arrives with a valid, matching csrf. Payload set to numbers 0 to 9999, padded to 4 digits:

Screenshot

Resource pool set to 1 concurrent request to keep the macro-driven session state consistent between requests:

Screenshot

One request returned 302. I used "Show response in browser / request in browser, original session" to get a URL representing that successful state:

Screenshot

Pasting the link in the browser failed with an invalid session error.

Screenshot

The 2FA code resets periodically — by the time the link was loaded, the session/code state had already rotated. I reran the attack, got another 302, and this time copied the session cookie value directly from the successful response and set it manually in the browser instead of using the "request in browser" helper.

Screenshot
Screenshot

Lab solved :P

Dead Ends & Rabbit Holes

"Show response in browser → original session" produced a link that failed with an invalid session error on first use — the 2FA code and session state had already rotated by the time it was loaded. Taking the session cookie value straight from the successful 302 response and setting it manually in the browser worked reliably instead.

Resources