I'm trying to write a program which will try to brute-force the secret used for signing the signature in a JWT token via a words-list.
The problem is that whenever I generate a token using PyJWT, the header (after base64 decoding) is: {"typ":"JWT","alg":"HS512"}
but most JWT tokens that I'm trying to crack have the following header: {"alg":"HS512","typ":"JWT"}
token = jwt.encode({'some': 'payload'}, 'secret', algorithm='HS512'}
This is the token I get:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzb21lIjoicGF5bG9hZCJ9.EgMnzcJYrElON09Bw_OwaqR_Z7Cq30n7cgTZGJqtK1YHfG1cGnGJoJGwOLj6AWg9taOyJN3Dnqd9NXeTCjTCwA
As can be expected, the hashed signature is going to be different and my program won't work properly, I know it is possible to add in more data in the header but not how to flip between the "typ" and "alg".
Any help will appreciated, preferably, I would like to stay with python and not change to a different programming language.
If you are going to brute-force JWT (which will be a huge undertaking, good luck with that) then just generate the signature yourself, directly, from the first two parts. Python dictionaries and JSON objects are unordered structures so either order is valid, the JWT specs do not specify an order, and any JWT implementation just takes the existing data for the first two parts to verify the signature. They would not re-generate the JSON.
The PyJWT library provides all the supported algorithms as separate objects in the jwt.algorithms
module; just call jwt.algorithms.get_default_algorithms()
to get a dictionary mapping name to Algorithm
instance.
Each such object has .sign(msg, key)
and .verify(msg, key, sig)
methods. Pass in the first two segments (base64-encoded, with .
, as a bytes
object) as the message, and you'll get the binary signature (not base64 encoded) back when using .sign()
, or when verifying with .verify()
, you pass in the binary signature decoded from the base64 data.
So, for a given token
as a bytes
object, you can get the algorithm and verify a key with:
import json
from jwt.utils import base64url_decode
from jwt.algorithms import get_default_algorithms
algorithms = get_default_algorithms()
msg, _, signature_part = token.rpartition(b'.')
header = json.loads(base64url_decode(msg.partition(b'.')[0]))
algo = algorithms[header['alg']]
signature = base64url_decode(signature_part)
# bytes key from other source; brute-force or otherwise
if algo.verify(msg, key, signature):
# key correct
Given your sample token
and key
set to b'secret'
, the above validates:
>>> import json
>>> from jwt.utils import base64url_decode
>>> from jwt.algorithms import get_default_algorithms
>>> token = b'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzb21lIjoicGF5bG9hZCJ9.EgMnzcJYrElON09Bw_OwaqR_Z7Cq30n7cgTZGJqtK1YHfG1cGnGJoJGwOLj6AWg9taOyJN3Dnqd9NXeTCjTCwA'
>>> key = b'secret'
>>> algorithms = get_default_algorithms()
>>> msg, _, signature_part = token.rpartition(b'.')
>>> header = json.loads(base64url_decode(msg.partition(b'.')[0]))
>>> algo = algorithms[header['alg']]
>>> signature = base64url_decode(signature_part)
>>> algo.verify(msg, key, signature)
True
Brute-forcing by generating the key in a loop is then trivial to verify. Note that anything beyond a small key (using a limited alphabet) is very rapidly going to be unfeasible; a 16-byte fully random key value (128 bits) would take you a few decades to brute-force on modern hardware even with a system programming language, let alone the slower speed of a Python loop.