Enumeracion y explotacion de Json Web Tokens (JWT)

Enumeracion y explotacion de Json Web Tokens (JWT)

🔐 Enumeración y explotación de JSON Web Tokens (JWT)

Los JSON Web Tokens (JWT) son el mecanismo más habitual hoy en día para gestionar autenticación y autorización en aplicaciones web. Es un estándar abierto (RFC 7519) que define un formato compacto para transmitir información firmada entre cliente y servidor. El problema es que muchas implementaciones confían ciegamente en el contenido del token sin validar correctamente su firma, y ahí es donde un atacante puede entrar. En este post vamos a ver, con dos laboratorios prácticos, cómo enumerar la estructura de un JWT y explotar dos fallos clásicos: la aceptación del algoritmo none y el uso de secretos débiles.

🔍 Escenario
Vamos a trabajar sobre dos laboratorios del proyecto SKF Labs, ambos aplicaciones Node.js con un login que acepta usuario y contraseña. El objetivo en los dos casos es el mismo: autenticarnos como un usuario normal (user:user), capturar el JWT que nos genera la aplicación, y manipularlo para escalar privilegios hasta el usuario admin, que es la cuenta «mortal» (objetivo) del laboratorio.

💡 Paso 1 — Levantar el laboratorio JWT-null
Nos vamos a la ruta del laboratorio:

/skf-labs/nodejs/JWT-null

Instalamos dependencias y arrancamos la aplicación:

npm install
npm start

Accedemos a localhost:5000, donde encontramos un login con dos usuarios sobre los que podemos insertar usuario y contraseña. Nos autenticamos como user:user y, tras el login, la interfaz muestra un botón de admin que, si lo pulsamos siendo un usuario normal, nos deniega el acceso.

💡 Paso 2 — Localizar el token
El JWT que nos entrega la aplicación al autenticarnos se guarda en el localStorage del navegador. Ahí es donde debemos ir a copiarlo para poder analizarlo y manipularlo.

💡 Paso 3 — Analizar la estructura del JWT
Con el token en la mano, lo pegamos en jsonwebtoken.io para decodificarlo. En la cabecera (header) encontramos algo como esto:

{
  "alg": "HS256",
  "typ": "JWT"
}

El campo alg indica el algoritmo de firma. Aquí está la clave del ataque: hay implementaciones de librerías JWT que, si el campo alg viene establecido como none, aceptan el token sin verificar ninguna firma. Si esto ocurre, ya no necesitamos conocer el secreto de firma del servidor: simplemente construimos nuestro propio token a mano.

💡 Paso 4 — Forjar un token con alg: none
Un JWT se compone de tres partes separadas por puntos: header.payload.signature, cada una codificada en Base64. Si el algoritmo puede ser none, la parte de la firma se puede omitir directamente.

Construimos la cabecera:

echo -n '{"alg":"NONE","typ":"JWT"}' | base64

Y el payload, indicando el id de usuario que queremos suplantar (en este caso el id:2, correspondiente a un rol con más privilegios) y las fechas de emisión y expiración:

echo -n '{"id":2,"iat":fechacreacion,"exp":fechaexpiracion}' | base64

Con la cabecera y el payload codificados, el token final queda así:

header.payload.

Ojo con el detalle importante: después del payload hay que dejar el punto final, aunque no haya firma. Ese punto marca la tercera parte del JWT (vacía), y muchas implementaciones vulnerables la aceptan igualmente porque ni siquiera comprueban que la firma esté presente cuando alg es none.

Sustituimos el token del localStorage por este que hemos forjado y volvemos a pulsar el botón de admin. Si el laboratorio es vulnerable, la aplicación nos reconoce como el usuario con id:2 sin haber necesitado ningún secreto.

🔍 Escenario 2 — Secretos débiles (JWT-secret)
El segundo laboratorio se encuentra en:

/skf-labs/nodejs/JWT-secret

Repetimos la instalación:

npm install

