Common Attacks and Vulnerabilities

1

Common Attacks and Vulnerabilities

Hashing is a foundational primitive in modern security systems, used to protect stored passwords, verify file integrity, authenticate messages, and underpin digital signatures. Yet hashing is not magic — it is a mathematical operation with specific properties, and when those properties are weakened, misapplied, or deliberately exploited, entire security systems can collapse. Understanding the major attack categories against hash-based systems is essential for any practitioner who designs, evaluates, or maintains software that depends on cryptographic hashing. The attacks described here are not theoretical: they have been used in real-world breaches to recover millions of user credentials, forge digital signatures, and bypass authentication mechanisms. Each attack exploits a specific weakness, whether in the algorithm itself, in the way the algorithm is applied, or in the implementation details of comparison logic.

Brute-Force Attacks on Hashed Passwords

A brute-force attack on a hashed password database is deceptively straightforward in concept. Because hash functions are deterministic — the same input always produces exactly the same output — an attacker who obtains a stored hash can verify any candidate password by hashing it with the same algorithm and comparing the result. There is no need to reverse the hash mathematically. The attacker simply iterates through candidate passwords until a match is found.

The practical feasibility of brute-force depends on three interacting factors: the size of the search space, the speed of the hash function, and the computational resources available to the attacker. Modern GPUs can compute billions of MD5 or SHA-1 hashes per second. A six-character lowercase password has only 266 ≈ 308 million combinations — a search space exhausted in a fraction of a second on commodity hardware. Even adding uppercase letters and digits grows the space to about 56 billion combinations for six characters, which still falls within reach of a determined attacker with GPU clusters.

The primary defenses against brute-force are:

  • Slow hashing functions: Algorithms such as bcrypt, scrypt, and Argon2 are deliberately computationally expensive. bcrypt, for example, includes a configurable cost factor. Increasing this factor by one doubles the computation time. Where MD5 can be computed billions of times per second per GPU, bcrypt at a cost factor of 12 allows only a few hundred attempts per second on the same hardware. This asymmetry punishes attackers far more than it inconveniences legitimate logins.
  • Strong password policies: Requiring longer passwords with diverse character classes expands the search space exponentially. A random 12-character password drawn from a 94-character printable ASCII set represents 9412 ≈ 4.76 × 1023 combinations — far beyond exhaustive search even with fast algorithms.
  • Account lockout and rate limiting: Online brute-force (attacking a live login endpoint) is defeated by rate limiting and lockout policies, though these do not protect offline attacks against stolen hash databases.

To illustrate the difference in hashing speed, consider the following rough comparison of candidate throughput on a single modern GPU:

Hash Algorithm Approximate Hashes/Second (GPU) Time to Exhaust 6-char Lowercase
MD5 ~50 billion < 1 second
SHA-256 ~10 billion ~1 second
bcrypt (cost 10) ~20,000 ~4 hours
Argon2id (moderate settings) ~1,000 ~3.5 days

The table makes clear why algorithm choice is not a cosmetic decision: the difference between MD5 and Argon2id represents many orders of magnitude in attacker cost, turning a trivial attack into an impractical one.

Rainbow Table Attacks

A rainbow table attack is a form of precomputed brute-force. Instead of computing hashes at attack time, an adversary builds a large lookup table in advance, mapping known passwords to their digests. When a hash database is stolen, the attacker simply queries the table for each hash, retrieving the corresponding plaintext instantly — no computation required at attack time.

Rainbow tables exploit a key property of hash functions: given a fixed algorithm, the hash of a given string is always the same. If ten users all choose the password password123 and the system stores unsalted SHA-1 hashes, all ten records will contain the identical hash cbfdac6008f9cab4083784cbd1874f76618d2a97. One table lookup recovers all ten passwords simultaneously.

The critical defense against rainbow tables is salting. A salt is a random value — typically 16 bytes or more — generated uniquely for each user at account creation. The salt is concatenated with the password before hashing:

stored_hash = hash_function(salt + password)

