JSON Web Tokens (JWTs)

JSON Web Token (JWT) is a format for transmitting cryptographically secure data. JWTs are typically used by web applications as a stateless session token.

in JWT-based authentication, the session token is replaced by a JWT containing user information. After verifying the token's signature, the server can retrieve all user info from the JWT claims sent by the user.

JWTs consist of three main parts: a header, a payload, and a signature, which are base64-encoded and separated by dot . characters:

<Base64_Header>.<Base64_Payload>.<Base64_Signature>
  • Header - contains metadata about the token itself, holding information that allows interpreting it such as the ecryption algorithm used to secure the JWT token

  • Payload - contains the actual data making up the token. This data comprises multiple standard (registered) claims or arbitrary, user-defined claims

  • Signature - computed based on the JWT's header, payload, and a secret signing key, using the algorithm specified in the header. The integrity of the JWT token is protected by the signature.


Useful Tools and Resources


Attacking Signature Verification

The signature protects data within the JWT's payload. Without knowing the JWT's secret key, it's impossible to manipulate the token without invalidating it.

However, there are some misconfigurations in web applications that lead to improper signature verification, enabling us to manipulate the data within a JWT's payload.

Basic Misconfigurations

Case 1 - JWT signature verification is not in place

The first easy misconfiguration is when the web application does not check the JWT's integrity. If the web application is misconfigured to accept JWTs without verifying their signature, we can manipulate our JWT to escalate privileges or change our user data.

Case 2 - Signing algorithm set to None

Setting a manipulated JWT's algorithm to none implies that the JWT does not contain a signature, and the web application should accept it without computing one, which sometimes allows bypassing signature verification checks.

To forge a JWT with the none algorithm, we must set the alg-claim in the JWT's header to none

Algorithm Confusion

Description

Algorithm confusion is a JWT attack that forces the web application to use a different algorithm to verify the JWT's signature than the one used to create it.

If a web application uses an asymmetric algorithm like RS256, it signs JWTs with a private key and verifies them using a public key. However, if an attacker crafts a JWT that claims to use a symmetric algorithm like HS256, the situation changes, as it uses the same key for both signing and verification.

If the application naively trusts the alg field in the token header, it will attempt to verify the token using HS256 with whatever key it already has, which in this case is the public key.

Because the public key is, by definition, public, an attacker can sign a forged HS256 token using that public key, and the application will incorrectly accept it as valid.

Performing the Attack

To execute the algorithm confusion attack, we need the public key used by the web application for signature verification, which you can get:

  • from the web application's certificate

  • from the web application's JKWS key set usually at https://example.com/.well-known/jwks.json

  • from the JWT itself

To gain the key from the JWT, get two valid JWTs from the web application (by logging in two times) and use rsa_sign2n

git clone https://github.com/silentsignal/rsa_sign2n
cd rsa_sign2n/standalone/
docker build . -t sig2n
docker run -it sig2n /bin/bash
python3 jwt_forgery.py <JWT1> <JWT2>

The tool may output multiple public key candidates based on the two JWTs you provided.

Additionally, the tool automatically creates symmetric JWTs signed with the computed public key in different formats, which you can use to test for an algorithm confusion vulnerability.

If a token is accepted, you have confirmed the algorithm confusion vulnerability exists.

To forge a new token with edited claims, use the public key saved by the tool to a local file named filename_x509.pem within the docker container in CyberChef JWT Sign. Set the signing algorithm to HS256 and paste the public key into the Private/Secret key field.


Cracking the JWT Secret

After gaining access to a valid JWT, it is possible to attempt to brute-force the signing secret to obtain it. JWT supports three symmetric algorithms based on potentially guessable secrets: HS256, HS384, and HS512. If your token uses one of those algorithms, you might be able to crack its secret key.

To crack a JWT you can either use hashcat or jwt_tool:

hashcat -m 16500 jwt.txt /path/to/wordlist.txt
python3 jwt_tool.py JWT -C -d /path/to/wordlist.txt

Some wordlists other than rockyou.txt to crack JWTs are:

After finding the JWT's secret key, you can forge a new one using Cyberchef JWT Sign or jwt_tool


Exploiting JWT Standard Claims

Several standard claims might be leveraged by an attacker to exploit JWTs

jwk claim

jwk contains information about the public key verification for asymmetric JWTs

If the web application is misconfigured to accept arbitrary keys provided in the jwk claim, you can forge a JWT, sign it with your private key, and then provide the corresponding public key in the jwk claim for the web application to verify the signature and accept the JWT.

To do that, first generate your keys using

openssl genpkey -algorithm RSA -out exploit_private.pem -pkeyopt rsa_keygen_bits:2048
openssl rsa -pubout -in exploit_private.pem -out exploit_public.pem

Then, you can manually sign the new JWT using Cyberchef, or use the following script to generate the JWT (note: edit your payload accordingly)

from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from jose import jwk
import jwt

# JWT Payload
jwt_payload = {'parameter1': 'value1', 'parameter2': 'value2'}

# convert PEM to JWK
with open('exploit_public.pem', 'rb') as f:
    public_key_pem = f.read()
public_key = serialization.load_pem_public_key(public_key_pem, backend=default_backend())
jwk_key = jwk.construct(public_key, algorithm='RS256')
jwk_dict = jwk_key.to_dict()

# forge JWT
with open('exploit_private.pem', 'rb') as f:
    private_key_pem = f.read()
token = jwt.encode(jwt_payload, private_key_pem, algorithm='RS256', headers={'jwk': jwk_dict})

print(token)

Then run:

pip3 install pyjwt cryptography python-jose
python3 exploit.py

jku claim

jku is similar to the jwk claim: it holds a URL that serves the key details rather than holding them directly.

When a web application does not correctly check this claim, it can be exploited using a nearly identical process to the jwk claim: instead of embedding the key details into it, the attacker hosts the key details on their web server and sets the JWT's jku claim to the corresponding URL.

Also, the jwk claim can be exploited for blind GET based SSRF attacks!


kid claim

The kid claim tells the server which public key to use to verify the JWT's signature by specifying the key's identifier. The web application will then look for a matching key in its key store.

Depending on how the server manages the value of the kid and checks for a corresponding key, injection vulnerabilities such as SQLi or Path traversal can occur.

If the kid allows a path traversal vulnerability, it is possible to arbitrarily edit a JWT by making the kid point to a file whose contents are known, then sign the JWT with a symmetric key whose value corresponds to the contents of this file.

The easiest idea is to redirect the kid claim's value to the /dev/null file: since this file is empty, the attacker can create a symmetric key whose value is an empty character string. Since the kid points to an empty value, the attacker can modify the JWT as he wishes and sign it with his empty symmetric key. Since the /dev/null file (and therefore the symmetric key) has an empty value, the JWT’s signature will be valid.