๐Ÿ›ก OWASP Lab
A02:2021 High

Cryptographic Failures

Theory

Formerly called Sensitive Data Exposure, this was renamed in 2021 to identify the root cause: weak or missing cryptography. It sits at #2 because almost every application stores or transmits passwords, tokens, or PII โ€” and the difference between a safe and a catastrophic breach often comes down to a single algorithm choice.

What Goes Wrong

  • Passwords hashed with MD5 or SHA-1 โ€” these are general-purpose hashing algorithms, not password hashing functions. They are brutally fast (billions per second on a GPU), making every password in a leaked database crackable within hours
  • Base64 used as "encryption" โ€” base64 is reversible encoding, not encryption. atob("dXNlcjoxMjM=") in any browser returns the original string immediately
  • Data sent over HTTP โ€” credentials and session tokens travel in plaintext; any network observer captures them
  • Hardcoded secrets in source code โ€” API keys and JWT secrets committed to git leak to anyone with repo access
  • ECB mode symmetric encryption โ€” identical plaintext blocks produce identical ciphertext, leaking data patterns (see the famous ECB penguin image)
  • Weak random number generation โ€” using random.randint() or a timestamp as a secret/token is predictable

Why MD5 is Broken for Passwords

# MD5 of "admin123" = 0192023a7bbd73250516f069df18b500
# This exact hash is in every publicly available rainbow table.
# Speed on a modern GPU: ~10 BILLION MD5 hashes per second.

# The entire rockyou.txt wordlist (14 million passwords) takes < 1 second:
$ hashcat -m 0 0192023a7bbd73250516f069df18b500 rockyou.txt
Cracked: admin123   (0.4 seconds)

# Compare bcrypt (cost 12) on the same hardware:
# Speed: ~20,000 bcrypt hashes per second
# Time to crack same wordlist: ~700 seconds per password (not per list)

Password Hashing Algorithm Comparison

Algorithm    GPU Speed        Salted?   Designed for passwords?
-----------  ---------------  --------  -----------------------
MD5          10 billion/s     No        No  โ€” general hashing
SHA-256       3 billion/s     No        No  โ€” general hashing
bcrypt           20,000/s     Yes       Yes โ€” deliberately slow, adjustable cost
Argon2id          1,000/s     Yes       Yes โ€” memory-hard; current best practice
scrypt           10,000/s     Yes       Yes โ€” memory-hard alternative

Vulnerable Code Pattern

import hashlib, base64, json

# VULNERABLE โ€” MD5 is not a password hashing function
def store_password(password: str) -> str:
    return hashlib.md5(password.encode()).hexdigest()    # crackable in seconds

# VULNERABLE โ€” base64 is encoding, not encryption
def create_session(user_id: int, role: str) -> str:
    data = json.dumps({"user_id": user_id, "role": role})
    return base64.b64encode(data.encode()).decode()      # trivially reversible

Fixed Code

import bcrypt, secrets

# FIXED โ€” bcrypt with cost factor 12 (~100ms per hash; brute-force infeasible)
def store_password(password: str) -> str:
    return bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12)).decode()

def verify_password(plain: str, hashed: str) -> bool:
    return bcrypt.checkpw(plain.encode(), hashed.encode())

# FIXED โ€” cryptographically random 256-bit session token
def create_session() -> str:
    token = secrets.token_hex(32)   # 32 bytes = 256 bits; stored server-side
    session_store[token] = {"user_id": user.id, "role": user.role}
    return token   # only the opaque token goes in the Set-Cookie header

Real-World Breaches

  • RockYou (2009) โ€” 32 million passwords stored in plaintext; became the source of the most-used password cracking wordlist ever
  • LinkedIn (2012) โ€” 6.5 million unsalted SHA-1 password hashes leaked; all were cracked within days
  • Adobe (2013) โ€” 153 million accounts; passwords encrypted with 3DES in ECB mode. The hints column let researchers decrypt millions without the key
  • Ashley Madison (2015) โ€” 36 million accounts; many passwords stored in MD5; cracked and published publicly

How to Fix โ€” Checklist

  • Passwords โ€” use bcrypt (cost โ‰ฅ 12), Argon2id, or scrypt. Never MD5, SHA-1, or SHA-256 alone
  • Session tokens โ€” generate with secrets.token_hex(32) (Python) or crypto.randomBytes(32) (Node). The token is an opaque reference; all data lives server-side
  • Sensitive data at rest โ€” AES-256-GCM with authenticated encryption; store keys separately from data (HSM or secrets manager)
  • TLS everywhere โ€” force HTTPS; set Strict-Transport-Security: max-age=31536000; includeSubDomains
  • Secrets in config โ€” environment variables or a dedicated secrets manager (HashiCorp Vault, AWS Secrets Manager). Never commit secrets to git
Challenge 1

Weak Cookie โ€” Base64-encoded session

Log in as alice. Inspect the Set-Cookie header โ€” the session is just base64-encoded JSON. Decode it, change user_id to 1, re-encode, then call /web/a02/profile with your modified cookie.

Hint
echo '{"user_id": 2, "role": "user"}' | base64 โ†’ modify user_id to 1
Challenge 2

MD5 Password Hashes Exposed in API

The user listing API returns MD5-hashed passwords. Crack the admin password hash using a rainbow table or hashcat.

Hint
MD5 is not a password hashing function โ€” /web/a02/api/users returns all hashes.