100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Web Application Security
60 minintermediate

Practice — Breaking a Vulnerable JWT-Based Login System

What You'll Build

In this lab you will run a small login service that issues and verifies JSON Web Tokens insecurely, then exploit two classic flaws from Lesson 14: the 'none' algorithm bypass and a weak, brute-forceable signing secret. Finally you will harden the verifier so both attacks fail, proving the fix with tests.

Analogy🏏Cricket
💪 Think of it like fitness: Assessment day is when the training gets tested for real, you take the map of your programme and put each targeted lift under load to see which strengths hold and which weaknesses are genuine, recording every number. Just as the measured test turns a plan into proven results, using your attack-surface map to probe injection, authentication, and API authorization, and confirming each real flaw with captured evidence, turns recon into demonstrated findings. This reveals where knowledge becomes proof: you deliberately load each mapped weakness and record exactly what gives way.

The aim is to make JWT verification concrete. By forging a token that grants yourself admin rights, and by cracking a short secret offline, you will feel exactly why the verifier, not the token, must decide how trust is established. That intuition, once earned by breaking a system, is far stickier than reading the rule.

Analogy🏏Cricket
🏏 Think of it like cricket: A disciplined bowling attack does not spray deliveries everywhere and hope; guided by the scouting report, it targets each batter's flagged weakness, confirms the ball is doing what was planned, and records every dismissal for the analysts. Just as that planned, evidence-backed attack turns a dossier into wickets rather than wasted overs, working your test plan area by area, confirming each real flaw by safely triggering it, and documenting it rigorously turns recon into evidenced findings. This reveals the method: targeted, confirmed, and recorded beats spray-and-pray every time.

Prerequisites

  • Node.js 18+ and npm installed, plus a terminal for running commands.
  • Completion of the Module 3 reading on JWT vulnerabilities, whose 'none' and algorithm concepts you will exploit directly.
  • Basic comfort decoding base64 and reading a JWT's three dot-separated parts (header, payload, signature).
  • An understanding that these techniques are for this local lab or systems you are explicitly authorised to test only.

Setup & Project Structure

Create a minimal Express service with two endpoints: a login that issues a JWT after checking a hard-coded user, and a protected route that verifies the token insecurely by trusting its header. Deliberately configure the service with a short, guessable signing secret so you can later crack it. Keep the service local and inspect every token you receive.

Analogy🏏Cricket
📷 Think of it like photography: A professional does not trust memory on a shoot; every frame is captured with its settings, the aperture, the shutter, the exact scene, so the shot can be reproduced and proven later. Just as that per-frame metadata makes each image a self-contained, repeatable record, working your test plan area by area and capturing the exact request, the response that proves impact, the reproduction steps, and the affected endpoint makes each finding a self-contained record. This reveals the discipline: evidence recorded at the moment of capture is what makes a finding stand up in the report.
bash
# Scaffold the vulnerable JWT service
mkdir jwt-lab && cd jwt-lab
npm init -y
npm install express jsonwebtoken
# In app.js: /login issues a token signed with a WEAK secret,
# and /admin verifies WITHOUT pinning the algorithm (trusts header).
node app.js          # serves on http://localhost:3000

Step 1 — Foundation: Obtain and Decode a Token

Log in as a normal user and capture the JWT. Split it on its dots into header, payload, and signature, and base64-decode the first two to read them. Note the algorithm in the header and your role in the payload. This decoded view is your map: it shows exactly which fields you will tamper with and what a valid token looks like before you alter it.

Analogy🏏Cricket
💪 Think of it like fitness: Before changing your form, you film a lift and freeze the frame to read exactly where your knees, hips, and bar sit, because you cannot correct what you have not first seen clearly. Capturing the JWT and splitting it on its dots to base64-decode the header and payload is that freeze-frame: you read the algorithm in the header and your role in the payload, mapping precisely which fields you will later tamper with. Just as the clear reference frame shows what to adjust, the decoded token shows what to forge. This reveals the discipline behind the attack: read the current state exactly before you deliberately change it.
bash
# Log in and capture the token
TOKEN=$(curl -s http://localhost:3000/login -d 'user=alice&pass=alice123' \
        | sed 's/.*"token":"//; s/".*//')

# Decode header and payload (the first two dot-separated parts)
echo "$TOKEN" | cut -d. -f1 | base64 -d 2>/dev/null; echo
echo "$TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null; echo
# Note: header alg (e.g. HS256) and payload role (e.g. "user").

Step 2 — Core Logic: Forge with the 'none' Algorithm

Now attempt the none-algorithm bypass. Build a new token whose header sets alg to none, whose payload claims the admin role, and which has an empty signature. Send it to the protected route. If the verifier trusts the header's algorithm, it performs no signature check and grants admin access, confirming the flaw exactly as Lesson 14 described.

