10 Herramientas de Hashing que Todo Hacker Debe Conocer
El encoding es reversible, el hashing no. Esa es la línea que separa
la representación de datos de la integridad. Un hash no guarda tu contraseña,
guarda su huella digital. No encripta tu archivo, genera su firma.Entender qué hace cada función hash, cuándo usarla (y cuándo no),
es esencial tanto para atacar como para defender sistemas.Este es el segundo artículo de la trilogía. El primero cubrió encoding;
el próximo cubrirá encriptación.
¿Qué es hashing y por qué importa?
Una función hash toma cualquier entrada y produce una salida de longitud fija
(llamada hash, resumen o digest). Es unidireccional: dado un hash, es
computacionalmente inviable encontrar la entrada original.
Hashing ≠ Encoding ≠ Encriptación
| Propiedad | Encoding | Hashing | Encriptación |
|---|---|---|---|
| Reversible | Sí | No | Sí (con clave) |
| Longitud salida | Variable | Fija | Variable |
| Misma entrada = misma salida | Sí | Sí | No (con IV) |
| Uso principal | Transporte | Integridad, passwords | Confidencialidad |
Propiedades clave de un hash criptográfico
- Determinista: misma entrada → mismo hash
- Rápido de computar: para cualquier entrada
- Resistente a preimagen: dado un hash, no se encuentra la entrada
- Resistente a segunda preimagen: dada una entrada, no se encuentra otra con el mismo hash
- Resistente a colisión: no se encuentran dos entradas con el mismo hash
- Efecto avalancha: un bit de cambio en la entrada cambia ~50% de los bits de salida
1. MD5 (Message Digest 5)
Longitud: 128 bits (32 caracteres hex). Desarrollado por Ronald Rivest en 1991.
Roto desde 2004 (colisiones demostradas por Wang et al.). Sigue siendo el hash
más ubicuo en sistemas legacy, y el que más aparece en CTFs.
Es rápido, está en todas partes, pero no debe usarse para seguridad.
Colisiones prácticas se generan en segundos con herramientas como hashcoll o md5coll.
Usos en CTF
- Identificación de tipo de hash: Es el más común en retos de cracking.
El formato5d41402abc4b2a76b9719d911017c592es inconfundible (32 hex). - Colisiones de MD5: Retos donde dos strings diferentes deben producir
el mismo hash para bypassear una validación de integridad. Se genera un
par colisionante con herramientas comomd5collohashclash. - Hash Length Extension Attack: MD5 (como SHA1 y SHA256) es vulnerable.
Si el servidor usahash(secret + mensaje)como MAC, un atacante puede
extender el mensaje y calcular el nuevo hash sin conocer el secret. - Fuerza bruta de contraseñas: Hashes
MD5($password)de usuarios.
Modo hashcat:-m 0. - Doble MD5: Algunos retos usan
md5(md5($password)). Sigue siendo
débil porque la cadena no tiene sal.
Usos en el mundo real
- Verificación de integridad legacy: Descargas de software antiguo que
aún proporcionan checksums MD5. No es seguro contra atacantes activos
(pueden generar un archivo malicioso con el mismo MD5 que el legítimo). - Almacenamiento de contraseñas (mal práctica): Sistemas que almacenan
contraseñas comoMD5(password)sin sal son vulnerables a ataques de
rainbow tables y fuerza bruta. El leak de LinkedIn 2012 (6.5M hashes
SHA1 sin sal) es el caso más famoso. - Deduplicación de datos: Sistemas internos que usan MD5 para detectar
archivos duplicados (donde no hay adversario). - Forense: Identificar muestras de malware conocidas por su hash MD5
en VirusTotal.
🧪 Código Python — MD5
import hashlib
# ─── Generar hash ─────────────────────────────────────────
def md5_hash(data: str) -> str:
return hashlib.md5(data.encode()).hexdigest()
def md5_file(path: str) -> str:
h = hashlib.md5()
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(8192), b""):
h.update(chunk)
return h.hexdigest()
# ─── Doble MD5 (común en CTFs) ──────────────────────────
def md5_double(data: str) -> str:
return md5_hash(md5_hash(data))
# ─── MD5 con sal simple ──────────────────────────────────
def md5_with_salt(password: str, salt: str) -> str:
return md5_hash(salt + password)
# ─── Verificar (comparación segura contra timing) ────────
import hmac
def md5_verify(data: str, expected_hash: str) -> bool:
return hmac.compare_digest(md5_hash(data), expected_hash)
# ─── Detectar formato MD5 ────────────────────────────────
import re
def es_md5(cadena: str) -> bool:
return bool(re.fullmatch(r'[0-9a-fA-F]{32}', cadena))
# ─── Fuerza bruta simple ─────────────────────────────────
def md5_crack(hash_obj: str, wordlist: list) -> str:
for word in wordlist:
if md5_hash(word) == hash_obj:
return word
return None
2. SHA1 (Secure Hash Algorithm 1)
Longitud: 160 bits (40 caracteres hex). Desarrollado por la NSA en 1995.
Roto desde 2017 (colisión SHAttered demostrada por Google y CWI).
Fue el estándar de hash durante dos décadas. Hoy está en retiro activo,
pero sigue siendo el hash más común en integridad de archivos legacy,
firmas digitales antiguas, y por supuesto, CTFs.
Usos en CTF
- SHAttered: El reto de colisión SHA1 más famoso. Dos archivos PDF con
el mismo SHA1 pero contenido radicalmente diferente. Explota la colisión
para bypassear validación de firma o integridad. - Hash Length Extension: SHA1 también es vulnerable al mismo ataque
que MD5. Misma estructura Merkle-Damgård, misma vulnerabilidad. - Firma digital débil: Retos donde un binario o script está firmado y
la firma SHA1 puede ser falseada por colisión. - Identificación: 40 caracteres hex. El segundo hash más común en CTFs
después de MD5. Modo hashcat:-m 100. - Commit de Git: Git usa SHA1 para identificar commits. Hay CTFs que
explotan colisiones SHA1 para suplantar commits o firmas.
Usos en el mundo real
- Verificación de integridad legacy: Distribuciones Linux que aún
proporcionan SHA1SUMS (aunque la mayoría ya migró a SHA256). - Git: SHA1 es el núcleo del sistema de objetos de Git. Una colisión
SHA1 permitiría manipular el historial de un repositorio (ataque
demostrado por Linus Torvalds como «ataque al Git»). El proyecto Git
está migrando a SHA256. - Certificados SSL/TLS: Los certificados SHA1 fueron descontinuados
por los navegadores en 2017. Hoy cualquier certificado SHA1 es
considerado inválido. - Forense: Identificación de malware por SHA1 en bases como VirusTotal.
🧪 Código Python — SHA1
import hashlib
import hmac
# ─── Generar hash ─────────────────────────────────────────
def sha1_hash(data: str) -> str:
return hashlib.sha1(data.encode()).hexdigest()
def sha1_file(path: str) -> str:
h = hashlib.sha1()
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(8192), b""):
h.update(chunk)
return h.hexdigest()
# ─── SHA1 con sal ─────────────────────────────────────────
def sha1_with_salt(password: str, salt: str) -> str:
return hashlib.sha1((salt + password).encode()).hexdigest()
# ─── Verificar ────────────────────────────────────────────
def sha1_verify(data: str, expected_hash: str) -> bool:
return hmac.compare_digest(sha1_hash(data), expected_hash)
# ─── Detectar formato ─────────────────────────────────────
import re
def es_sha1(cadena: str) -> bool:
return bool(re.fullmatch(r'[0-9a-fA-F]{40}', cadena))
# ─── SHA1 de un commit de Git ─────────────────────────────
def sha1_git_object(tipo: str, contenido: bytes) -> str:
"""Calcula el hash Git de un objeto (blob, tree, commit, tag)."""
raw = f"{tipo} {len(contenido)}\0".encode() + contenido
return hashlib.sha1(raw).hexdigest()
# git hash-object == sha1_git_object("blob", b"hello world")
3. SHA256 (SHA-2)
Longitud: 256 bits (64 caracteres hex). Desarrollado por la NSA en 2001.
Es el estándar actual de hashing criptográfico. Resistente a colisiones
(ninguna conocida práctica).
Es el hash más usado hoy en día: certificados SSL/TLS, firmas de software,
blockchain (Bitcoin usa SHA256 doble), verificación de integridad de
descargas, y prácticamente cualquier aplicación moderna que necesite un hash.
Usos en CTF
- Hash Length Extension: A pesar de ser más seguro que MD5/SHA1, SHA256
también usa estructura Merkle-Damgård y es vulnerable a length extension. - Bitcoin/blockchain: Retos que involucran minado, prueba de trabajo,
o direcciones de Bitcoin (SHA256 + RIPEMD160). - Firma de archivos: Verificar integridad de binarios en retos de
forense o reversing. - Identificación: 64 caracteres hex. Modo hashcat:
-m 1400. - Merkle tree: Retos que implementan árboles de Merkle para verificar
integridad de datos distribuidos. - PBKDF2-HMAC-SHA256: Derivación de clave usada en algunos retos de
criptografía.
Usos en el mundo real
- Verificación de integridad de descargas:
sha256sumes el comando
estándar para verificar ISOs de Linux, instaladores, y cualquier archivo
descargado. Kali Linux, Ubuntu, Debian todos proporcionan SHA256SUMS. - Blockchain: Bitcoin usa SHA256 doble (
SHA256(SHA256(data))) como
prueba de trabajo y para direcciones. Entender SHA256 es esencial para
auditar criptomonedas. - Certificados SSL/TLS: Todos los certificados modernos usan SHA256
como algoritmo de firma. Un certificado SHA256 es requisito para
ser aceptado por navegadores actuales. - Firma de código: Binarios firmados con SHA256 (Authenticode,
APK signing, etc.). - Integridad de contenedores: Docker usa SHA256 para identificar
imágenes y capas.
🧪 Código Python — SHA256
import hashlib
import hmac
# ─── Generar hash ─────────────────────────────────────────
def sha256_hash(data: str) -> str:
return hashlib.sha256(data.encode()).hexdigest()
def sha256_file(path: str) -> str:
h = hashlib.sha256()
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(8192), b""):
h.update(chunk)
return h.hexdigest()
# ─── SHA256 doble (Bitcoin) ───────────────────────────────
def sha256_double(data: str) -> str:
return sha256_hash(sha256_hash(data))
# ─── Verificar ────────────────────────────────────────────
def sha256_verify(data: str, expected_hash: str) -> bool:
return hmac.compare_digest(sha256_hash(data), expected_hash)
# ─── Detectar formato ─────────────────────────────────────
import re
def es_sha256(cadena: str) -> bool:
return bool(re.fullmatch(r'[0-9a-fA-F]{64}', cadena))
# ─── Iteraciones HMAC-SHA256 (PBKDF2-like simple) ────────
def pbkdf2_sha256_simple(password: str, salt: str, iterations: int = 100000) -> str:
key = password.encode()
for _ in range(iterations):
key = hashlib.sha256(key + salt.encode()).digest()
return key.hex()
# ─── Checksum de archivo (equivalente a sha256sum) ───────
def sha256sum(path: str) -> str:
import os
if not os.path.isfile(path):
return None
return f"{sha256_file(path)} {os.path.basename(path)}"
4. SHA512 (SHA-2)
Longitud: 512 bits (128 caracteres hex). Parte de la misma familia SHA-2.
Más lento que SHA256 porque procesa bloques de 1024 bits vs 512 bits,
pero ofrece un espacio de salida mucho mayor.
Usado donde se requiere seguridad adicional contra ataques cuánticos
(las funciones hash son menos vulnerables a Grover que el cifrado simétrico,
pero el espacio de 512 bits ofrece más margen). También es común en
sistemas Unix/Linux para almacenar contraseñas ($6$).
Usos en CTF
- Identificación: 128 caracteres hex. Menos común que SHA256, pero
aparece en retos de criptografía avanzada. - Hashing de contraseñas Linux: Los hashes
/etc/shadowcon formato$6$salt$hashusan SHA512 (5000 rondas por defecto). Retos de
escalada de privilegios o cracking. - Derivación de clave: Algunos protocolos custom usan SHA512 para
generar claves más largas en una sola iteración. - Modo hashcat:
-m 1700para SHA512 plano,-m 1800para SHA512
crypt (Linux).
Usos en el mundo real
- Contraseñas en Linux: El formato
$6$en/etc/shadowusa SHA512
con 5000 rondas por defecto. Hardening de sistemas implica aumentar
el número de rondas en/etc/login.defs. - Verificación de integridad: Distribuciones de alta seguridad que
proporcionan checksums SHA512 además de SHA256. - DNSsec: Algoritmos de firma DNSSEC usan SHA512.
- Derivación de clave: Donde se necesitan 512 bits de material de
clave (ej: cifrado AES-256 + HMAC-SHA256 con dos claves derivadas
del mismo material).
🧪 Código Python — SHA512
import hashlib
import hmac
# ─── Generar hash ─────────────────────────────────────────
def sha512_hash(data: str) -> str:
return hashlib.sha512(data.encode()).hexdigest()
def sha512_file(path: str) -> str:
h = hashlib.sha512()
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(8192), b""):
h.update(chunk)
return h.hexdigest()
# ─── Verificar ────────────────────────────────────────────
def sha512_verify(data: str, expected_hash: str) -> bool:
return hmac.compare_digest(sha512_hash(data), expected_hash)
# ─── Detectar formato ─────────────────────────────────────
import re
def es_sha512(cadena: str) -> bool:
return bool(re.fullmatch(r'[0-9a-fA-F]{128}', cadena))
# ─── Simular hash estilo /etc/shadow $6$ ──────────────────
def sha512_crypt_simple(password: str, salt: str) -> str:
"""Versión simplificada del hash SHA512 crypt de Linux."""
h = hashlib.sha512((salt + password).encode()).digest()
for _ in range(4999): # 5000 rondas total
h = hashlib.sha512(h + password.encode()).digest()
return f"$6${salt}${h.hex()}"
# ─── Parsear hash de /etc/shadow ─────────────────────────
def parse_shadow_hash(hash_str: str) -> dict:
"""Analiza un hash de /etc/shadow."""
parts = hash_str.split("$")
if len(parts) < 4:
return {"error": "formato inválido"}
return {
"algoritmo": {"1": "MD5", "5": "SHA256", "6": "SHA512"}.get(parts[1], parts[1]),
"salt": parts[2],
"hash": parts[3],
}
5. HMAC (Hash-based Message Authentication Code)
HMAC no es una función hash — es un MAC (Message Authentication Code)
construido a partir de una función hash y una clave secreta.
HMAC(K, m) = H((K XOR opad) || H((K XOR ipad) || m))
Provee integridad + autenticación: garantiza que el mensaje no fue
modificado y que fue generado por alguien que conoce la clave.
Usos en CTF
- Hash Length Extension Attack: HMAC fue diseñado específicamente para
resistir este ataque. Si un CTF usaH(secret || message)como MAC,
es vulnerable. Si usaHMAC(secret, message), no lo es. - JWT con HMAC: Tokens JWT firmados con HS256 (HMAC-SHA256). Retos
de secret débil, fuerza bruta del secret, o confusión RS256 → HS256. - API tokens: Retos donde un token HMAC debe ser forjado sin conocer
la clave. - Timing attack: Comparación de HMAC. Usar
hmac.compare_digest()
previene timing attacks; usar==los permite.
Usos en el mundo real
- APIs REST: Firmas HMAC para autenticar requests (AWS Signature V4,
Stripe, Binance, etc.). El request incluye un HMAC de los parámetros
usando una clave secreta compartida. - JWT (HS256): El algoritmo HS256 usa HMAC-SHA256 para firmar tokens.
Es el más común en aplicaciones web. - OAuth 1.0: Las firmas HMAC son parte del protocolo.
- Webhooks: Los webhooks de GitHub, Stripe, etc. se firman con HMAC
para verificar que vienen del servicio legítimo. - Derivación de clave: PBKDF2 usa HMAC iterativamente.
🧪 Código Python — HMAC
import hmac
import hashlib
import base64
# ─── HMAC básico ──────────────────────────────────────────
def hmac_sha256(key: str, message: str) -> str:
return hmac.new(key.encode(), message.encode(), hashlib.sha256).hexdigest()
def hmac_sha1(key: str, message: str) -> str:
return hmac.new(key.encode(), message.encode(), hashlib.sha1).hexdigest()
def hmac_md5(key: str, message: str) -> str:
return hmac.new(key.encode(), message.encode(), hashlib.md5).hexdigest()
# ─── HMAC en Base64 (JWT, APIs) ──────────────────────────
def hmac_b64(key: str, message: str) -> str:
sig = hmac.new(key.encode(), message.encode(), hashlib.sha256).digest()
return base64.urlsafe_b64encode(sig).rstrip(b"=").decode()
# ─── Verificar HMAC (con comparación segura) ─────────────
def hmac_verify(key: str, message: str, expected: str) -> bool:
computed = hmac_sha256(key, message)
return hmac.compare_digest(computed, expected)
# ─── Fuerza bruta de clave HMAC ──────────────────────────
def hmac_crack_key(message: str, target_hmac: str, wordlist: list) -> str:
for word in wordlist:
if hmac_sha256(word, message) == target_hmac:
return word
return None
# ─── Simular JWT HS256 ───────────────────────────────────
import json
def jwt_hs256(payload: dict, secret: str) -> str:
header = base64url_encode(json.dumps({"alg": "HS256", "typ": "JWT"}))
body = base64url_encode(json.dumps(payload))
message = f"{header}.{body}"
sig = hmac_b64(secret, message)
return f"{message}.{sig}"
def base64url_encode(data: str) -> str:
return base64.urlsafe_b64encode(data.encode()).decode().rstrip("=")
6. bcrypt
bcrypt es un algoritmo de hashing de contraseñas diseñado por Niels Provos
y David Mazières en 1999. Está específicamente diseñado para ser lento
y adaptativo: el factor de costo se puede incrementar a medida que
el hardware mejora.
A diferencia de MD5/SHA256, bcrypt incluye la sal automáticamente en el
output, y su lentitud (por diseño) hace que la fuerza bruta sea inviable
para contraseñas de complejidad media.
Usos en CTF
- Identificación: El formato
$2b$10$...con 60 caracteres es
inconfundible. Identificar el tipo de hash es el primer paso. - Cracking con wordlists: bcrypt es el más lento de crackear.
Un hashcat en una GPU hace ~300 hashes/segundo vs millones para MD5.
Los CTFs suelen dar pistas sobre la contraseña para hacerlo viable. - Factor de costo: Algunos retos usan
$2b$04$(costo 4, muy bajo)
para hacerlo crackeable. Otros usan$2b$12$(estándar) o más. - Comparación de hashes: Entender que bcrypt incluye sal y costo en
el propio hash (todo-en-uno) es clave.
Usos en el mundo real
- Almacenamiento de contraseñas: Es el estándar recomendado por OWASP.
Usado por Django (por defecto desde 1.4), Flask-Bcrypt, y muchas otras
plataformas. - Migración desde MD5/SHA1: Proyectos que migran su almacenamiento de
contraseñas a bcrypt por seguridad. - Hardening de autenticación: Incrementar el factor de costo según
el hardware disponible. Costo 12 (~250ms) es el recomendado actualmente. - Defensa contra rainbow tables: Cada hash bcrypt incluye una sal
única de 128 bits, haciendo las rainbow tables inviables.
🧪 Código Python — bcrypt
import bcrypt as _bcrypt # pip install bcrypt
# ─── Hash de contraseña ───────────────────────────────────
def bcrypt_hash(password: str, rounds: int = 12) -> str:
"""Genera un hash bcrypt. El salt se genera automáticamente."""
salt = _bcrypt.gensalt(rounds=rounds)
return _bcrypt.hashpw(password.encode(), salt).decode()
# ─── Verificar contraseña ─────────────────────────────────
def bcrypt_verify(password: str, hash_str: str) -> bool:
return _bcrypt.checkpw(password.encode(), hash_str.encode())
# ─── Extraer información del hash ─────────────────────────
def bcrypt_parse(hash_str: str) -> dict:
"""Analiza un hash bcrypt y extrae sus componentes."""
parts = hash_str.split("$")
if len(parts) != 4 or not parts[1].startswith("2"):
return {"error": "formato bcrypt inválido"}
return {
"version": parts[1],
"costo": int(parts[2]),
"salt": parts[3][:22],
"hash": parts[3][22:],
}
# ─── Estimar tiempo de cracking ───────────────────────────
def bcrypt_estimate_strength(hash_str: str, wordlist_size: int = 1000000) -> str:
"""Estima el tiempo de fuerza bruta para este hash."""
info = bcrypt_parse(hash_str)
if "error" in info:
return "hash inválido"
costo = info["costo"]
# Aprox: 2^costo * ~1ms por hash en CPU
tiempo_por_hash_ms = 2 ** (costo - 10) # normalizado a costo 10 ≈ 5ms
tiempo_total = (wordlist_size * tiempo_por_hash_ms) / 1000 # segundos
if tiempo_total < 60:
return f"{tiempo_total:.0f} segundos"
if tiempo_total < 3600:
return f"{tiempo_total/60:.0f} minutos"
return f"{tiempo_total/3600:.1f} horas"
# ─── Bruteforce limitado (solo para CTFs con pistas) ─────
def bcrypt_crack(hash_str: str, wordlist: list) -> str:
for word in wordlist:
if _bcrypt.checkpw(word.encode(), hash_str.encode()):
return word
return None
7. scrypt
scrypt es un algoritmo de derivación de clave diseñado por Colin Percival
en 2009 (para Tarsnap). Es intencionalmente lento y consume mucha memoria (memory-hard), lo que lo hace resistente a ataques con hardware
especializado (FPGAs, ASICs).
Mientras que bcrypt solo es caro en CPU, scrypt también es caro en memoria:
un atacante necesita mucha RAM para paralelizar el ataque.
Usos en CTF
- Identificación: Formato
$7$(no confundir con SHA512$6$). - Factor de memoria: El parámetro N (coste de CPU/memoria), r (tamaño
de bloque), p (paralelismo). Retos con parámetros débiles para hacer
el hash crackeable. - Derivación de clave: Algunos CTFs piden derivar una clave de
cifrado desde una contraseña usando scrypt. - Cryptocurrency: Litecoin, Dogecoin y otras usan scrypt como prueba
de trabajo. Retos de minado o análisis de blockchain.
Usos en el mundo real
- Criptomonedas: Litecoin (LTC), Dogecoin (DOGE) y otras usan scrypt
como algoritmo de prueba de trabajo (minado). - Derivación de clave: Convertir contraseñas en claves de cifrado
para discos encriptados (LUKS usa scrypt desde 2018 o PBKDF2). - Almacenamiento de contraseñas: Menos común que bcrypt en apps web,
pero usado en sistemas más security-conscious. - Hardening: Aumentar los parámetros N, r, p para proteger contra
hardware especializado.
🧪 Código Python — scrypt
import hashlib
import base64
# ─── scrypt básico (usando hashlib - Python 3.6+) ────────
def scrypt_hash(password: str, salt: str = None, N: int = 16384,
r: int = 8, p: int = 1, dklen: int = 64) -> str:
"""Deriva clave usando scrypt. salt se genera si no se provee."""
import os
if salt is None:
salt = base64.b64encode(os.urandom(16)).decode()
key = hashlib.scrypt(
password.encode(),
salt=salt.encode() if isinstance(salt, str) else salt,
n=N, r=r, p=p, dklen=dklen
)
return f"$7$N={N},r={r},p={p}${salt}${key.hex()}"
# ─── Verificar contraseña contra hash scrypt ─────────────
def scrypt_verify(password: str, hash_str: str) -> bool:
"""Verifica una contraseña contra un hash scrypt generado por scrypt_hash()."""
parts = hash_str.split("$")
if len(parts) != 4:
return False
params_str = parts[1]
salt = parts[2]
expected_hex = parts[3]
# Parsear parámetros
import re
params = {}
for p in params_str.split(","):
k, v = p.split("=")
params[k.strip()] = int(v.strip())
computed = hashlib.scrypt(
password.encode(),
salt=salt.encode(),
n=params.get("N", 16384),
r=params.get("r", 8),
p=params.get("p", 1),
dklen=len(bytes.fromhex(expected_hex))
)
return hmac.compare_digest(computed.hex(), expected_hex)
# ─── scrypt con parámetros débiles (CTF) ─────────────────
def scrypt_ctf_weak(password: str, salt: str) -> str:
"""scrypt con parámetros débiles para CTFs (crackeable)."""
key = hashlib.scrypt(
password.encode(), salt=salt.encode(),
n=2, r=1, p=1, dklen=32 # N=2 es extremadamente débil
)
return f"$7$N=2,r=1,p=1${salt}${key.hex()}"
# ─── Identificar formato scrypt ──────────────────────────
def es_scrypt(hash_str: str) -> bool:
return hash_str.startswith("$7$")
8. Argon2
Argon2 es el ganador del Password Hashing Competition (PHC) en 2015.
Diseñado por Alex Biryukov, Daniel Dinu y Dmitry Khovratovich. Es el
estado del arte en hashing de contraseñas.
Tres variantes:
- Argon2d: Resistente a timing attacks (usa acceso a memoria dependiente de datos)
- Argon2i: Resistente a side-channel attacks (usa acceso a memoria independiente)
- Argon2id: Híbrido (recomendado por defecto)
Actualmente recomendado por OWASP como el estándar para almacenamiento
de contraseñas.
Usos en CTF
- Identificación: Formato
$argon2id$v=19$m=65536,t=3,p=4$....
Cada vez más común en CTFs modernos. - Parámetros débiles: Valores bajos de m (memoria), t (tiempo),
p (paralelismo) hacen el hash crackeable. - Derivación de clave: Argon2 puede derivar claves de cualquier
longitud desde una contraseña. - Cracking: Es el más lento de los algoritmos de password. Un hashcat
con GPU hace pocos hashes/segundo. Los CTFs con Argon2 suelen dar
pistas o usar parámetros muy reducidos.
Usos en el mundo real
- Estándar OWASP: Recomendado para nuevo desarrollo. Si estás
empezando un proyecto hoy, usa Argon2id. - PHP 7.2+:
password_hash()conPASSWORD_ARGON2IDdisponible
desde PHP 7.3. - Python:
hashlib.scrypt()yargon2-cffi(pip). - Key Derivation Function: Derivar claves de cifrado desde
contraseñas de usuario. - Hardening: Parámetros recomendados: m=19456 (19MB), t=2, p=1.
🧪 Código Python — Argon2
# Requiere: pip install argon2-cffi
from argon2 import PasswordHasher, exceptions
from argon2 import Type as Argon2Type
# ─── Hash con Argon2id (recomendado) ──────────────────────
def argon2_hash(password: str) -> str:
"""Genera hash Argon2id con parámetros seguros por defecto."""
ph = PasswordHasher(
time_cost=2, # t = 2 iteraciones
memory_cost=19456, # m = 19 MB
parallelism=1, # p = 1 hilo
hash_len=32, # 256 bits de salida
type=Argon2Type.ID # Argon2id (recomendado)
)
return ph.hash(password)
# ─── Verificar ────────────────────────────────────────────
def argon2_verify(password: str, hash_str: str) -> bool:
"""Verifica contraseña contra hash Argon2."""
ph = PasswordHasher()
try:
return ph.verify(hash_str, password)
except exceptions.VerifyMismatchError:
return False
# ─── Hash con parámetros débiles (CTF) ───────────────────
def argon2_ctf_weak(password: str) -> str:
"""Argon2 con parámetros mínimos para que sea crackeable."""
ph = PasswordHasher(
time_cost=1,
memory_cost=8, # 8 KB (mínimo)
parallelism=1,
hash_len=16,
type=Argon2Type.I
)
return ph.hash(password)
# ─── Parsear hash ─────────────────────────────────────────
def argon2_parse(hash_str: str) -> dict:
"""Extrae parámetros de un hash Argon2."""
parts = hash_str.split("$")
if len(parts) < 5 or not parts[1].startswith("argon2"):
return {"error": "formato Argon2 inválido"}
info = {
"type": parts[1],
"version": parts[2].lstrip("v="),
}
for param in parts[3].split(","):
k, v = param.split("=")
info[k] = int(v)
return info
9. NTLM (Windows NT LAN Manager)
NTLM no es una función hash tradicional — es un protocolo de autenticación
de Microsoft. El «hash NTLM» es en realidad el resultado de aplicar
MD4 (tres veces a la contraseña en Unicode).
Formato: 32 caracteres hex (como MD5, pero usa MD4 internamente).
A diferencia de los hashes de Unix, NTLM no usa sal. Esto significa
que dos usuarios con la misma contraseña tienen exactamente el mismo hash
NTLM, y las rainbow tables son efectivas.
Usos en CTF
- Pass-the-Hash: El ataque más famoso de Windows. Obtienes el hash
NTLM de un usuario y lo usas directamente para autenticarte sin
necesidad de conocer la contraseña en texto plano. - Identificación: 32 caracteres hex (como MD5). Modo hashcat
-m 1000. - Dump de SAM: Retos donde extraes hashes del archivo SAM de Windows
y necesitas crackearlos. - Mimikatz: Retos que simulan el volcado de credenciales con Mimikatz.
- Kerberos: Hashes NTLM también se usan para generar tickets Kerberos.
Usos en el mundo real
- Ataques Pass-the-Hash: Es la técnica más usada en post-explotación
de entornos Windows. Una vez que obtienes un hash NTLM (de SAM, LSASS,
o NTDS.dit), puedes moverte lateralmente sin crackear la contraseña. - Active Directory: Los hashes NTLM se almacenan en NTDS.dit (el
controlador de dominio). Un atacante que extrae NTDS.dit obtiene todos
los hashes del dominio. - Responder / SMB Relay: Herramientas como Responder capturan hashes
NTLM en la red para relay o cracking. - Cracking: Sin sal, los hashes NTLM se crackean rápido con hashcat
(modo-m 1000). Una GPU moderna hace miles de millones de intentos
por segundo.
🧪 Código Python — NTLM
import hashlib
import hmac
import binascii
# ─── Hash NTLM (MD4 de Unicode little-endian) ─────────────
def ntlm_hash(password: str) -> str:
"""Calcula el hash NTLM de una contraseña."""
# La contraseña se convierte a UTF-16LE (little-endian)
password_utf16 = password.encode("utf-16le")
# MD4 (disponible en hashlib en algunos builds, o vía Cryptodome)
try:
md4 = hashlib.new("md4", password_utf16)
return md4.hexdigest().upper()
except ValueError:
# Fallback: usar Cryptodome si hashlib no tiene md4
from Cryptodome.Hash import MD4
md4 = MD4.new(password_utf16)
return md4.hexdigest().upper()
# ─── Hash NTLMv2 (más seguro que NTLMv1) ─────────────────
def ntlmv2_hash(username: str, password: str, domain: str = "") -> str:
"""NTLMv2 combina usuario+dominio con el hash NTLM."""
ntlm = ntlm_hash(password)
identity = (username.upper() + domain).encode("utf-16le")
h = hmac.new(binascii.unhexlify(ntlm), identity, hashlib.md5)
return h.hexdigest().upper()
# ─── Detectar formato ─────────────────────────────────────
import re
def es_ntlm(cadena: str) -> bool:
"""NTLM tiene 32 hex, como MD5. Pero suele estar en mayúsculas."""
return bool(re.fullmatch(r'[0-9A-Fa-f]{32}', cadena))
# ─── Pass-the-Hash simulado ───────────────────────────────
def pass_the_hash(ntlm_hash_str: str, target: str) -> str:
"""
Simula pass-the-hash. En la práctica usarías herramientas como
impacket o mimikatz. Esto solo muestra el concepto.
"""
return f"[*] Autenticándose contra {target} con hash {ntlm_hash_str}"
# ─── Cracking NTLM con wordlist ───────────────────────────
def ntlm_crack(hash_obj: str, wordlist: list) -> str:
for word in wordlist:
if ntlm_hash(word) == hash_obj:
return word
return None
10. Identificador Universal de Hashes
A diferencia del encoding (donde cada formato es visualmente distinto),
los hashes son difíciles de distinguir solo por su longitud. MD5 y NTLM
son ambos 32 hex. SHA256 y SHA3-256 son ambos 64 hex.
Esta herramienta intenta identificar cualquier hash usando longitud,
formato, prefijos, y contenido.
🧪 Código Python — Identificador de hashes + Cracking
import re
import hashlib
# ─── Base de datos de tipos de hash ───────────────────────
HASH_PATTERNS = [
# (nombre, regex, longitud_hex, prefijo, modo_hashcat)
("MD5", r'^[0-9a-fA-F]{32}$', 32, None, "0"),
("NTLM", r'^[0-9a-fA-F]{32}$', 32, None, "1000"),
("SHA1", r'^[0-9a-fA-F]{40}$', 40, None, "100"),
("SHA256", r'^[0-9a-fA-F]{64}$', 64, None, "1400"),
("SHA512", r'^[0-9a-fA-F]{128}$',128, None, "1700"),
("bcrypt", r'^\$2[aby]\$\d{2}\$[A-Za-z0-9./]{53}$', None, "$2", "3200"),
("bcrypt-old", r'^\$2[aby]\$\d{2}\$[A-Za-z0-9./]{53}$', None, "$2", "3200"),
("scrypt", r'^\$7\$.*', None, "$7", "8900"),
("Argon2i", r'^\$argon2i\$.*', None, "$argon2i", "9200"),
("Argon2id", r'^\$argon2id\$.*', None, "$argon2id", "9200"),
("SHA256-crypt", r'^\$5\$[A-Za-z0-9./]+\$[A-Za-z0-9./]+$', None, "$5", "7400"),
("SHA512-crypt", r'^\$6\$[A-Za-z0-9./]+\$[A-Za-z0-9./]+$', None, "$6", "1800"),
("MD5-crypt", r'^\$1\$[A-Za-z0-9./]+\$[A-Za-z0-9./]+$', None, "$1", "500"),
("SHA3-256", r'^[0-9a-fA-F]{64}$', 64, None, "17300"),
("SHA3-512", r'^[0-9a-fA-F]{128}$',128, None, "17400"),
("RIPEMD160", r'^[0-9a-fA-F]{40}$', 40, None, "6000"),
("Whirlpool", r'^[0-9a-fA-F]{128}$',128, None, "6100"),
("MySQL", r'^[0-9a-fA-F]{16}$', 16, None, "200"),
("MySQL5", r'^\*[0-9a-fA-F]{40}$', 40, "*", "300"),
("CRC32", r'^[0-9a-fA-F]{8}$', 8, None, "11500"),
("LM Hash", r'^[0-9a-fA-F]{32}$', 32, None, "3000"),
("HalfLMCHalLM", r'^[0-9a-fA-F]{16}$', 16, None, "1500"),
]
def identificar_hash(cadena: str) -> list:
"""Identifica posibles tipos para un hash dado."""
cadena = cadena.strip()
resultados = []
for nombre, regex, _, prefijo, modo_hc in HASH_PATTERNS:
if prefijo and not cadena.startswith(prefijo):
continue
if regex and re.fullmatch(regex, cadena, re.IGNORECASE):
resultados.append({
"tipo": nombre,
"hashcat_mode": modo_hc,
"longitud": len(cadena),
})
if not resultados:
return [{"tipo": "desconocido", "hashcat_mode": "?", "longitud": len(cadena)}]
return resultados
# ─── Analizador completo ──────────────────────────────────
def analizar_hash(hash_str: str) -> dict:
"""Analiza un hash y devuelve información detallada."""
return {
"hash": hash_str,
"longitud": len(hash_str),
"caracteres": set(hash_str),
"tiene_prefijo": any(hash_str.startswith(p) for _, _, _, p, _ in HASH_PATTERNS if p),
"posibles_tipos": identificar_hash(hash_str),
"recomendacion_hashcat": identificar_hash(hash_str)[0]["hashcat_mode"]
if identificar_hash(hash_str) else "?",
}
# ─── Cracking automático (intenta según tipo) ────────────
def crack_hash(hash_str: str, wordlist: list) -> str:
"""Intenta crackear un hash identificando primero su tipo."""
tipos = identificar_hash(hash_str)
if not tipos:
return None
tipo = tipos[0]["tipo"]
for word in wordlist:
word = word.strip()
if tipo == "MD5" or tipo == "NTLM":
try:
if hashlib.md5(word.encode()).hexdigest().upper() == hash_str.upper():
return word
except: pass
if tipo == "SHA1":
if hashlib.sha1(word.encode()).hexdigest() == hash_str:
return word
if tipo == "SHA256":
if hashlib.sha256(word.encode()).hexdigest() == hash_str:
return word
if tipo == "SHA512":
if hashlib.sha512(word.encode()).hexdigest() == hash_str:
return word
return None
# ─── Detección de hash débil por longitud ─────────────────
def detectar_hash_debil(hash_str: str) -> bool:
"""Detecta si un hash tiene una longitud que indica debilidad."""
longitudes_debiles = {8: "CRC32 / LM Half",
16: "MySQL pre-5.7 / HalfLM",
32: "MD5 / NTLM (débiles)"}
return longitudes_debiles.get(len(hash_str), None)
Bonus: Comparación de velocidad de cracking
Esta tabla muestra cuántos hashes por segundo puede probar una GPU
moderna (RTX 4090) para cada algoritmo, y cuánto tarda en crackear
una contraseña de 8 caracteres alfanuméricos (~2.8 billones de combinaciones):
| Algoritmo | Hash/s (RTX 4090) | Tiempo para 8 chars alfanum |
|---|---|---|
| NTLM | ~200 B/s | < 1 segundo |
| MD5 | ~150 B/s | < 1 segundo |
| SHA1 | ~50 B/s | ~1 minuto |
| SHA256 | ~20 B/s | ~2 minutos |
| SHA512 | ~7 B/s | ~6 minutos |
| bcrypt (costo 5) | ~200 K/s | ~160 días |
| bcrypt (costo 10) | ~10 K/s | ~9 años |
| bcrypt (costo 12) | ~2.5 K/s | ~35 años |
| scrypt | ~500 /s | ~180 años |
| Argon2id | ~100 /s | ~900 años |
Nota: Los valores son aproximados y dependen de la implementación, configuración de memoria (scrypt/Argon2) y velocidad de la GPU.
🧪 Código Python — Benchmark de velocidad
import time
import hashlib
def benchmark_hash(name: str, func, iterations: int = 10000) -> float:
"""Mide hashes por segundo de una función hash."""
data = "test_password_123"
start = time.perf_counter()
for _ in range(iterations):
func(data)
elapsed = time.perf_counter() - start
hashes_per_sec = iterations / elapsed
print(f"{name:15s} → {hashes_per_sec:>12.0f} hash/s")
return hashes_per_sec
# benchmark_hash("MD5", lambda d: hashlib.md5(d.encode()).hexdigest())
# benchmark_hash("SHA256", lambda d: hashlib.sha256(d.encode()).hexdigest())
# benchmark_hash("SHA512", lambda d: hashlib.sha512(d.encode()).hexdigest())
Tabla comparativa
| # | Algoritmo | Bits | Hex chars | Colisiones conocidas | Uso principal | Hashcat mode |
|---|---|---|---|---|---|---|
| 1 | MD5 | 128 | 32 | Sí (prácticas) | Legacy, checksums | 0 |
| 2 | SHA1 | 160 | 40 | Sí (prácticas) | Legacy, Git | 100 |
| 3 | SHA256 | 256 | 64 | No | Estándar actual | 1400 |
| 4 | SHA512 | 512 | 128 | No | Alta seguridad, Linux | 1700 |
| 5 | HMAC | variable | variable | — | Autenticación de APIs | — |
| 6 | bcrypt | 192 | 60 | No | Contraseñas (estándar) | 3200 |
| 7 | scrypt | variable | variable | No | Contraseñas (memory-hard) | 8900 |
| 8 | Argon2 | variable | variable | No | Contraseñas (PHC winner) | 9200 |
| 9 | NTLM | 128 | 32 | Sí (rainbow tables) | Windows auth | 1000 |
| 10 | Identificador | — | — | — | Pentesting | — |
Conclusión
El hashing es la base de la integridad digital y la autenticación moderna.
Saber identificar qué tipo de hash tienes delante, entender sus fortalezas
y debilidades, y conocer las herramientas para atacarlo o defenderlo,
es una habilidad fundamental en seguridad informática.
Regla de oro:
- ¿Necesitas verificar integridad? → SHA256
- ¿Necesitas almacenar contraseñas? → Argon2id o bcrypt
- ¿Estás en un CTF? → Identifica el tipo, elige el modo hashcat correcto
- ¿Ves un hash de 32 hex sin prefijo? → Podría ser MD5 o NTLM. No los confundas.
Este artículo es el segundo de una trilogía. El primero cubrió encoding.
Próximo: Encriptación — cifrado simétrico y asimétrico en CTFs y en el mundo real.