Fallos criptográficos
Los errores criptográficos son vulnerabilidades que surgen no del algoritmo en sí, sino de cómo se usa. Pequeños fallos en la configuración, generación de claves, elección de algoritmos o…
Definición
Los errores criptográficos son vulnerabilidades que surgen no del algoritmo en sí, sino de cómo se usa. Pequeños fallos en la configuración, generación de claves, elección de algoritmos o almacenamiento de secretos pueden convertir una protección criptográfica en papel mojado. Son sorprendentemente comunes y suelen ser “quick wins” en pentesting y auditorías.
OWASP los clasifica como A02 — Cryptographic Failures (antes llamado “Sensitive Data Exposure”), destacando que el problema principal es la exposición de datos sensibles por implementación criptográfica deficiente.
Contexto
Por qué ocurren
La criptografía moderna (AES, RSA, SHA-256) es matemáticamente sólida cuando se usa bien. Los ataques no rompen el algoritmo sino que explotan:
- Claves débiles o predecibles (baja entropía, derivadas de timestamps o datos del usuario).
- Algoritmos obsoletos susceptibles a colisiones o ataques matemáticos (MD5, SHA-1, RSA con parámetros débiles).
- Secretos hardcodeados en código accesible al cliente.
- Ausencia de verificación de integridad junto al cifrado.
- Falta de salting en hashes de contraseñas.
Impacto
- Descifrado de datos protegidos (credenciales, tokens, datos de negocio).
- Escalada de privilegios manipulando tokens cifrados sin autenticación.
- Acceso no autorizado a APIs y backends con claves expuestas.
- Robo de identidad y account takeover.
- Cumplimiento: OWASP Top 10, GDPR, PCI-DSS exigen cifrado robusto.
Desarrollo
1. Claves RSA débiles — Fuerza bruta y ataques matemáticos
RSA basa su seguridad en la dificultad de factorizar un número n = p × q, donde p y q son primos grandes. La clave pública es (n, e) y la privada se deriva de d = e⁻¹ mod φ(n), donde φ(n) = (p−1)(q−1).
Una clave de 128 bits tiene 2¹²⁸ combinaciones posibles (inviable por fuerza bruta con hardware actual). Sin embargo, la seguridad se rompe si la generación de p o q es deficiente.
Vulnerabilidades documentadas (paper “P and Q”, Ross Anderson & Serge Vaudenay):
Primos predecibles: si p o q se generan con un RNG débil (ej. sembrado con timestamp del sistema), un atacante puede recrear el proceso y derivar los primos.
Primos compartidos entre claves: si varias claves RSA comparten un primo p, el atacante puede calcular el MCD:
MCD(n₁, n₂) = pEste cálculo se hace en tiempo polinomial y rompe ambas claves simultáneamente.
Primos demasiado cercanos: si |p - q| es pequeño, el método de factorización de Fermat factoriza n eficientemente.
Exponente público pequeño (e = 3) + mismo texto cifrado: si el mismo plaintext se cifra para múltiples destinatarios con e = 3 y sin padding, el teorema chino del resto (CRT) permite recuperar el plaintext.
Demostración del crecimiento exponencial del tiempo de factorización:
import timefrom sympy.ntheory import factorint
n_small = 253 # 11 × 23n_medium = 988027 # 941 × 1051n_large = 2147483647 # primo grande
for n in [n_small, n_medium, n_large]: start = time.time() factorint(n) print(f"Time to factor {n}: {time.time() - start:.6f} seconds")Resultado típico:
Time to factor 253: 0.000019 secondsTime to factor 988027: 0.000041 secondsTime to factor 2147483647: 0.000094 secondsEl tiempo crece exponencialmente con el tamaño de n, haciendo la factorización inviable para claves bien generadas.
Ejercicio práctico de descifrado RSA (TryHackMe lab):
Datos del reto:
n = 43941819371451617899582143885098799360907134939870946637129466519309346255747e = 65537c = 9002431156311360251224219512084136121048022631163334079215596223698721862766Proceso para recuperar el plaintext:
from sympy import factorintfrom Crypto.Util.number import inverse, long_to_bytes
# Paso 1: Factorizar n (usar FactorDB o MSIEVE/YAFU para n grandes)factors = factorint(n)p, q = factors.keys()
# Paso 2: Calcular phi(n)phi_n = (p - 1) * (q - 1)
# Paso 3: Calcular la clave privada de = 65537d = inverse(e, phi_n)
# Paso 4: Descifrarc = 9002431156311360251224219512084136121048022631163334079215596223698721862766plaintext = pow(c, d, n)flag = long_to_bytes(plaintext)print(flag.decode())Herramientas de factorización: FactorDB, MSIEVE, YAFU, sympy.factorint.
Mitigaciones:
- Generar claves con RNG criptográficamente seguro (CSPRNG), nunca sembrado con datos predecibles.
- Usar RSA de mínimo 2048 bits (preferible 4096).
- Siempre aplicar padding: OAEP (recomendado) o PKCS#1 v1.5.
- Exponente público estándar: e = 65537. Evitar e = 3.
- No cifrar el mismo plaintext para múltiples destinatarios sin padding aleatorio.
2. Rotura de hashes
El hashing es unidireccional: convierte una entrada en una cadena de tamaño fijo (hash). Se usa para almacenamiento de contraseñas, integridad de datos y HMAC. Sus vulnerabilidades principales son:
Algoritmos débiles: MD5 y SHA-1
MD5 y SHA-1 son susceptibles a ataques de colisión (dos entradas distintas que producen el mismo hash) y están oficialmente deprecados para fines de seguridad. No deben usarse para contraseñas ni firmas digitales.
Falta de salting — Rainbow tables
Si no hay salt, la misma contraseña produce siempre el mismo hash. Los atacantes usan tablas arcoíris (rainbow tables): bases de datos precalculadas de pares hash → plaintext que permiten revertir hashes al instante.
El salt (valor aleatorio único añadido a cada contraseña antes del hash) hace que rainbow tables pre-calculadas sean inútiles.
SHA-256 no es adecuado para contraseñas
SHA-256 está optimizado para ser rápido, lo cual es ideal para checksums e integridad pero es un problema para contraseñas. Las GPUs modernas pueden calcular miles de millones de SHA-256 por segundo, haciendo que la fuerza bruta sea altamente efectiva.
Comparación de velocidad con aceleración GPU:
| Función hash | Hashes por segundo (GPU aprox.) |
|---|---|
| MD5 | ~100 mil millones H/s |
| SHA-256 | ~1 mil millones H/s |
| bcrypt (cost=12) | ~1.000 H/s |
| Argon2id | ~100 H/s |
Los esquemas de hash de contraseñas (PHS) como Argon2, bcrypt y PBKDF2 están diseñados específicamente para ser lentos y adaptables: a medida que aumenta el hardware, sus parámetros de coste se pueden aumentar.
Tabla de elección:
| Objetivo | Algoritmo recomendado | Razón |
|---|---|---|
| Almacenamiento de contraseñas | Argon2id, bcrypt, PBKDF2 | Lento y adaptable; desincentiva fuerza bruta |
| Integridad de datos (checksums) | SHA-256, SHA-3, BLAKE2 | Rápido y eficiente |
| Autenticación de mensajes (HMAC) | HMAC-SHA256, HMAC-SHA3 | Verifica integridad, no almacena contraseñas |
HMAC débil — Crack con Hashcat
HMAC (Hash-Based Message Authentication Code) combina una función hash con una clave secreta para verificar autenticidad e integridad. Si la clave es débil o predecible, puede crackearse.
Ejercicio práctico (TryHackMe lab):
Message: CanYouGuessMySecretSHA1-Digest: 1484c3a5d65a55d70984b4d10b1884bda8876c1dUsando Hashcat en modo 150 (HMAC-SHA1, clave como contraseña):
# Guardar hash y mensajeecho -n "1484c3a5d65a55d70984b4d10b1884bda8876c1d:CanYouGuessMySecret" > digest.txt
# Crackear con RockYouhashcat -a 0 -m 150 digest.txt /usr/share/wordlists/rockyou.txtSi la clave secreta es débil (en wordlist), Hashcat la recupera en segundos.
Mitigaciones:
- Usar Argon2id, bcrypt o PBKDF2 para contraseñas, nunca SHA-1/MD5/SHA-256 directo.
- Generar salts aleatorios únicos por usuario (mínimo 16 bytes).
- Para HMAC: usar claves largas y aleatorias, no palabras de diccionario.
- Actualizar algoritmos hash periódicamente según recomendaciones NIST.
3. Claves expuestas en código del lado del cliente
Incluir claves criptográficas, secretos de API o credenciales en código JavaScript u otros archivos accesibles al cliente es un error crítico. Cualquiera con acceso a la aplicación puede recuperarlas con DevTools del navegador.
Riesgos:
- Acceso no autorizado: la clave permite descifrar datos o interactuar con la API como usuario autenticado.
- Manipulación de datos: el atacante puede generar cargas firmadas o modificar mensajes cifrados.
- Abuso de API: claves hardcodeadas dan acceso directo a endpoints privilegiados.
Escenarios comunes:
- Clave AES hardcodeada en JavaScript para cifrar mensajes antes de enviarlos al backend.
- Secretos de API en archivos de configuración servidos desde el servidor web.
- Tokens JWT firmados con clave hardcodeada en el frontend.
Ejercicio práctico (TryHackMe lab — http://bcts.thm/labs/lab3):
La aplicación cifra los mensajes del usuario con AES antes de enviarlos al backend. La clave AES está hardcodeada en el JavaScript: b"1234567890123456".
Con la clave expuesta, el atacante puede cifrar palabras de una wordlist y forzar el endpoint para encontrar el mensaje correcto que da acceso:
import requests, base64from Crypto.Cipher import AESfrom Crypto.Util.Padding import pad
url = "http://bcts.thm/labs/lab3/process.php"encryption_key = b"1234567890123456" # Clave extraída del JS
def encrypt_message(message, iv): padded = pad(message.encode(), AES.block_size) cipher = AES.new(encryption_key, AES.MODE_CBC, iv) ciphertext = cipher.encrypt(padded) return base64.b64encode(ciphertext).decode(), base64.b64encode(iv).decode()
def send_payload(ciphertext, iv): response = requests.post(url, json={"data": ciphertext, "iv": iv}) return response.text
def bruteforce(): with open("wordlist.txt", "r") as f: words = f.readlines() for word in words: word = word.strip() iv = AES.get_random_bytes(16) ciphertext, iv_b64 = encrypt_message(word, iv) response = send_payload(ciphertext, iv_b64) if "Access granted!" in response: print(f"[+] Mensaje correcto: {word}") break
bruteforce()La función bruteforce() itera la wordlist, cifra cada palabra con la clave expuesta y la envía al servidor hasta obtener “Access granted!”.
Mitigaciones:
- Nunca hardcodear claves, secretos de API o credenciales en código del lado del cliente.
- Almacenar secretos en el backend: variables de entorno, KMS (AWS KMS, Azure Key Vault, HashiCorp Vault).
- Realizar todas las operaciones criptográficas sensibles en el servidor.
- Auditar el código frontend antes de cada release con herramientas de secret scanning (git-secrets, truffleHog, GitGuardian).
4. Cifrado no autenticado — Ataques de inversión de bits (Bit-Flipping)
El cifrado garantiza confidencialidad pero no necesariamente integridad. Cuando se usa cifrado sin un mecanismo de verificación de integridad (MAC), un atacante puede modificar el texto cifrado de forma que el texto descifrado cambie de forma controlada y predecible.
AES en modo CBC sin MAC
AES-CBC (Cipher Block Chaining) cifra bloques de 16 bytes de forma encadenada. El vector de inicialización (IV, primeros 16 bytes) se XOR con el primer bloque de plaintext antes del cifrado. Si el IV se modifica, el primer bloque de plaintext descifrado cambia exactamente en los bits modificados.
Plaintext descifrado[0][i] = Plaintext_original[0][i] XOR IV_original[i] XOR IV_modificado[i]Para cambiar el carácter '0' (ASCII 0x30) a '1' (ASCII 0x31), basta hacer XOR con 0x01 en el byte correspondiente del IV.
Ejercicio práctico (TryHackMe lab — http://bcts.thm/labs/lab4/)
La aplicación almacena el rol del usuario en una cookie cifrada con AES-CBC: role = cifrado("0"). Si se manipula el IV del token cifrado para cambiar el rol de "0" a "1", el sistema concede acceso de administrador sin detectar la manipulación.
import base64, sysfrom binascii import unhexlify, hexlify
original_token = sys.argv[1] # Token hex de la cookie 'role'
try: cipher_bytes = bytearray(unhexlify(original_token))except ValueError: print("Token inválido. Debe ser una cadena hexadecimal.") exit(1)
block_size = 16
# Modificar el byte 0 del IV para cambiar '0' (0x30) a '1' (0x31)# XOR con 0x01 invierte el bit menos significativoprint(f"[DEBUG] IV original: {hexlify(cipher_bytes[:block_size]).decode()}")cipher_bytes[0] ^= 0x01 # '0' XOR 0x01 = '1' en el plaintext descifradoprint(f"[DEBUG] IV modificado: {hexlify(cipher_bytes[:block_size]).decode()}")
modified_token = hexlify(cipher_bytes).decode()print(f"\nToken modificado: {modified_token}")print("Usa este token como cookie 'role' en el navegador para acceder como admin.")Ejecución:
python3 exploit.py <token_hex_de_cookie_role>Al reemplazar la cookie role con el token modificado y refrescar la página, el servidor descifra el token manipulado y obtiene role=1 (admin), concediendo acceso al panel de administración.
Por qué funciona: AES-CBC no verifica que el ciphertext no haya sido alterado. El servidor confía en que si puede descifrar el token, es legítimo.
Mitigaciones:
- Usar cifrado autenticado: modos como AES-GCM o AES-CCM que incluyen autenticación integrada (AEAD).
- Si se usa AES-CBC, añadir siempre un MAC (HMAC-SHA256) separado y verificarlo antes de descifrar.
- Nunca derivar permisos o roles directamente de datos cifrados sin verificar integridad.
- Implementar validación del lado del servidor independiente del token.
Ejemplos
Los cuatro ejercicios del lab bcts.thm de TryHackMe cubren cada una de las técnicas anteriores en entornos controlados. Ver secciones de Desarrollo para los scripts completos y el paso a paso de cada explotación.
Herramientas utilizadas en los labs:
sympy.factorint: factorización de primos para RSA débil.FactorDB.com: base de datos online de factorizaciones.MSIEVE,YAFU: factorización de enteros de alto rendimiento.Hashcat(modo 150 para HMAC-SHA1): cracking de claves HMAC débiles.pycryptodome(Crypto.Cipher.AES): implementación AES para demostración de fuerza bruta y bit-flipping.
Pitfalls
- “SHA-256 es seguro, lo usaré para contraseñas”: SHA-256 es rápido por diseño. Para contraseñas, la velocidad es una desventaja. Usar siempre Argon2id, bcrypt o PBKDF2.
- “Mi código JavaScript es minificado, nadie lo leerá”: la ofuscación no es seguridad. DevTools, Prettier o cualquier desofuscador revelan el código en segundos. Nunca incluir secretos en código cliente.
- “Uso AES-CBC, eso es cifrado fuerte”: AES-CBC sin MAC no protege contra manipulación del ciphertext. “Cifrado fuerte” no equivale a “cifrado autenticado”.
- “Mi clave RSA tiene 1024 bits, es suficiente”: NIST recomienda mínimo 2048 bits desde 2010. Las claves de 1024 bits son factorizables con hardware moderno.
- “No necesito padding en RSA porque mi clave es grande”: el tamaño de la clave no protege contra ataques CRT o de broadcast attack. El padding (OAEP) es obligatorio.
- “Genero mis IVs aleatoriamente, estoy protegido”: si el modo de cifrado no incluye autenticación (GCM vs CBC), IVs aleatorios no previenen bit-flipping. El atacante modifica el ciphertext, no el IV del cliente.
- Testing en producción: el lab usa
bcts.thmen entorno controlado. Estos ataques en producción pueden comprometer datos reales y tener consecuencias legales.
Diagrama
mindmap
root((Cryptographic Failures))
Claves RSA débiles
Primos predecibles (RNG débil)
Primos compartidos (MCD attack)
Primos cercanos (Fermat)
Exponente pequeño (e=3, CRT)
::icon(fa fa-key)
Hashes débiles
MD5/SHA-1 (colisiones)
Sin salt (rainbow tables)
SHA-256 para contraseñas (velocidad)
HMAC con clave débil (Hashcat)
::icon(fa fa-hashtag)
Claves expuestas
Hardcoded en JavaScript
Archivos de config públicos
Secret scanning ausente
::icon(fa fa-eye)
Cifrado no autenticado
AES-CBC sin MAC
Bit-flipping en IV
Escalada de rol/privilegios
::icon(fa fa-unlock)
Referencias
- TryHackMe — Broken Cryptography & Failures: https://tryhackme.com/room/brokencryptographyfailures
- OWASP Top 10 A02 — Cryptographic Failures: https://owasp.org/Top10/A02_2021-Cryptographic_Failures/
- Ross Anderson & Serge Vaudenay — “Why cryptosystems fail” (P and Q paper): https://www.cl.cam.ac.uk/archive/rja14/Papers/psandqs.pdf
- NIST SP 800-131A — Transitioning the Use of Cryptographic Algorithms: https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-131Ar2.pdf
- OWASP Cryptographic Storage Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Cryptographic_Storage_Cheat_Sheet.html
- Argon2 specification: https://github.com/P-H-C/phc-winner-argon2
- FactorDB: https://factordb.com/
- Hashcat example hashes: https://hashcat.net/wiki/doku.php?id=example_hashes
Ver también: Cryptography Basics, OWASP Top 10 (2025), Hashcat