Manual de Burp Suite Community Edition
Capítulo 14 · Recuperación de contraseña y verificación
Serie: Burp Suite desde cero | unfantasmaenelsistema.com
Los capítulos anteriores fueron fallos puntuales -un campo mal validado, un objeto sin comprobar su dueño. Este capítulo encadena varios pasos pequeños de un mismo flujo -"olvidé mi contraseña"- hasta llegar a algo mucho más grave que cualquier hallazgo suelto: control total de una cuenta ajena, en tres peticiones HTTP y sin necesitar la contraseña original en ningún momento.
Qué aprenderás en este capítulo
Cómo funciona el flujo de "Forgot Password" de Juice Shop, paso a paso · Que el endpoint que revela la pregunta de seguridad no exige estar logueado, y que su respuesta ya delata si un email existe · Que sí hay un límite de intentos, pero es lo bastante generoso como para no proteger una respuesta débil · Que acertar la respuesta cambia la contraseña en una sola petición, sin pedir la contraseña antigua ni ningún paso más · Por qué preguntarse qué le falta a este flujo importa tanto como haber encontrado el fallo
"Forgot Password" pide tres cosas seguidas: el email, la respuesta a una pregunta de seguridad que aparece sola en cuanto el email es válido, y la contraseña nueva.

El formulario completo: email, pregunta de seguridad, contraseña nueva y su repetición

Al escribir el email de una cuenta real, la pregunta se rellena sola: "Your eldest siblings middle name?"
Esa pregunta se rellena sola porque el navegador hace, por su cuenta, un `GET /rest/user/security-question?email=...`. En Burp, esa petición sale con la cookie de sesión de quien esté navegando en ese momento -pero merece la pena probar qué pasa sin ella.

La petición real capturada: llega con Authorization, pero eso no significa que haga falta

La misma petición sin ninguna cabecera de sesión, contra un email que no existe: 200 OK con {} vacío
Autenticado o no, la respuesta ya dice demasiado
Un email real devuelve `{"question":{...}}`; uno inventado devuelve `{}` -ambos con el mismo código 200, pero con cuerpos que se distinguen a simple vista, y sin exigir sesión alguna. Es la misma clase de enumeración de usuarios de la que ya se habló en el capítulo 6, pero aquí no hace falta ni una cuenta propia para explotarla: junto con la lista completa de correos que el capítulo 13 demostró que se puede sacar de `/api/Users/`, cualquiera tiene ya un mapa completo de qué cuentas son reales y cuál es la pregunta exacta que hay que responder para cada una.
A diferencia de otros endpoints de este manual, este sí lleva algún tipo de límite -algo que conviene comprobar siempre en las cabeceras de la respuesta, no solo en el cuerpo:

401 con una respuesta incorrecta -pero fíjate en X-RateLimit-Limit: 100 y X-RateLimit-Remaining: 89
Un límite generoso no es lo mismo que ningún límite, pero tampoco es una defensa real
Cien intentos antes de bloquear es mucho mejor que la ausencia total de límite -hay que reconocerlo como lo que es, una protección real, no cosmética. Pero cien intentos siguen siendo de sobra para una respuesta sacada de un diccionario corto: el nombre de una mascota, una ciudad natal, el tipo de pregunta que mucha gente reutiliza en todas partes y que a menudo se puede adivinar o encontrar con una búsqueda rápida. Un límite de intentos protege contra la fuerza bruta ciega; no protege contra una respuesta débil combinada con un poco de OSINT.
Con la respuesta correcta, el mismo endpoint hace dos cosas a la vez -verificar la respuesta y fijar la contraseña nueva- sin pedir en ningún momento la contraseña antigua:

200 OK: la cuenta completa en la respuesta, incluido un detalle que no debería estar ahí -el hash MD5 de la contraseña nueva, en texto plano
Dos fallos en la misma respuesta
El primero es de diseño: cambiar una contraseña no debería depender solo de acertar una pregunta -es la única verificación de todo el proceso, sin un segundo canal (un email con un enlace de un solo uso, por ejemplo) que confirme que quien pide el cambio es realmente el dueño de esa cuenta. El segundo es más pequeño pero igual de revelador: la API nunca debería devolver un hash de contraseña en una respuesta, ni siquiera el de una contraseña que el propio cliente acaba de fijar.
Y el resultado es completamente real, no solo un 200 en Burp -la nueva contraseña abre la cuenta como cualquier otra:

Sesión iniciada con la contraseña impuesta por el atacante -la cuenta ha cambiado de manos
Un flujo de recuperación bien diseñado no debería confiar en un solo secreto que el usuario ya reveló en algún sitio -la gracia de un segundo factor (un correo con un enlace temporal, un código enviado por SMS, una app de autenticación) es que vive en un canal que un atacante remoto no controla solo por saber una respuesta. Aquí no hay ningún segundo canal: la pregunta de seguridad es el principio y el final de la verificación.
Un ejercicio para dejar planteado
Juice Shop también permite activar autenticación en dos pasos (TOTP) desde Privacy & Security. Vale la pena preguntarse -y probarlo con una cuenta propia- si este mismo flujo de recuperación pide ese segundo código cuando está activado, o si lo pasa por alto igual que pasa por alto la contraseña antigua. Un segundo factor que se puede esquivar reseteando la contraseña por otra vía dejó de ser, en la práctica, un segundo factor.
Tres peticiones, ninguna especialmente compleja por separado, que encadenadas llegan mucho más lejos que la suma de sus partes: una pregunta de seguridad expuesta sin autenticar que además delata qué cuentas existen, un límite de intentos generoso pero insuficiente contra una respuesta débil, y un cambio de contraseña que no exige ni la contraseña anterior ni ningún segundo canal de verificación.
El capítulo siguiente deja atrás el terreno de los formularios tradicionales y entra en el de las APIs REST y GraphQL que hay debajo de una SPA moderna -el tipo de superficie que ni siquiera aparece como un formulario en pantalla, y que solo Burp deja ver.
unfantasmaenelsistema.com | Seguridad, sistemas y todo lo que no debería funcionar (pero funciona)