Analogy🏏Cricket
♟️ Think of it like chess: The sharpest players open with the simplest testing move to see whether the opponent knows the basic refutation before committing to anything deep. Your first probe is the same: build a token whose header sets alg to none, whose payload claims the admin role, and whose signature is empty, then send it to the protected route. If the verifier trusts the header's algorithm, it performs no signature check and hands you admin, exposing the most elementary flaw at once. Just as the simple opening reveals whether the defence knows its fundamentals, the 'none' token reveals whether the verifier does. This reveals a testing habit: try the cheapest, most damaging bypass first.
bash
# Forge a 'none' token: header alg=none, payload role=admin, empty sig
HEADER=$(printf '{"alg":"none","typ":"JWT"}' | base64 | tr -d '=' | tr '/+' '_-')
PAYLOAD=$(printf '{"user":"alice","role":"admin"}' | base64 | tr -d '=' | tr '/+' '_-')
FORGED="$HEADER.$PAYLOAD."          # note trailing dot = empty signature

curl -s http://localhost:3000/admin -H "Authorization: Bearer $FORGED"
# Vulnerable verifier: returns admin data. Hardened verifier: 401.

Step 3 — Integration & Enhancement: Crack the Weak Secret

Next, target the weak symmetric secret. Because the service signed with a short, guessable value, you can brute-force it offline against your captured token using a wordlist tool. Once recovered, you can mint a fully valid HS256 token with any claims you like, including admin, that passes even a signature check. This demonstrates why symmetric secrets must be long and random.

Analogy🏏Cricket
🍳 Think of it like cooking: If a pantry lock uses a three-digit combination, a determined guest simply tries every combination at leisure until it opens, no skill required, only patience. A short JWT signing secret is that weak combination: because the service signed with a guessable value, you brute-force it offline against your captured token with a wordlist tool. Once recovered, you can mint a fully valid HS256 token with any claims you like, admin included, that even passes a signature check. Just as a longer combination makes exhaustive guessing hopeless, a long random secret makes offline cracking infeasible. This reveals why secret length is decisive: entropy is what starves brute force.
bash
# Brute-force the weak HMAC secret offline (educational, local only)
# Using a JWT cracking tool against the captured token:
#   jwt-cracker "$TOKEN" -d 0123456789abcdefghijklmnopqrstuvwxyz -m 8
# or hashcat mode 16500 with a small wordlist.

# Once the secret is recovered (e.g. "secret123"), mint a valid token:
node -e 'console.log(require("jsonwebtoken").sign(
  {user:"alice",role:"admin"}, "secret123", {algorithm:"HS256"}))'
# This token passes a signature check — because the secret was guessable.

Step 4 — Testing & Verification: Harden the Verifier

Finally, fix the service and prove both attacks fail. Pin the expected algorithm on the verifier so the none token and any switched algorithm are rejected, and replace the weak secret with a long random one from the environment. Then re-run your forged none token and any minted token from the old secret; both must now return 401. Lock this in with automated tests.

Analogy🏏Cricket
💰 Think of it like finance: After a fraud, a bank does not merely apologise; it closes the exploited loophole, reissues credentials from a secure source, and installs monitoring that proves the old trick now bounces. Hardening the service is the same closure: pin the expected algorithm so the 'none' token and any switched algorithm are rejected, and replace the weak secret with a long random one drawn from the environment. Re-running your forged none token and any token minted from the old secret must now return 401, and automated tests lock the fix in place. This reveals the completion of the loop: a fix is only real once it is enforced and continuously proven.
javascript
// HARDENED verifier: pin algorithm, use a strong secret, validate claims
const SECRET = process.env.JWT_SECRET;        // long, random, from env
function requireAuth(req, res, next) {
  try {
    const token = req.get('Authorization')?.replace('Bearer ', '');
    req.claims = jwt.verify(token, SECRET, {
      algorithms: ['HS256'],                  // header 'none' now rejected
      issuer: 'jwt-lab', audience: 'jwt-lab'  // and claims validated
    });
    next();
  } catch { return res.status(401).end(); }   // any tampering -> 401
}

// verify.test.js — prove both exploits are closed
test('none-alg forged token is rejected', async () => {
  expect((await get('/admin', forgedNoneToken)).status).toBe(401);
});
test('token from the old weak secret is rejected', async () => {
  expect((await get('/admin', oldSecretToken)).status).toBe(401);
});

Warning: Run this lab only against your own local service. JWT cracking tools and forged tokens are safe on your machine but constitute unauthorised access if aimed at any system you do not own or have explicit written permission to test. Keep everything on localhost and never point these techniques at a live third-party application.

Extension Challenge: Extend the lab three ways. Implement the algorithm-confusion attack from Lesson 14 by switching an RS256 service to HS256 and signing with its public key, then confirm your pinned-algorithm fix blocks it. Add token expiry and prove a replayed expired token is rejected, tying into Lesson 16. Finally, add a refresh-token endpoint with rotation so a reused refresh token is detected and the session revoked.

  • JWT security rests entirely on verification, so forging a token that passes verification grants full impersonation — here, admin access.
  • The 'none' algorithm bypass works whenever the verifier trusts the header's alg field and performs no signature check on an unsigned token.
  • A short or guessable symmetric secret can be brute-forced offline from any captured token, after which fully valid tokens can be minted at will.
  • Pinning the exact expected algorithm on the verifier rejects both the 'none' token and any switched-algorithm confusion attack.
  • A long, random secret loaded from the environment, plus validated issuer, audience, and expiry claims, closes the weak-signing and replay gaps.
  • Offensive practice — forging and cracking first — reveals the verifier's real weaknesses more convincingly than reading the rules, then the fix is proven with tests.
Lesson 18 of 35
0% complete