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.
Setup
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.
nc verbal-sleep.picoctf.net <PORT_FROM_INSTANCE>wc -l cheeses.txt # confirm wordlist downloaded; expect a few hundred lineshead cheeses.txtSolution
Walk me through it- Step 1Understand the hash constructionSalt 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.
- Step 2Build a brute-force scriptIterate 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() PYLearn 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. - Step 3Submit the cheese name and capture the flagOnce 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)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.
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.