Skip to main content

Free 30-min security demo  — We'll scan your real code and show live findings, no commitment Book Now

Offensive360
Academy Cryptographic Failures
Intermediate · 20 min

Cryptographic Failures

Understand why MD5 and SHA-1 are broken for passwords, how to use bcrypt and Argon2 correctly, and what TLS misconfigurations expose you to.

1 Weak Hashing Algorithms — MD5 and SHA-1 for Passwords

Cryptographic failures (OWASP Top 10 #2) cover a wide range of mistakes: using broken algorithms, insufficient key lengths, storing sensitive data unencrypted, and using cryptography incorrectly. Password hashing is the most common failure point.

The broken approach:

import hashlib

def hash_password(password: str) -> str:
    # CRITICALLY WRONG — MD5 is a fast general-purpose hash, not a password hash
    return hashlib.md5(password.encode()).hexdigest()

def hash_password_v2(password: str) -> str:
    # STILL WRONG — SHA-1 and SHA-256 are also fast hashes, not designed for passwords
    return hashlib.sha256(password.encode()).hexdigest()

Why fast hashes are wrong for passwords:

  • MD5 can compute ~10 billion hashes per second on a modern GPU
  • SHA-256 can compute ~3 billion hashes per second on a modern GPU
  • A 6-character password hash can be cracked in under a second
  • Rainbow tables precompute hashes for common passwords — no salt means instant lookup
  • MD5 has been cryptographically broken since 2004 — hash collisions are trivially generated

Real breach impact: When LinkedIn leaked 117 million SHA-1 password hashes in 2012, 90% were cracked within 3 days using GPU clusters and rainbow tables.

2 Proper Password Hashing — bcrypt, Argon2, PBKDF2

Password hashing algorithms are specifically designed to be intentionally slow and memory-hard, making brute-force attacks computationally expensive regardless of hardware.

bcrypt — the battle-tested standard:

import bcrypt

def hash_password(password: str) -> bytes:
    # Cost factor 12: ~250ms on modern hardware — fine for login, brutal for brute-force
    salt = bcrypt.gensalt(rounds=12)
    return bcrypt.hashpw(password.encode('utf-8'), salt)

def verify_password(password: str, hashed: bytes) -> bool:
    return bcrypt.checkpw(password.encode('utf-8'), hashed)

Argon2 — OWASP's current recommendation (winner of the 2015 Password Hashing Competition):

from argon2 import PasswordHasher

ph = PasswordHasher(
    time_cost=2,       # iterations
    memory_cost=65536, # 64 MB of RAM per hash — memory-hard
    parallelism=2,     # threads
)

hashed = ph.hash("user_password")
ph.verify(hashed, "user_password")  # raises VerifyMismatchError if wrong

PBKDF2 — built into Python's standard library:

import hashlib, os

def hash_password(password: str) -> str:
    salt = os.urandom(32)
    key = hashlib.pbkdf2_hmac('sha256', password.encode(), salt, 600_000)
    # Store: salt + key (both needed for verification)
    return (salt + key).hex()

Key properties of proper password hashes: automatic salt generation (prevents rainbow tables), configurable work factor (increase as hardware improves), memory hardness (Argon2), and algorithm agility (the hash stores its own parameters so you can re-hash on next login as costs increase).

Never roll your own crypto. Do not implement bcrypt or Argon2 yourself — use vetted libraries. Do not invent new "hybrid" hash schemes. One mistake (bad PRNG, timing side-channel, truncated output) can silently break all security.

3 TLS Misconfiguration & Key Management

Even with correct password hashing, data in transit and at rest must be protected. TLS misconfigurations expose sensitive data to network attackers.

Common TLS mistakes:

  • TLS 1.0 / 1.1 still enabled — both are deprecated (RFC 8996, 2021). Only TLS 1.2+ should be supported
  • Weak cipher suites — RC4, 3DES, and export-grade ciphers are broken. Use only AES-GCM or ChaCha20-Poly1305
  • Self-signed certificates — clients cannot verify authenticity; vulnerable to MitM
  • Certificate validation disabled in code — common "quick fix" that destroys all TLS security
  • Mixed content — HTTPS page loading HTTP resources; cookies and tokens exposed

The catastrophic mistake — disabling certificate validation:

import requests

# NEVER DO THIS — disables all TLS certificate validation
response = requests.get('https://api.example.com', verify=False)

Key management failures:

  • Hardcoding API keys, passwords, or TLS private keys in source code
  • Storing secrets in environment variables that are logged or exposed in error messages
  • Using the same key for all environments (dev, staging, production)
  • Never rotating keys after a potential compromise

Best practices: Use a secrets manager (AWS Secrets Manager, HashiCorp Vault, Azure Key Vault). Rotate keys regularly. Use short-lived credentials where possible. Enable HSTS (HTTP Strict Transport Security) with a long max-age. Use OCSP stapling. Monitor certificates for expiry and unexpected reissuance (certificate transparency logs).

Knowledge Check

0/4 correct
Q1

Why is MD5 unsuitable for password hashing even when a salt is added?

Q2

What property of Argon2 makes it especially resistant to GPU-based cracking?

Q3

A developer adds verify=False to all requests.get() calls because a staging server has a self-signed cert. What is the impact?

Q4

Which of the following is the OWASP-recommended modern password hashing algorithm?

Code Exercise

Replace MD5 Password Hashing with bcrypt

The function below hashes passwords with MD5 — critically insecure. Replace it with a proper password hashing function using bcrypt, argon2-cffi, or hashlib.pbkdf2_hmac.

python