Browse CTFs New CTF Sign in

bcrypt Pepper Exposure Analysis: Reconstructing Hash Inputs from Leaked Secret Values

encoding_crypto_classical Difficulty 1–5 30 min certifiable

Theory

Cryptanalysis Methodology

bcrypt is a password hashing function designed by Niels Provos and David Mazières (1999) that incorporates a work factor (cost parameter) to make brute-force attacks computationally expensive. The bcrypt output format is $2b$COST$SALT_AND_HASH where COST is a log2 value (default 12 → 2^12 = 4096 iterations), SALT is a 22-character Base64url string encoding 16 random bytes, and HASH is a 31-character Base64url string encoding the 24-byte output.

Peppering is an additional secret mixed into the password before hashing. Unlike a salt (which is stored alongside the hash and is unique per user), a pepper is a fixed application-wide secret stored outside the database — typically in an environment variable, a secrets manager, or application source code. The goal is to make hash cracking infeasible even if the database is compromised, since without the pepper the attacker cannot verify candidate passwords.

Common pepper implementations: - bcrypt(password + pepper) — pepper appended as a string suffix - bcrypt(pepper + password) — pepper prepended as a string prefix - bcrypt(hmac(key=pepper, msg=password)) — HMAC pre-processing (more principled) - bcrypt(sha256(password + pepper).hex()) — digest pre-processing

bcrypt input truncation. A critical constraint: bcrypt truncates its input at 72 bytes. If password + pepper exceeds 72 bytes, the excess is ignored. A long pepper may effectively cancel itself out for long passwords. This is why the HMAC-then-bcrypt pattern (bcrypt(hmac(...))) is preferred: the HMAC output is always 32 bytes, well within the 72-byte limit.

CTF attack scenarios. In CTF challenges involving bcrypt pepper: 1. The pepper is exposed in a leaked .env file, a git history commit, a Docker image layer, or an error log. 2. The analyst reconstructs the hash input by prepending/appending the pepper to candidate passwords and running bcrypt verification. 3. Standard hashcat cannot crack peppered bcrypt directly because the pepper modification is not a built-in rule; a custom wrapper script or hashcat plugin is required.

Technical Deep-Dive

import bcrypt, hmac, hashlib

# Verify a peppered bcrypt hash (pepper appended)
def verify_peppered_bcrypt_append(password: str, pepper: str, stored_hash: bytes) -> bool:
    input_bytes = (password + pepper).encode("utf-8")
    return bcrypt.checkpw(input_bytes, stored_hash)

# Verify with pepper prepended
def verify_peppered_bcrypt_prepend(password: str, pepper: str, stored_hash: bytes) -> bool:
    input_bytes = (pepper + password).encode("utf-8")
    return bcrypt.checkpw(input_bytes, stored_hash)

# Verify HMAC-then-bcrypt pattern
def verify_hmac_bcrypt(password: str, pepper: str, stored_hash: bytes) -> bool:
    mac = hmac.new(pepper.encode(), password.encode(), hashlib.sha256).digest()
    return bcrypt.checkpw(mac, stored_hash)

# CTF: crack peppered bcrypt given a wordlist and leaked pepper
def crack_peppered(
    wordlist_path: str,
    stored_hash: bytes,
    pepper: str,
    mode: str = "append",   # "append" | "prepend" | "hmac"
) -> str | None:
    with open(wordlist_path, encoding="utf-8", errors="ignore") as f:
        for line in f:
            pw = line.strip()
            if mode == "append":
                match = verify_peppered_bcrypt_append(pw, pepper, stored_hash)
            elif mode == "prepend":
                match = verify_peppered_bcrypt_prepend(pw, pepper, stored_hash)
            else:
                match = verify_hmac_bcrypt(pw, pepper, stored_hash)
            if match:
                return pw
    return None
# Check if a .env file or git history leaks the pepper
git log --all --full-history -- "**/.env" "**/*.env" | head -40
git show HEAD~5:.env 2>/dev/null | grep -i pepper

# Extract all environment variable names from Docker image layers
docker save myimage:latest | tar -xO --wildcards "*/layer.tar" 
  | tar -xO ./app/.env 2>/dev/null

# Crack bcrypt with known pepper using a Python wrapper (hashcat mode 3200 = bcrypt)
# hashcat does not support pepper natively; use this wrapper:
python3 -c "
import bcrypt, sys
stored = sys.argv[1].encode()
pepper = sys.argv[2]
for pw in open(sys.argv[3]):
    pw = pw.strip()
    if bcrypt.checkpw((pw+pepper).encode(), stored):
        print(pw); break
" '$2b$12$hashhere' 'leaked_pepper_value' /usr/share/wordlists/rockyou.txt

Common Cryptanalysis Errors

1. Assuming bcrypt is unbreakable without a pepper. Standard bcrypt with cost 12 is slow (about 0.3 seconds per hash on a CPU), but GPU crackers achieve ~20,000 hashes/second on modern hardware (hashcat mode 3200, RTX 4090). Common passwords remain crackable from rockyou.txt without a pepper.

2. Forgetting the 72-byte truncation. If the pepper is 50 bytes and the password is 30 bytes, the bcrypt input is only the first 72 bytes of the concatenation — the last 8 bytes of the password are silently discarded. This creates a security vulnerability: passwords that differ only in their final bytes hash identically.

3. Not trying both prepend and append. If the pepper is known but verification fails, try the other orientation. Some codebases prepend; others append. Also try lowercase/uppercase variants of the pepper in case of transcription errors.

4. Using bcrypt on pre-hashed input incorrectly. Some developers do bcrypt(md5(password)) to bypass the 72-byte limit, but MD5 output is hex-encoded (32 printable characters) which dramatically reduces the entropy passed to bcrypt. The correct approach is to hex-encode the HMAC-SHA256 output (64 characters, all within bcrypt's limit) or use bcrypt directly on the raw HMAC bytes.

5. Ignoring Docker layer history as an exposure vector. Pepper values set as ENV instructions in a Dockerfile are visible in docker history --no-trunc even if they were overridden in a later layer. Always check image layer history when assessing pepper exposure.

Challenge Lab

Reinforce your learning with a hands-on generated challenge based on this card's competency.