Attacking Authentication
Login is a promise the app must re-check on every request, and it usually slips at the reset flow, the OTP, or the session token.
Builds on
What you will learn
- -Spot username enumeration from different responses and from response timing
- -Tell credential stuffing apart from brute force and explain why rate limits matter
- -Find weak password reset and MFA flows, from guessable tokens to skippable second factors
- -Explain the common JWT and session mistakes and how a server verifies tokens correctly
One way past a login is to inject into the query behind it, the trick from the last lesson: a single quote, a comment marker, and the comparison never runs. That is a single door. has a whole hallway of them. The reset flow, the one-time code, the "remember me" , the an trusts on the next request. This lesson walks that hallway.
Authentication answers one question: who is this request from. The login is the moment the app decides. The session is the promise it carries forward, re-checked on every request after. An attacker who cannot force the moment often forges the promise instead, and most apps guard the moment far better than they guard the promise.
Username enumeration comes first
Guessing a password is far cheaper once the attacker knows the account exists. Apps leak that constantly. A login that answers "invalid password" for a real account and "invalid username" for a fake one is naming its users out loud. A reset form that answers "email sent" for known addresses and "no account found" for unknown ones does the same thing from a different page.
Timing leaks even when the text does not. A careful hashes the submitted password with bcrypt or Argon2, which is deliberately slow. If it only runs that hash when the username exists, real accounts take measurably longer to reject than fake ones. Sending one wrong password against a list of candidates and watching the response time split is enough to separate the real from the invented.
Registration forms give it away too. A signup that refuses a taken username confirms which names are already registered. The defense is boring and effective: one generic answer for every failure, and the same amount of work whether the account exists or not.
Brute force and credential stuffing are not the same
points many password guesses at one account. stuffing points one guess at many accounts, and that one guess is a real username-and-password pair stolen from some other site's breach. Stuffing works because people reuse passwords, and it slips past naive defenses because it never makes many attempts against any single account.
That distinction drives the defense. A per-account lockout after five bad tries stops brute force cold and does almost nothing to stuffing, which might try one password per account across a million accounts. Rate limiting has to consider the source IP and the whole login , not just the target username. A crude lockout also hands the attacker a denial-of-service lever and a fresh signal, because "this account is locked" is another way of saying "this account exists".
Good defenses . Slow, per-user raises the cost of every guess. Exponential backoff on repeated failures buys time. A challenge after a few failures blunts automation. Multi-factor turns a correct password into a first step rather than the finish line. Throttling should be gentle to real users and expensive to a script.
Guessing credentials against a nobody gave written permission to test is illegal and destructive. It locks out real people and floods with noise that on-call engineers have to chase. Every technique here belongs on a lab target, a CTF box, or an engagement with signed . Nowhere else.
A site locks any account after five failed passwords, yet attackers still take over thousands of accounts overnight. Which technique best explains it?
The password reset is a second login, usually weaker
Every reset flow is an alternate way to take over an account, and teams tend to build it in a hurry. The token is where it breaks. A reset link built from a sequential id, a timestamp, or a handful of random digits is guessable: the attacker requests a reset for the victim, then walks the token space until one works.
Expiry and reuse are the next failure. A token that never expires sits in an old inbox as a permanent skeleton key. A token that still works after the password already changed lets an attacker undo the victim's own reset. A single-use, short-lived, high- token tied to one account closes all three gaps.
poisoning is the subtle one. Many apps build the reset from the incoming Host header instead of a fixed configured . The attacker sends a reset request for the victim with the Host header pointed at an attacker-controlled domain. The mailer stamps that domain into the link, so the victim's email now carries a valid token pointing at the attacker's server. A click, or even an automated link-preview fetch by the mail , delivers the token straight to the attacker. The server should build that link from a base URL it , never from a header the client can set.
Multi-factor is only as strong as its enforcement
A second factor stops a stolen password, but only if the server actually requires it. Three bypasses show up again and again.
Skipping the step is the first. The password page to a code prompt at /, but the server marked the session fully authenticated at the password step, so requesting the real destination like /account directly sails through. The code prompt was decoration. The fix is to treat the session as half-authenticated until the second factor verifies, and to gate every protected route on the full state.
Brute-forcing the code is the second. A six-digit one-time code is a million possibilities, which sounds large and is not when the verify endpoint has no rate limit and no attempt cap. The code endpoint needs the same throttling and lockout as the password, plus a short validity window so the code expires before a script can walk the space.
Response tampering is the third. Some flows check the code in the browser, or the server returns a result the client is trusted to honor, something shaped like a success-or-failure flag. Intercepting the response and flipping failure to success walks right past a factor that was never enforced on the server. The rule underneath all three: the server decides, and it decides after the second factor, not before.
After a correct password, the app shows a one-time-code prompt at /mfa. An attacker requests /account directly and lands on the dashboard. What went wrong?
Sessions carry the promise forward
A perfect password check is wasted if the token it hands out is weak. The session token is worth inspecting on its own. A token that is a of the username is not a , it is a guess. Two session weaknesses cause most real takeovers.
Session fixation comes first. If the app accepts a the client already holds and keeps that same id after login, an attacker can plant a known id in the victim's browser, wait for the victim to authenticate, and then present the same id from the attacker's own browser as an authenticated session. The defense is to regenerate the session id at every privilege change, above all at login, so the pre-login value becomes worthless.
Tokens that never rotate are the second. A token that does not change on login can be captured once and replayed forever. Logout that only deletes the cookie in the browser while the server still honors the token is the same bug wearing a different hat: the promise outlives the moment it was supposed to end. Sessions should be short-lived, rotate on login, and be invalidated server-side on logout.
JWT is where trust goes to break
A Web Token is three base64url chunks split by dots: header, , . The payload is readable by anyone with a base64 decoder, no key required, so it is not a place for secrets. The signature is the only thing stopping an attacker from rewriting the , and developers break that check in a few predictable ways.
The alg-none bug is the classic. The header names the signing algorithm. A verifier that honors a header of "none" and an empty signature is checking nothing, so an edited payload with a claim like an admin role sails through. A correct verifier pins the algorithm on the server side and refuses "none" outright.
The weak-secret bug is the other common one. An HS256 token signed with a guessable secret can be cracked offline, where there is no rate limit and no lockout to slow the attempt down, just a wordlist against the token. A recovered secret lets an attacker mint any token they want.
$hashcat -m 16500 token.jwt rockyou.txtCorrect verification is the answer to both: pin the expected algorithm server-side, reject "none", verify the signature with a strong secret or the right , and honor the expiry claim so an old token stops working.
A JWT library accepts a token whose header says alg is none and whose signature is empty. Why is that exploitable?
Spin up DVWA (or use OWASP Juice Shop or the PortSwigger Web Security Academy) so every request lands on a target you control.
- Open the DVWA "Brute Force" module and set security to low. Watch how the page responds to a wrong password versus a right one, then note the exact difference an attacker would filter on.
- Point ffuf at the login and let it find the difference for you:
ffuf -w rockyou.txt -X POST -d 'username=admin&password=FUZZ' -u http://localhost/dvwa/vulnerabilities/brute/ -b 'PHPSESSID=YOUR_COOKIE; security=low' -fr 'incorrect'. The-fr incorrectfilter hides every failed login, so only a success surfaces. - Raise DVWA to "high" security and run the same command. The added token and delay should break your automation. Observe what changed in the responses.
- In Juice Shop, walk the forgot-password flow and read the security question logic, then find the JWT the app stores and decode its payload in your browser console with
atob(token.split('.')[1]). Confirm you can read the claims without any key.
Write down which defense would have stopped each step: generic error messages, rate limiting, a stronger reset flow, and correct token verification.
Authentication only proves who a request is from. It says nothing about what that is allowed to reach, and apps that second question wrong even more often than the first. A logged-in user changing an id in a URL to read someone else's invoice is the next lesson: access control and .