The salt is then stored in plaintext alongside the hash (there is no security value in hiding the salt; its purpose is uniqueness, not secrecy). When the user logs in, the system retrieves their salt, recomputes the hash with the provided password, and compares. This defeats precomputed rainbow tables because:

  • Even if two users share the same password, their different salts produce different hashes, so a single table entry cannot match both.
  • An attacker would need to construct a separate rainbow table for every unique salt value, making precomputation astronomically expensive. With a 128-bit random salt, the number of possible salts is 2128 — precomputation becomes completely infeasible.
  • Publicly available rainbow tables (which cover billions of common passwords against MD5, SHA-1, and NTLM) become useless because they contain entries computed without any salt.

Systems that store passwords hashed with unsalted MD5 or SHA-1 — still found in legacy codebases and older CMS installations — are trivially broken. Services like CrackStation maintain freely accessible tables covering hundreds of billions of candidate strings. A breach of such a system results in near-instant mass credential recovery.

Collision Attacks

A hash collision occurs when two distinct inputs produce the identical digest. Formally, a collision-resistant hash function should make it computationally infeasible to find any pair (M1, M2) where M1 ≠ M2 yet hash(M1) = hash(M2). When this property breaks down, serious integrity and authentication failures follow.

The most consequential real-world exploitation of collision attacks involves digital certificates and file signatures. Consider a scenario where an attacker crafts two documents: one is a legitimate certificate signing request; the other is a malicious certificate for a different domain. If both documents hash to the same value, and a Certificate Authority signs the hash of the legitimate document, the resulting signature is mathematically valid for the malicious document as well. This is not hypothetical — the Flame malware (discovered 2012) used an MD5 collision against Microsoft's Terminal Services licensing certificates to forge a code-signing certificate, enabling it to impersonate Windows Update.

MD5 is completely broken for collision resistance. The first practical MD5 collision was demonstrated in 2004, and today collisions can be generated in seconds on consumer hardware using tools like HashClash. SHA-1 was formally broken in 2017 when Google's Project Zero demonstrated the SHAttered attack, producing the first known SHA-1 collision — two different PDF files with the same SHA-1 hash.

The implications extend beyond password storage:

  • File integrity verification: Using MD5 or SHA-1 checksums to verify software downloads provides no meaningful security; an attacker can produce a malicious binary with the same checksum as the legitimate one.
  • Digital signatures: Any signature scheme that hashes data before signing is only as strong as the collision resistance of the hash function used.
  • Certificate forgery: As demonstrated by Flame, weak collision resistance in certificate hashing enables man-in-the-middle attacks at a fundamental level.

SHA-2 (SHA-256, SHA-384, SHA-512) and the SHA-3 family (based on the Keccak construction) currently maintain strong collision resistance and are the recommended algorithms for all cryptographic applications requiring integrity guarantees.

Weak and Deprecated Hash Functions

Not all hash functions are created equal, and the security landscape has made certain algorithms obsolete for any security-sensitive use. Understanding why they are deprecated — rather than simply knowing which ones to avoid — allows practitioners to reason about future algorithm transitions and to evaluate vendor claims.

Algorithm Digest Size Status Primary Weaknesses
MD5 128 bits Broken / Deprecated Collision attacks (trivial), preimage attacks feasible, extremely fast (aids brute-force)
SHA-1 160 bits Deprecated by NIST Collision attacks demonstrated (SHAttered), deprecated in TLS and code signing
SHA-256 256 bits Current / Recommended No known practical attacks; suitable for integrity but too fast for password hashing alone
SHA-3 (Keccak) 224–512 bits Current / Recommended Different internal construction from SHA-2; no known practical weaknesses
bcrypt 192 bits (output) Recommended for passwords Slower to adapt to GPU parallelism due to memory access patterns; good for passwords
Argon2id Configurable Recommended for passwords (NIST SP 800-63B) Memory-hard; resists GPU and ASIC acceleration; winner of Password Hashing Competition

