Search code examples
python-3.xjwtpyjwt

How do I edit the JWT header in python 3.7?


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.


Solution

  • 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.