Skip to main content

Guess My Cheese (Part 2) picoCTF 2025 Solution

The service salts and hashes each cheese name, so brute-force the wordlist against every salt until a digest matches.

Published: April 2, 2025Updated: August 25, 2026

Description

Part 2 replaces the Affine cipher from Guess My Cheese Part 1 with SHA-256 hashing plus a weak salt. The service gives you a hash and you must find the cheese name and 1-byte salt that produce it.

Launch the instance, connect over netcat, and copy the target hash to a local file. From there the workflow is offline: disconnect, brute locally, reconnect to submit.

The hash construction is sha256(cheese_name + salt) (or salt-then-cheese), where the salt is a 1-byte value rendered as a 2-character lowercase hex string (e.g., 0xAB becomes "ab").

Download the cheese wordlist from the challenge resources, then confirm it actually downloaded (wc -l cheeses.txt) before launching the brute-force.

bash
nc verbal-sleep.picoctf.net <PORT_FROM_INSTANCE>
bash
wc -l cheeses.txt   # confirm wordlist downloaded; expect a few hundred lines
bash
head cheeses.txt

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
1-byte salts are why we use bcrypt; the Hash Cracking for CTF guide covers wordlist + rule attacks against the same kind of construction.
  1. Step 1Understand the hash construction
    Observation
    The description says SHA-256 with a weak salt, and the salt is one byte written as two hex characters. Work out the search space first: the wordlist size times 256.
    Salt format: 1-byte value rendered as a 2-character lowercase hex string (e.g., 0xAB becomes the literal string "ab"). That's only 256 possible salts. Combined with the finite cheese wordlist, the total search space is tiny - brute-force offline.
    Learn more

    Salt in cryptographic hashing is extra random data added to a value before hashing, preventing precomputed rainbow table attacks. A salt must be sufficiently long and random to be effective. A 1-byte (8-bit) salt only has 256 possible values, so precomputing a table for all cheeses × 256 salts is trivial. This challenge illustrates why real password hashing uses salts of at least 16 bytes (e.g., bcrypt, scrypt, and Argon2 all generate salts of 16+ bytes by default).

    The salt in this challenge is represented as a 2-character lowercase hex string - for example, byte value 0xAB becomes the string "ab". This means the hash input is not raw bytes but ASCII characters, so you must format the salt as a hex string when reconstructing the input to hash.

    SHA-256 is a cryptographic hash function that produces a fixed 256-bit (32-byte) output. It is computationally infeasible to reverse directly: given only the hash, finding the input requires trying every possible value. However, when the input space is small (a known wordlist × 256 salts), brute-force enumeration is practical and takes milliseconds on modern hardware.

  2. Step 2Build a brute-force script
    Observation
    A few hundred cheeses times 256 salts is nothing, and the salt could sit before or after the name. Script both orderings and compare every digest against the target.
    Iterate every cheese in the wordlist crossed with every salt value 0x00 through 0xff. The script tries the salt at the start and at the end of the cheese name, hashes each candidate with SHA-256, and compares to the target. Lowercase the target hash up front so a case mismatch never silently fails.
    python
    python3 - <<'PY'
    import hashlib
    
    # Paste the hash from the service here:
    target_hash = 'PASTE_TARGET_HASH_HERE'.lower()
    
    # Load the cheese wordlist (download from challenge page)
    with open('cheeses.txt') as f:
        cheeses = [line.strip() for line in f]
    
    def try_hash(data: str) -> str:
        return hashlib.sha256(data.encode()).hexdigest()
    
    # Salt is at start or end. The script tries both.
    for cheese in cheeses:
        for salt_byte in range(256):
            salt_hex = format(salt_byte, '02x')
            for candidate in [cheese + salt_hex, salt_hex + cheese]:
                if try_hash(candidate) == target_hash:
                    print(f'Found! cheese={cheese!r}, salt={salt_hex!r}')
                    print(f'Input was: {candidate!r}')
                    exit()
    PY

    Expected output

    Found! cheese='Gouda', salt='3f'
    Input was: 'Gouda3f'
    What didn't work first

    Tried: Hash only one arrangement - either cheese + salt or salt + cheese - and give up when no match is found.

    Nothing says which order the server uses, so trying one silently skips half the search space. The script then exits printing nothing, as if the cheese were missing from the wordlist. Cover both orders in the same loop.

    Tried: Pass the raw salt byte as a single character using chr(salt_byte) instead of formatting it as a 2-character hex string.

    The salt is a lowercase hex string like '3f', not the raw byte it represents. Hashing with the byte instead of the two characters gives a different digest for every candidate and the script finds nothing. Format the value as two hex characters, which is what the server concatenates.

    Learn more

    Python's hashlib.sha256(data.encode()).hexdigest() hashes a string and returns the hex-encoded digest, exactly what you need to compare against the challenge's hex hash output. The .encode() converts the Python string to UTF-8 bytes before hashing, which matches how the challenge server constructs its inputs.

    format(salt_byte, '02x') formats an integer as a zero-padded 2-character lowercase hex string: 0 → "00", 10 → "0a", 255 → "ff". This matches the 2-nibble hex format the challenge uses for salts. Case matters for hashing - "ab" and "AB" are different strings and will produce different hashes, so stick to lowercase unless you have reason to believe the challenge uses uppercase.

    The script tries the two positions the challenge actually uses (salt at start, salt at end). If neither matches, the rule of thumb is to extend the loop to insert the salt at every index (0 through len(cheese)) and re-run. Real password managers sometimes interleave the salt for marginal extra entropy - with a 1-byte salt, this barely matters against a targeted attack.

  3. Step 3Submit the cheese name and capture the flag
    Observation
    The script prints the cheese name and the hex salt, which are exactly the two things the service asks for. Reconnect and enter them.
    Once the script finds the matching cheese and salt, connect to the service, enter the cheese name when prompted, and then provide the hex salt value. The server verifies the match and prints the flag.
    bash
    nc verbal-sleep.picoctf.net <PORT_FROM_INSTANCE>
    bash
    # Enter cheese name: Gouda  (example, exactly as it appears in the wordlist)
    bash
    # Enter salt: 3f          (example hex value)
    Learn more

    The challenge demonstrates the offline attack model: the attacker does not need live access to the hashing server during the search phase. Once you have the hash (from one connection), you can run the brute-force locally at full CPU speed, then connect again with the answer. For a wordlist of ~1000 cheeses × 256 salts = 256,000 candidates, even a slow laptop finds the answer in under a second.

    This is precisely why modern password storage uses slow hashing functions like bcrypt, scrypt, or Argon2 instead of raw SHA-256. These algorithms are deliberately expensive to compute (tunable to take ~100ms per hash), which makes offline brute-force attacks impractical even with a large wordlist and full GPU resources. SHA-256 can be computed at billions of hashes per second on modern hardware; bcrypt at the same security setting takes orders of magnitude longer.

Interactive tools
  • Hash IdentifierIdentify unknown hash types by length and prefix. Covers MD5, SHA-1, SHA-256, SHA-512, bcrypt, NTLM, and more.
  • Checksum CalculatorCompute CRC32, MD5, SHA-1, SHA-256, SHA-384, and SHA-512 hashes for text or uploaded files. Verify against known hashes.

Flag

Reveal flag

picoCTF{ch33s3_h45h_...}

Build a brute-force of all cheeses x 256 hex salts (append and prepend), compare SHA-256 hashes against the target, then submit the matching cheese and salt.

Key takeaway

A salt defeats precomputed rainbow tables only when it is long and random enough to make precomputation pointless. One byte gives 256 values, so every word in a known list can be hashed against all of them in milliseconds on an ordinary laptop. Real password storage uses 16 bytes or more with a deliberately slow function like bcrypt or Argon2, tuned to cost hundreds of milliseconds per attempt, which keeps offline brute force uneconomical even on GPUs.

Related reading

Useful tools for Cryptography

Where to go next