MD5's 128-bit digest is problematic beyond just collision attacks. A shorter digest means a smaller output space (2128 possible values), and the computational speed of MD5 — orders of magnitude faster than purpose-built password hashing algorithms — means that brute-force and dictionary attacks run far more efficiently against it. Using MD5 or SHA-1 for password storage is a compounding mistake: the algorithm is already broken for collision resistance, and its speed makes it an ideal target for offline cracking campaigns.

Migrating away from deprecated algorithms in production systems requires planning. A common and safe approach is on-login rehashing: when a user successfully authenticates (meaning you have their plaintext password momentarily available), rehash their credential with the new algorithm, update the stored record, and mark the account as migrated. Users who never log in may need to be prompted to reset their passwords. This approach avoids requiring a mass password reset and gradually upgrades the entire user base with each successful login.

Unsalted Hashing and Its Consequences

The consequences of storing unsalted hashes go beyond the individual user — they create systemic vulnerabilities that amplify the blast radius of any breach. When no salt is applied, every user who has chosen the same password (a common occurrence: password, 123456, letmein are perennially popular) will have an identical stored hash. An attacker who cracks one hash has simultaneously compromised every account sharing that password.

Consider a breach of 10 million user records stored as unsalted SHA-256 hashes. An attacker runs the top 10,000 most common passwords through SHA-256 — a computation taking milliseconds — and matches them against all 10 million records simultaneously. Password reuse statistics from breach analyses consistently show that 10–20% of accounts use passwords from the top 1,000. Without salts, those millions of accounts fall within seconds. With unique salts, the attacker must perform 10 million separate cracking campaigns, one per user, multiplying the cost by a factor of millions.

The consequences extend beyond the breached service:

  • Credential stuffing: Recovered credentials are immediately tested against other services (banking, email, social media). Password reuse across services is endemic, so a single unsalted breach often compromises accounts on dozens of other platforms.
  • Bulk sale and automation: Cracked credential lists from unsalted breaches are packaged and sold on underground markets within hours of a breach being discovered, enabling automated credential stuffing attacks at scale.
  • Regulatory consequences: Data protection frameworks such as GDPR and CCPA impose obligations regarding appropriate technical safeguards. Storing passwords as unsalted hashes — particularly with deprecated algorithms — may constitute a failure of these obligations, resulting in significant fines.

Correct salting practice requires that: (1) a cryptographically secure random number generator produces the salt; (2) the salt is at minimum 16 bytes (128 bits) long; (3) a unique salt is generated for each user at registration and on each password change; and (4) the salt is stored alongside the hash so verification is possible. Modern password hashing libraries such as Python's bcrypt or argon2-cffi handle salt generation and storage automatically within the hash output string, reducing the risk of implementation errors.

# Python example using argon2-cffi
from argon2 import PasswordHasher

ph = PasswordHasher()

# Hashing a password — salt is generated and embedded automatically
hash_value = ph.hash("user_password_here")
# hash_value contains the algorithm, parameters, salt, and digest in one string

# Verifying a password
try:
    ph.verify(hash_value, "user_password_here")
    print("Password is correct")
except Exception:
    print("Password is incorrect")

Timing Attacks on Hash Comparison

A timing attack is a side-channel attack — it does not exploit a weakness in the cryptographic algorithm itself, but rather in the way the algorithm's output is used in code. The vulnerability arises in how most programming languages implement string or byte comparison.

Standard equality comparison (such as Python's == operator on strings, or C's strcmp) is short-circuit: it compares characters left to right and returns False the moment it finds a mismatch. This means a comparison between "abcdef" and "aXYZWV" returns faster than a comparison between "abcdef" and "abcdeX", because the latter matches five characters before finding a mismatch while the former fails on the second character. The execution time therefore leaks information about how many leading characters match.

In the context of token or HMAC verification, an attacker who can make repeated requests and measure response times (even across a network, using statistical averaging over many samples) can deduce the correct value one byte at a time. For a 32-byte HMAC, this reduces the verification space from 25632 to 256 × 32 — a catastrophic reduction in security.

The defense is mandatory use of constant-time comparison functions, which always compare every byte of both inputs regardless of where a mismatch is found, so execution time does not vary with the data:

import hmac

