Authorization disclaimer. This walkthrough applies only to the intentionally vulnerable PortSwigger Web Security Academy lab linked below. Do not attempt these techniques against systems you do not own or have explicit permission to test.

Overview

The lab’s two-step login flow does not securely bind the second-factor check to the account authenticated during the password step. Instead, /login2 uses a client-controlled verify cookie to decide whose code is being checked. Because the code contains four digits and the lab does not rate-limit attempts, changing that cookie to carlos makes it possible to test the complete code space for that synthetic account.

The `POST /login2` broken-logic brief supplies the credentials for Wiener, names Carlos as the target, and provides email access for observing the normal verification flow.
The `POST /login2` broken-logic brief supplies the credentials for Wiener, names Carlos as the target, and provides email access for observing the normal verification flow.

Lab objective/setup

The objective is to access the account belonging to carlos without knowing his password. The provided account wiener is useful for observing the normal authentication sequence: password submission, receipt of a verification code by email, and code submission to POST /login2.

What I observed

After signing in as wiener, the lab delivered a one-time code to the provided email client.

The email client shows the four-digit security code issued for Wiener's login attempt.
The email client shows the four-digit security code issued for Wiener's login attempt.

The response from the first login step set verify=wiener in a cookie.

The first login response redirects to `/login2` and sets `verify=wiener`, placing the account under verification in a client-controlled cookie.
The first login response redirects to `/login2` and sets `verify=wiener`, placing the account under verification in a client-controlled cookie.

The subsequent /login2 request sent that same cookie alongside the verification code, showing that the second step relied on it to identify the account under verification.

A request with `verify=carlos` receives the `/login2` code-entry page instead of being rejected for lacking Carlos's password step.
A request with `verify=carlos` receives the `/login2` code-entry page instead of being rejected for lacking Carlos's password step.

Changing the cookie to verify=carlos caused the application to present the code-entry flow for Carlos rather than rejecting the inconsistent authentication state.

The enlarged email message shows Wiener's four-digit code, confirming the format and limited code space used by the challenge.
The enlarged email message shows Wiener's four-digit code, confirming the format and limited code space used by the challenge.
The modified `POST /login2` request sends `verify=carlos` with `mfa-code=1140`; the response renders “Incorrect security code,” proving attempts reach Carlos's challenge.
The modified `POST /login2` request sends `verify=carlos` with `mfa-code=1140`; the response renders “Incorrect security code,” proving attempts reach Carlos's challenge.

Why it is vulnerable

Authentication state should be maintained server-side and tied to the session that successfully completed the password step. Here, the application trusts a mutable cookie to choose the account for the second factor. The weakness is compounded by a four-digit code and the absence of effective attempt throttling on /login2, leaving only 10,000 possible values.

Walkthrough in the authorized PortSwigger lab

  1. Complete the normal login flow for wiener and inspect the requests in Burp Suite.
  2. Send the POST /login2 request to Intruder and change the cookie to verify=carlos.
  3. Mark the four-digit mfa-code value as the payload position and test values from 0000 through 9999 within this lab only.
  4. Compare response status and length to identify the response that differs from failed attempts.
Intruder results contain one `302` response for code `0293`, with a response length distinct from the failed four-digit attempts.
Intruder results contain one `302` response for code `0293`, with a response length distinct from the failed four-digit attempts.

In the captured lab session, the distinct response corresponded to code 0293. Using that result opened Carlos’s account.

Verification

The browser displayed Carlos’s account page and the Academy marked the lab as solved. This verifies both parts of the flaw: the altered cookie selected another account, and repeated code attempts were accepted until the correct value was supplied.

The solved banner appears above Carlos's My Account page after the successful code response is opened in the browser.
The solved banner appears above Carlos's My Account page after the successful code response is opened in the browser.

Impact and defensive lesson

Bind every MFA challenge to a server-side, short-lived authentication transaction created only after the correct password is supplied. Never use a client-provided username as the authority for selecting the challenged account. Codes should expire promptly, be single-use, and be protected by per-account and per-session attempt limits, progressive delays, monitoring, and safe recovery controls.

References

Migration provenance

This article was rewritten in English from the private migration source declared in the front matter. The reasoning and eight screenshots are preserved; the prose, structure, captions, and accessibility text were normalized without adding observations.