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
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1
Understand the hash constructionObservationI noticed the challenge description explicitly stated 'SHA-256 hashing plus a weak salt' and that the salt is a single byte rendered as a 2-character hex string, which suggested I should first calculate the total search space (wordlist size times 256) before deciding on a brute-force strategy.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.
Step 2
Build a brute-force scriptObservationI noticed the total search space was only a few hundred cheeses times 256 possible 1-byte salt values, and that the salt could appear before or after the cheese name, which suggested writing a Python script to try both orderings exhaustively and compare each SHA-256 digest against the target hash.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.pythonpython3 - <<'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() PYExpected 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.
The challenge does not document which order it uses, so skipping one arrangement silently misses half the search space. The script exits without printing anything, making it look like the cheese is simply not in the wordlist. Trying both orders in the same loop guarantees a hit if the inputs are correct.
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 challenge represents the salt as a lowercase hex string such as '3f', not as the raw byte with ASCII value 63. Hashing cheese + chr(63) produces a completely different digest than hashing cheese + '3f', so every candidate mismatches and the script finds nothing. Using format(salt_byte, '02x') produces the 2-character hex string the server actually 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.Step 3
Submit the cheese name and capture the flagObservationI noticed the brute-force script printed the matching cheese name and its hex salt, which are exactly the two values the service prompts for, suggesting I reconnect via netcat and enter them to receive the flag.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.bashnc 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.
Interactive tools
- Cipher Identifier & Auto-DecoderPaste any ciphertext and the tool auto-runs every common decoder (base64, hex, Morse, ROT, Atbash, Bacon, binary, decimal, URL) and ranks the results by English-likeness.
- Frequency AnalysisAnalyze letter frequencies in a substitution cipher and interactively build the decryption mapping with auto-filled guesses.
- ROT / Caesar CipherDecode Caesar-shifted and ROT-encoded text. Drag the shift slider or scan all 26 rotations at once.
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.