# CORRECT: constant-time comparison
def verify_token(user_supplied: bytes, expected: bytes) -> bool:
    return hmac.compare_digest(user_supplied, expected)

# INCORRECT: short-circuit comparison leaks timing information
def verify_token_insecure(user_supplied: bytes, expected: bytes) -> bool:
    return user_supplied == expected  # Do not use this for security comparisons

Any system that compares user-supplied values against stored secrets — including session tokens, CSRF tokens, API keys, HMACs, and password reset tokens — must use constant-time comparison. This is true even when the secret values are already hashed: if the comparison itself leaks timing information, an attacker can exploit it regardless of the algorithm used to produce the values being compared.

Dictionary and Wordlist Attacks

A dictionary attack is a targeted form of brute-force that exploits human psychology rather than mathematical exhaustion. Because users overwhelmingly choose passwords based on meaningful words, patterns, and keyboard sequences, attackers can recover a disproportionate number of passwords by testing only a curated list of candidates rather than the full character space.

Modern wordlist attacks are sophisticated far beyond simple word lists. Tools like Hashcat and John the Ripper support rule-based mutation engines that transform base words with common substitutions (a → @, e → 3, s → $), capitalization patterns, number suffixes, and concatenations. A wordlist of 1 million words combined with a comprehensive rule set can generate billions of plausible candidate passwords while still targeting the patterns real users employ. Wordlists derived from previous breaches — including the rockyou.txt list of 14 million passwords leaked in 2009, still widely used today — provide a direct map of real human password behavior.

The interaction between dictionary attacks and slow hashing functions is critical to understand. Against MD5, an attacker testing 10 billion candidates per second can process the entire rockyou.txt list in under two milliseconds. Against Argon2id configured to take 100 milliseconds per hash, the same list requires over 400,000 hours — rendering even dictionary attacks impractical. The following comparison illustrates this:

Scenario Hash Function Time to Test rockyou.txt (14M entries)
Single GPU attacker MD5 (unsalted) < 0.001 seconds
Single GPU attacker SHA-256 (unsalted) ~0.001 seconds
Single GPU attacker bcrypt cost 12 ~19 hours
Single GPU attacker Argon2id (100ms/hash) ~16,000 hours

Password complexity requirements, while valuable, are insufficient on their own because users adapt to them in predictable ways: Password1! satisfies typical complexity rules (uppercase, lowercase, digit, symbol) while appearing in breach-derived wordlists. Security guidance has shifted toward prioritizing length and randomness. NIST SP 800-63B recommends allowing long passphrases (up to 64 characters or more) and checking new passwords against lists of known-compromised passwords rather than imposing arbitrary complexity rules that drive users toward predictable patterns.

A passphrase composed of four or five random common words — sometimes called a Diceware passphrase — provides high entropy (approximately 12–15 bits per word from a large wordlist, yielding 60–75 bits for five words) while being significantly more memorable than a shorter random-character password. Against any realistic dictionary attack, such a passphrase remains secure even against adversaries using fast hash functions, because the combination of random words does not appear in any wordlist.

Taken together, the attacks surveyed here — brute-force, rainbow table, collision, deprecation exploitation, unsalted hashing, timing side-channels, and dictionary attacks — represent the primary ways in which hash-based security fails in practice. Each has a corresponding set of defenses, and robust password hashing implementations address all of them simultaneously: use a slow, memory-hard algorithm such as Argon2id; generate a unique cryptographically random salt per credential; avoid deprecated algorithms like MD5 and SHA-1; and perform all comparisons using constant-time functions. No single measure is sufficient in isolation; defense in depth against the full threat model is the standard of care.

NotesThe topic covers all seven subtopic areas (brute-force, rainbow tables, collisions, weak/deprecated algorithms, unsalted hashing, timing attacks, dictionary attacks) with in-depth explanations, comparative tables, and code examples. The Flame malware case study and SHAttered attack are included as real-world collision examples. NIST SP 800-63B guidance is referenced for password policy recommendations. Python code examples use argon2-cffi and hmac.compare_digest to illustrate correct implementation patterns.