Al acceder a la aplicación, esta vez el ataque se llama weak secrets (secretos débiles). Aquí el algoritmo sigue siendo válido (por ejemplo HS256), pero el servidor firma los tokens con una clave secreta tan corta o predecible que se puede romper por fuerza bruta o diccionario con herramientas como hashcat o john. Una vez recuperado el secreto, podemos firmar nosotros mismos un token modificado (por ejemplo, cambiando el id a un usuario admin) y la aplicación lo aceptará como legítimo porque la firma cuadra. Repetimos aquí las mismas pruebas que en el laboratorio anterior: capturar el token del localStorage, decodificarlo en jsonwebtoken.io, y una vez obtenido el secreto, generar un token válido con los datos que nos interesen.

💡 Generando claves para RS256
Si en lugar de un algoritmo simétrico (HS256) nos encontramos un token cuya cabecera indica:

"alg":"rs256"

estamos ante un algoritmo asimétrico, y si necesitamos generar nuestro propio par de claves para pruebas, podemos hacerlo con:

openssl genrsa -out priv.Key.key 2048

🔑 Tipos de algoritmos de firma en JWT
Es importante conocer qué tipo de clave exige cada algoritmo, porque de eso depende si el ataque viable es fuerza bruta sobre un secreto compartido, o abuso de configuración con claves públicas/privadas.

1. HS256 (HMAC con SHA-256) — clave compartida (simétrica). Se genera así:

openssl rand -base64 32

Esto genera una clave compartida de 256 bits (32 bytes) en formato Base64. Es el algoritmo más habitual para ataques de secreto débil, ya que servidor y cliente usan la misma clave para firmar y verificar.

2. RS256 (RSA con SHA-256) — par de claves RSA (privada para firmar, pública para verificar). Clave privada:

openssl genpkey -algorithm RSA -out private_key.pem -aes256

Esto genera una clave privada RSA protegida con cifrado AES de 256 bits, guardada en private_key.pem. Clave pública a partir de la privada:

openssl rsa -in private_key.pem -outform PEM -pubout -out public_key.pem

Esto genera la clave pública RSA correspondiente, guardada en public_key.pem. Un fallo típico de configuración aquí es el conocido ataque de confusión de algoritmo (RS256 → HS256), donde el servidor usa la clave pública como si fuera un secreto HMAC.

3. ES256 (ECDSA con SHA-256) — par de claves de curva elíptica. Clave privada:

openssl ecparam -name prime256v1 -genkey -noout -out private_key.pem

Esto genera una clave privada de curva elíptica utilizando la curva prime256v1, guardada en private_key.pem. Clave pública a partir de la privada:

openssl ec -in private_key.pem -pubout -out public_key.pem

Esto genera la clave pública correspondiente.

4. PS256 (RSA-PSS con SHA-256) — par de claves RSA (privada para firmar, pública para verificar). La generación de la clave privada es similar a la de RS256 mencionada anteriormente.

🚫 ¿Qué está pasando realmente?
La fase de enumeración de un JWT consiste en obtener información sobre cómo la aplicación construye y valida sus tokens: qué algoritmo usa, si acepta alg: none, si el secreto es adivinable, qué campos lleva el payload (roles, IDs, permisos). Un atacante puede ir probando tokens falsos —construidos a mano o mediante fuerza bruta del secreto— y observando en todo momento si la aplicación los acepta o los rechaza. Si tiene éxito, puede extraer información sensible: nombres de usuario, roles, o datos de sesión.

La explotación llega cuando esa información se usa para saltarse los controles de autenticación o autorización: si la aplicación no valida correctamente la firma del JWT (como en el caso de alg: none) o usa un secreto débil, un atacante puede falsificar un token y hacerse pasar por cualquier usuario, incluido un administrador, sin conocer sus credenciales reales.

✅ Buenas prácticas
Para evitar este tipo de fallos: nunca aceptar alg: none en la validación del lado del servidor, forzar explícitamente el algoritmo esperado al verificar el token (en lugar de leerlo del propio JWT), usar secretos largos y aleatorios de al menos 256 bits para HS256, rotar las claves de firma periódicamente, y limitar siempre el tiempo de expiración de los tokens para reducir la ventana de explotación si uno se ve comprometido.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio