Disclaimer: This article is provided strictly for educational purposes and authorized security testing. Only run these techniques against systems you own or have explicit written permission to assess. Unauthorized access to computer systems is illegal in virtually every jurisdiction and can carry severe penalties.
Introduction
When a password database is breached, the only thing standing between the attacker and every user’s plaintext password is the hashing scheme used to store it. A fast general-purpose hash — MD5, SHA-1, or even unsalted SHA-256 — is the wrong tool for this job: these functions are designed to be computed as quickly as possible, which is exactly the opposite of what password storage needs. Modern GPUs and ASICs can compute billions of SHA-256 hashes per second, turning a stolen hash dump into an offline cracking problem that finishes in hours for anything but the longest, most random passwords.
The correct tools are password hashing functions — bcrypt, scrypt, and Argon2 — purpose-built to be slow and, for scrypt and Argon2, memory-hard, meaning they also require a configurable amount of RAM per computation. That memory cost is what makes GPU/ASIC parallelization expensive: an attacker cannot simply run millions of cheap, tiny cores in parallel the way they can against SHA-256, because each guess needs its own large memory buffer. Getting this right is one of the highest-leverage controls in an application’s entire security posture, because it directly determines how bad a database breach actually is.
Attack Prerequisites
Attacking password storage generally requires:
- A leaked password hash database — via SQL injection, a backup exposure, an insider, or a supply-chain compromise.
- Knowledge of the hashing scheme and parameters — usually embedded in the hash string itself (e.g.
$2b$12$...for bcrypt,$argon2id$v=19$m=65536,t=3,p=4$...for Argon2), which tells the attacker exactly how expensive each guess will be. - Cracking infrastructure sized to the algorithm — a rig of GPUs is sufficient against fast hashes (MD5/SHA-1/SHA-256) or even bcrypt at a low cost factor, while scrypt/Argon2 at reasonable memory parameters make GPU cracking economically unattractive because GPU memory bandwidth becomes the bottleneck.
- A wordlist, mask, or rule set (rockyou-derived lists, hashcat/John the Ripper rules) — even a slow hash falls quickly to a weak, common, or reused password.
How It Works
Password cracking is an offline guess-and-check problem: for every candidate password, the attacker computes hash(candidate) (plus the correct salt) and compares it to the stolen hash. The attacker’s cost per guess is exactly the defender’s cost per legitimate login. A fast hash means cheap guesses at massive scale — modern GPU cracking rigs perform on the order of tens of billions of SHA-256 or NTLM hashes per second, which makes even 10-character random passwords crackable in a practical timeframe once the underlying hash is fast.
bcrypt (Provos & Mazieres, 1999) is built on the Blowfish cipher’s key schedule and exposes a tunable cost factor that exponentially increases the number of internal rounds; it is CPU-hard but not memory-hard, which is why it remains crackable at meaningful scale with enough GPU hardware, though far slower than a raw hash. scrypt (Percival, 2009) adds a tunable memory-hardness parameter on top of CPU cost, specifically to defeat custom ASIC and GPU parallelization by forcing each parallel guess to allocate its own large memory block. Argon2 (winner of the 2015 Password Hashing Competition) generalizes this further with independently tunable time, memory, and parallelism parameters, and comes in three variants: Argon2d (maximizes GPU/ASIC resistance via data-dependent memory access, but has side-channel risk), Argon2i (data-independent access, safer against timing side channels, weaker GPU resistance), and Argon2id (a hybrid, and the generally recommended default).
All three schemes bake a unique, random salt into the stored hash automatically, which defeats precomputed rainbow-table attacks by forcing the attacker to crack every hash independently rather than once for the whole database. This is the second property that a bare SHA-256(password) storage scheme lacks entirely unless the developer manually adds and manages a per-user salt — and even then, it is still a fast hash and remains trivially GPU-crackable regardless of salting.
Vulnerable Code / Configuration
The still-common anti-pattern — storing a single fast, unsalted (or app-wide-salted) SHA-256 digest of the password:
import hashlib
def hash_password(password: str) -> str:
# VULNERABLE: SHA-256 is fast by design, unsalted, and not memory-hard
return hashlib.sha256(password.encode()).hexdigest()
def check_password(password: str, stored_hash: str) -> bool:
return hash_password(password) == stored_hash # also not constant-time
PythonA slightly ‘improved’ but still weak variant — adding a static, application-wide salt (or even an HMAC key) does not fix the core problem, because the function is still fast and every user shares the same salt material, so a single GPU rig cracks the whole database in parallel:
APP_SALT = b'static-company-salt' # VULNERABLE: shared across all users
def hash_password(password: str) -> str:
return hashlib.sha256(APP_SALT + password.encode()).hexdigest()
PythonThe correct replacement, using a purpose-built password hashing library (here, argon2-cffi, which implements Argon2id with sane defaults):
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError
ph = PasswordHasher(time_cost=3, memory_cost=65536, parallelism=4) # ~64MB, id variant
def hash_password(password: str) -> str:
return ph.hash(password) # unique salt generated internally
def check_password(password: str, stored_hash: str) -> bool:
try:
ph.verify(stored_hash, password)
return True
except VerifyMismatchError:
return False
PythonWalkthrough / Exploitation
From an assessor’s perspective, the first step after obtaining a hash dump is to identify the scheme from the hash format and pick the matching hashcat mode:
# Identify: bcrypt ($2a$/$2b$/$2y$), scrypt, or argon2 ($argon2id$...)
hashcat --identify dump.txt
BashFast, unsalted/weakly-salted SHA-256 (mode 1400) cracks at enormous speed on consumer GPU hardware — a rockyou-based dictionary-plus-rules attack against a large dump often completes in minutes to hours:
hashcat -m 1400 -a 0 dump_sha256.txt rockyou.txt -r rules/best64.rule
BashAgainst bcrypt (mode 3200), the same attack is dramatically slower per guess because of the cost-factor rounds, but still tractable for weak or common passwords, especially at a low configured cost factor:
hashcat -m 3200 -a 0 dump_bcrypt.txt rockyou.txt -r rules/best64.rule
BashAgainst Argon2id (mode 34000+ depending on hashcat version) with reasonable memory parameters, cracking throughput drops by orders of magnitude again because each guess now competes for GPU memory bandwidth rather than raw compute — this is the entire point of the memory-hardness parameter, and it is why weak/reused passwords are still the practical risk even under Argon2, not the algorithm itself.
Note: Never truncate the password before hashing (a historic bcrypt gotcha: the algorithm silently ignores input past 72 bytes) and never pre-hash a long passphrase with a fast hash before feeding it to bcrypt/Argon2 unless you understand the specific construction — done wrong, pre-hashing can reduce entropy or introduce null-byte truncation bugs.
Opsec: When auditing an application, check the hash prefix/parameters, not just the algorithm name — a bcrypt cost factor of 4 or an Argon2 memory cost of 8MB is functionally not much better than a fast hash against modern cracking hardware; recommend cost parameters calibrated so a single hash takes on the order of 250ms-500ms on the defender’s own hardware.
Detection and Defense
Getting password storage right is mostly a one-time architectural decision:
- Use Argon2id as the default choice for new systems, or bcrypt/scrypt where the platform or compliance requirement dictates it — never a bare hash function (MD5/SHA-1/SHA-256/SHA-512) for passwords.
- Tune cost parameters to your hardware, targeting roughly 250-500ms per hash on production hardware; re-tune periodically as hardware improves (a scheme that was fine in 2015 is not automatically fine today).
- Let the library manage salts — all three algorithms generate and embed a unique random salt per hash automatically; never implement your own salting on top of a fast hash as a substitute.
- Add a server-side pepper (an application-wide secret, stored outside the database, e.g. in a KMS/HSM) as defense-in-depth on top of proper hashing, not as a replacement for it.
- Enforce minimum password length over complexity rules, check against breach corpora (e.g. Have I Been Pwned’s Pwned Passwords API) at registration/change time, and support/encourage MFA so a cracked password alone is not sufficient for account takeover.
- Rehash on login when parameters are upgraded — detect the stored scheme/cost from the hash string and transparently re-hash with current parameters the next time the user successfully authenticates.
Real-World Impact
Breaches involving fast, unsalted, or weakly hashed password databases — LinkedIn’s 2012 unsalted SHA-1 breach and Adobe’s 2013 incident involving weak encryption of passwords are among the most cited examples in the industry — led directly to widespread adoption of bcrypt and, more recently, Argon2 as the default recommendation from OWASP and NIST guidance. Password hash cracking remains one of the most reliable outcomes of a data breach: once a dump is public, weakly hashed credentials are typically cracked within hours by the broader security/cracking community, while properly configured Argon2id or bcrypt databases can remain largely uncracked except for the weakest, most common passwords.
Conclusion
Password storage is not a place to reach for a familiar general-purpose hash function — the entire threat model is an offline attacker with unlimited compute trying to reverse a stolen value, and only purpose-built, slow, memory-hard functions like Argon2id, scrypt, or bcrypt are designed to resist that. Combined with proper per-user salting (handled automatically by these libraries), sane cost parameters tuned to current hardware, and breach-corpus checks at registration, this single control determines whether a database breach is a minor incident or a mass credential-stuffing event.
You Might Also Like
If you found this useful, these related deep-dives cover adjacent techniques and their defenses:



Comments