Tu contraseña probablemente no es tan segura como crees. Cada año, bases de datos con miles de millones de credenciales terminan en manos de atacantes, y las herramientas para descifrarlas son más rápidas que nunca.
En este artículo vas a entender exactamente cómo funcionan los ataques más comunes contra contraseñas, por qué los algoritmos clásicos ya no sirven, y qué defensas recomienda la industria hoy. Con ejemplos de código que puedes ejecutar tú mismo.
¿Cuánto tarda en descifrarse tu contraseña?
Antes de entrar en los métodos técnicos, mira esta tabla. Está basada en los benchmarks de Hive Systems (2024), que asumen un ataque de fuerza bruta con 12 GPUs RTX 4090 contra hashes bcrypt (32 iteraciones, cost factor 5). Con el cost factor recomendado de 12, los tiempos serían aproximadamente 128 veces mayores.
| Longitud | Solo números | Minúsculas | Mayúsc. + Minúsc. | + Números + Símbolos |
|---|---|---|---|---|
| 4 chars | Instantáneo | Instantáneo | Instantáneo | Instantáneo |
| 6 chars | Instantáneo | Instantáneo | Instantáneo | 4 segundos |
| 8 chars | Instantáneo | 22 horas | 8 meses | 12 años |
| 10 chars | 2 horas | 15 años | 41 mil años | 37 millones de años |
| 12 chars | 9 días | 10 mil años | mil millones de años | 2 billones de años |
| 16 chars | 202 años | 7 billones de años | — | — |
Lo que esta tabla muestra es simple: la longitud importa más que la complejidad. Una contraseña de 12 caracteres con minúsculas supera a una de 8 con todos los tipos de caracteres.
Cómo se almacenan las contraseñas — Qué es el hashing
Ningún servicio serio guarda tu contraseña en texto plano. En lugar de eso, aplican una función hash: una operación matemática de un solo sentido que convierte la contraseña en una cadena de longitud fija.
import hashlib
password = "MiContraseña123"
hash_result = hashlib.sha256(password.encode()).hexdigest()
print(hash_result)
# ejemplo: ejecuta este código para ver el hash real de tu entrada
El resultado siempre tiene la misma longitud (64 caracteres hexadecimales para SHA-256), sin importar si la entrada es "a" o un párrafo entero. Y lo más importante: no puedes revertir el hash para obtener la contraseña original.
Entonces, ¿cómo verifican tu contraseña al hacer login? El servidor calcula el hash de lo que escribes y lo compara con el hash almacenado. Si coinciden, entras.
Cuando el hashing no se usa
En mayo de 2025, se descubrió una base de datos con 184 millones de credenciales en texto plano — emails y contraseñas sin ningún tipo de hashing. Servicios de Apple, Google, Facebook, Instagram y muchos otros estaban incluidos. El investigador de seguridad Jeremiah Fowler la descubrió expuesta en un servidor Elasticsearch sin autenticación.
Esto demuestra que, aunque el hashing es la práctica estándar, hay servicios que simplemente ignoran la seguridad más básica.
Método de ataque 1: Fuerza bruta y ataques de diccionario
Fuerza bruta
El concepto es simple: probar todas las combinaciones posibles hasta encontrar la correcta.
Objetivo: hash("????")
Intentar: hash("aaaa") → ¿coincide? No
Intentar: hash("aaab") → ¿coincide? No
Intentar: hash("aaac") → ¿coincide? No
...
Intentar: hash("z9$!") → ¿coincide? Sí → contraseña encontrada
Para una contraseña de 8 caracteres con letras minúsculas (26 posibilidades por posición), hay 26^8 = 208 mil millones de combinaciones. Parece mucho, pero una GPU moderna procesa miles de millones de hashes por segundo.
Ataques de diccionario
En la práctica, nadie usa fuerza bruta pura contra contraseñas largas. Los ataques de diccionario son mucho más eficientes porque la mayoría de la gente elige contraseñas predecibles.
Lista: ["password", "123456", "qwerty", "iloveyou", ...]
Reglas de mutación:
- password → Password, PASSWORD, p@ssw0rd, password1, password!
- dragon → Dragon123, dr@gon, DRAGON!
Para cada palabra en la lista:
Para cada mutación:
hash(mutación) → ¿coincide con el objetivo? → encontrada
Herramientas como Hashcat y John the Ripper vienen con listas de millones de contraseñas filtradas y reglas de mutación sofisticadas. La lista RockYou2024 contiene casi 10 mil millones de contraseñas reales.
Credential stuffing
Si tu contraseña fue filtrada en un servicio, los atacantes la prueban automáticamente en cientos de otros servicios. Este ataque se llama credential stuffing y funciona porque el 65% de las personas reutiliza contraseñas en múltiples sitios.
No requiere descifrar ningún hash. Solo necesitan la lista de email + contraseña y un script que pruebe combinaciones en servicios populares.
Herramientas reales de ataque
Las dos herramientas más usadas para descifrar hashes son:
- Hashcat -- aceleración por GPU, soporta más de 350 tipos de hash. Es la herramienta estándar para auditorías de seguridad y pentesting profesional.
- John the Ripper -- más antiguo, funciona bien en CPU. Tiene una versión community (open source) y una versión pro con reglas de mutación adicionales.
Ambas son legales y de código abierto. Los equipos de seguridad las usan para auditar la fortaleza de las contraseñas de su propia organización. El problema es que los atacantes usan exactamente las mismas herramientas.
Un ataque típico con Hashcat se ve así:
# Ataque de diccionario con reglas de mutación
hashcat -m 0 -a 0 hashes.txt rockyou.txt -r rules/best64.rule
# -m 0: tipo de hash MD5
# -a 0: modo diccionario
# hashes.txt: archivo con hashes a descifrar
# rockyou.txt: diccionario de contraseñas
# -r rules/best64.rule: reglas de mutación (capitalize, append numbers, etc.)
Con este comando, Hashcat aplica 64 reglas de transformación a cada palabra del diccionario. Si el diccionario tiene 10 millones de entradas, se prueban 640 millones de variaciones. Contra MD5, esto tarda segundos.
Método de ataque 2: Tablas rainbow
Una tabla rainbow es un ataque de precomputación. En lugar de calcular hashes en tiempo real, el atacante genera una tabla enorme con pares de contraseña-hash antes del ataque.
import hashlib
def build_rainbow_table(wordlist):
"""Genera una tabla rainbow básica a partir de una lista de palabras."""
table = {}
for word in wordlist:
md5_hash = hashlib.md5(word.encode()).hexdigest()
table[md5_hash] = word
return table
def lookup(target_hash, table):
"""Busca un hash en la tabla rainbow."""
return table.get(target_hash, None)
# Ejemplo de uso
passwords = ["password", "123456", "admin", "letmein", "welcome"]
rainbow = build_rainbow_table(passwords)
# Simular un hash robado
stolen_hash = hashlib.md5("admin".encode()).hexdigest()
print(f"Hash robado: {stolen_hash}")
result = lookup(stolen_hash, rainbow)
if result:
print(f"Contraseña encontrada: {result}")
else:
print("No encontrada en la tabla")
Hash robado: 21232f297a57a5a743894a0e4a801fc3
Contraseña encontrada: admin
Por qué las sales destruyen las tablas rainbow
Una sal (salt) es un valor aleatorio que se añade a la contraseña antes de hacer el hash. Cada usuario tiene una sal diferente.
import hashlib
import os
def hash_with_salt(password):
"""Genera un hash con sal aleatoria."""
salt = os.urandom(16)
salted = salt + password.encode()
hash_result = hashlib.sha256(salted).hexdigest()
return salt.hex(), hash_result
# Mismo password, diferentes sales → diferentes hashes
salt1, hash1 = hash_with_salt("password")
salt2, hash2 = hash_with_salt("password")
print(f"Sal 1: {salt1}")
print(f"Hash 1: {hash1}")
print(f"Sal 2: {salt2}")
print(f"Hash 2: {hash2}")
print(f"¿Iguales? {hash1 == hash2}") # False
Con sales, la tabla rainbow del atacante se vuelve inútil. Tendría que generar una tabla completa para cada sal posible, lo cual es computacionalmente inviable.
Por qué MD5 y SHA-1 ya no protegen — La realidad GPU
MD5 y SHA-1 fueron diseñados para ser rápidos. Eso es exactamente lo que los hace peligrosos para almacenar contraseñas. Cuanto más rápido es el hash, más rápido puede un atacante probar combinaciones.
Estos son los benchmarks reales de una RTX 4090 con Hashcat (v6.2.6):
| Algoritmo | Hashes por segundo | Tiempo para 8 chars (a-z, A-Z, 0-9) |
|---|---|---|
| MD5 | 164,100 MH/s (164 mil millones) | ~22 minutos |
| SHA-1 | 50,600 MH/s (50.6 mil millones) | ~72 minutos |
| SHA-256 | 22,000 MH/s (22 mil millones) | ~2.8 horas |
| bcrypt (32 iteraciones) | 184 kH/s | Siglos |
La diferencia es brutal. MD5 se calcula a 164 mil millones de hashes por segundo. bcrypt (con 32 iteraciones, la configuración de Hive Systems) solo 184,000. Eso es aproximadamente 890,000 veces más rápido. Con el cost factor 12 recomendado (~1,400 H/s), la brecha supera los 100 millones de veces. Argon2id es aún más lento que bcrypt gracias a su diseño memory-hard.
La escala del problema
Un atacante no necesita una RTX 4090. Los servicios de computación en la nube permiten alquilar clusters de GPUs por horas. Por el costo de un café puedes tener acceso a poder de cómputo que hace una década requería un supercomputador.
Y no es solo una GPU. Hashcat soporta ataques distribuidos con múltiples GPUs. Los benchmarks de Hive Systems usan 12 RTX 4090 en paralelo. Organizaciones con más recursos pueden escalar aún más.
Defensas modernas — Argon2id vs bcrypt vs scrypt
La solución es usar algoritmos diseñados específicamente para ser lentos. Se llaman funciones de derivación de claves (KDF) y su propósito es hacer que cada intento de hash cueste tiempo y recursos significativos.
Prioridad según OWASP (2024)
OWASP (Open Web Application Security Project) recomienda este orden de prioridad:
- Argon2id -- ganador del Password Hashing Competition (2015), resistente a ataques con GPU y ASIC
- scrypt -- resistente a ataques de memoria, pero configuración más compleja
- bcrypt -- el más probado en producción, amplio soporte en librerías
Los tres son opciones válidas. Si estás empezando un proyecto nuevo, Argon2id es la mejor opción. Si ya usas bcrypt con un cost factor adecuado, no necesitas migrar urgentemente.
Ejemplo: bcrypt en Node.js
import bcrypt from "bcrypt";
const COST_FACTOR = 12;
async function hashPassword(plaintext) {
const salt = await bcrypt.genSalt(COST_FACTOR);
const hash = await bcrypt.hash(plaintext, salt);
return hash;
}
async function verifyPassword(plaintext, storedHash) {
const isMatch = await bcrypt.compare(plaintext, storedHash);
return isMatch;
}
// Uso
const hash = await hashPassword("MiContraseñaSegura!");
console.log(hash);
// $2b$12$LJ3m4ys3Lk0TSwMvF3Yx8e2Wz5kGd8vR0qN1pA7yH6cB9xD2mK4u
const valid = await verifyPassword("MiContraseñaSegura!", hash);
console.log(valid); // true
const invalid = await verifyPassword("contraseñaIncorrecta", hash);
console.log(invalid); // false
El cost factor (también llamado work factor) controla cuántas iteraciones internas ejecuta bcrypt. Cada incremento de 1 duplica el tiempo de cómputo. Un cost factor de 12 tarda aproximadamente 250ms en hardware moderno, lo cual es imperceptible para el usuario pero devastador para un atacante.
Ejemplo: Argon2id en Python
from argon2 import PasswordHasher
ph = PasswordHasher(
time_cost=1, # iteraciones
memory_cost=47104, # ~46 MB de RAM (recomendación OWASP)
parallelism=1, # hilos paralelos
)
# Hashear
hash_result = ph.hash("MiContraseñaSegura!")
print(hash_result)
# $argon2id$v=19$m=47104,t=1,p=1$c2FsdHZhbHVl$aGFzaHJlc3VsdA...
# Verificar
try:
ph.verify(hash_result, "MiContraseñaSegura!")
print("Contraseña correcta")
except Exception:
print("Contraseña incorrecta")
Lo que hace especial a Argon2id es que no solo es lento en CPU, sino que exige grandes cantidades de memoria RAM. Los ataques con GPU son eficientes porque las GPUs tienen miles de núcleos, pero cada núcleo tiene muy poca memoria. Si cada intento de hash requiere 46 MB de RAM, una GPU con 24 GB solo puede ejecutar unos 520 intentos en paralelo, en lugar de los miles que podría hacer con MD5.
La actualización NIST 2025 — Lo que cambió
NIST (National Institute of Standards and Technology) publicó en julio de 2025 la versión final de sus directrices de contraseñas: SP 800-63B Rev. 4. Los cambios reflejan décadas de investigación sobre cómo la gente realmente usa las contraseñas.
| Aspecto | Antes (NIST 2017) | Ahora (NIST 2025, Rev. 4) |
|---|---|---|
| Longitud mínima | 8 caracteres | 8 mínimo, 15 recomendado |
| Longitud máxima | Indefinido | Soportar al menos 64 caracteres |
| Complejidad obligatoria | Mayúsculas + números + símbolos | Abolida. No se debe exigir |
| Rotación periódica | Cada 60-90 días | Abolida. Solo cambiar ante evidencia de compromiso |
| Verificación contra filtraciones | No mencionado | Obligatorio. Verificar contra listas de contraseñas filtradas |
| Preguntas de seguridad | Permitidas | Prohibidas como factor de autenticación |
| SMS como 2FA | Permitido | Permitido pero restringido. Se prefieren TOTP/FIDO2 |
Por qué se abolió la complejidad obligatoria
La investigación demostró que los requisitos de complejidad hacen que la gente:
- Elija patrones predecibles:
Password1!,Summer2024!,Company@123 - Anote las contraseñas en post-its
- Reutilice la misma contraseña con variaciones mínimas
Una contraseña como caballo-correcto-batería-grapa (passphrase de 4 palabras) es más segura y fácil de recordar que P@ssw0rd! -- y la complejidad obligatoria la rechazaría.
Por qué se abolió la rotación periódica
El mismo problema. Forzar cambios cada 90 días produce contraseñas como Empresa2024Q1, Empresa2024Q2, Empresa2024Q3. El atacante que descifra una puede predecir las siguientes.
NIST ahora dice: solo cambia la contraseña cuando hay evidencia de que fue comprometida. La verificación contra bases de datos de filtraciones reemplaza la rotación periódica.
Qué significa esto para desarrolladores
Si desarrollas una aplicación que maneja contraseñas, estas son las acciones concretas:
- Acepta espacios y caracteres especiales. No restrinjas los caracteres permitidos. Los usuarios deben poder escribir passphrases.
- No trunces la contraseña. Soporta al menos 64 caracteres. Algunos sistemas antiguos truncan silenciosamente a 8 o 16 caracteres, destruyendo la seguridad de contraseñas largas.
- Verifica contra HIBP en el registro y cambio de contraseña. Si la contraseña aparece en filtraciones, pide al usuario que elija otra.
- No muestres indicadores de "fortaleza" basados solo en complejidad. Un medidor que dice que
P@ssw0rd!es "fuerte" engaña al usuario. Evalúa longitud y presencia en filtraciones. - Usa Argon2id o bcrypt. No inventes tu propio esquema de hashing. No uses PBKDF2 con pocas iteraciones.
Cómo verificar si tu contraseña fue filtrada
Have I Been Pwned (HIBP) ofrece una API que permite verificar contraseñas sin enviarlas por la red. El mecanismo se llama k-Anonymity y funciona así:
- Calculas el hash SHA-1 de tu contraseña localmente
- Envías solo los primeros 5 caracteres del hash a la API
- La API devuelve todos los hashes que empiezan con esos 5 caracteres
- Comparas localmente si tu hash completo está en la lista
Tu contraseña nunca sale de tu máquina.
# Verificar si "password123" ha sido filtrada
# Paso 1: Calcular SHA-1
echo -n "password123" | sha256sum
# No usamos SHA-256 -- HIBP usa SHA-1
echo -n "password123" | sha1sum
# cbfdac6008f9cab4083784cbd1874f76618d2a97
# Paso 2: Enviar los primeros 5 caracteres (CBFDA)
curl -s https://api.pwnedpasswords.com/range/CBFDA
# Paso 3: Buscar el resto del hash en la respuesta
curl -s https://api.pwnedpasswords.com/range/CBFDA | grep -i "C6008F9CAB4083784CBD1874F76618D2A97"
# C6008F9CAB4083784CBD1874F76618D2A97:2254650
# → Encontrada 2,254,650 veces en filtraciones
El número después de los dos puntos indica cuántas veces se ha encontrado esa contraseña en bases de datos filtradas. password123 aparece 2,254,650 veces. Si usas esta contraseña, cámbiala ahora.
Integrarlo en tu aplicación
import hashlib
import requests
def is_password_pwned(password):
"""Verifica si una contraseña aparece en filtraciones usando HIBP k-Anonymity."""
sha1 = hashlib.sha1(password.encode()).hexdigest().upper()
prefix = sha1[:5]
suffix = sha1[5:]
response = requests.get(f"https://api.pwnedpasswords.com/range/{prefix}")
response.raise_for_status()
for line in response.text.splitlines():
hash_suffix, count = line.split(":")
if hash_suffix == suffix:
return int(count)
return 0
# Ejemplo
count = is_password_pwned("password123")
if count > 0:
print(f"Esta contraseña fue filtrada {count} veces. No la uses.")
else:
print("Esta contraseña no aparece en filtraciones conocidas.")
Esta contraseña fue filtrada 2254650 veces. No la uses.
Conclusión
Estos son los puntos clave que deberías recordar:
- La longitud supera a la complejidad. Una contraseña de 16 caracteres con solo minúsculas es más resistente que una de 8 con símbolos. Usa passphrases si puedes.
- MD5 y SHA-1 están rotos para contraseñas. Una GPU moderna prueba miles de millones de hashes MD5 por segundo. Si desarrollas software, usa Argon2id, bcrypt o scrypt.
- Las sales son obligatorias. Sin sal, las tablas rainbow hacen el trabajo instantáneo. Los algoritmos modernos (bcrypt, Argon2id) generan la sal automáticamente.
- NIST 2025 cambió las reglas. No más complejidad obligatoria, no más rotación periódica. Sí: verificación contra filtraciones y longitud mínima de 15 caracteres recomendada.
- Verifica tus contraseñas hoy. Usa la API de HIBP o servicios como Have I Been Pwned. Si una contraseña fue filtrada, cámbiala inmediatamente y no la reutilices.
La seguridad de contraseñas no depende de trucos ingeniosos. Depende de longitud, buenos algoritmos y no reutilizar.