Description
One of seven candidate passwords matches the stored MD5 hash. Find which one.
Setup
Download level3.py and level3.flag.txt.enc from the challenge page.
Solution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1
Read the candidate list and stored hashObservationI noticed the challenge description mentioned seven candidate passwords and a stored MD5 hash, which suggested the file contained all the inputs needed to attempt a hash comparison without any brute-force guessing.Open level3.py - it contains a list of seven candidate passwords and an MD5 hash string that the correct password must produce.Learn more
This challenge introduces the concept of password hashing. Rather than storing the password directly, the script stores an MD5 hash of the correct password. To verify a user's input, the script hashes what they typed and compares the result to the stored hash - the original password never needs to be stored or compared directly.
MD5 produces a 128-bit hash represented as 32 hexadecimal characters. The same input always produces the same output (deterministic), but even a single character change in the input produces a completely different hash (the avalanche effect). This makes it easy to verify passwords without storing them in plaintext.
The weakness here is the small candidate list - with only 7 possible passwords, checking each one is trivial. This is the foundation of a dictionary attack: instead of trying every possible input (brute force), you test a curated list of likely passwords against the stored hash.
Step 2
Test each candidate with a loopObservationI noticed there were only seven candidates and a known MD5 hash in level3.py, which suggested a short Python loop using hashlib.md5 could find the matching password by hashing each candidate and comparing it to the stored digest.Write a short Python loop that hashes each candidate with hashlib.md5 and compares the hex digest to the stored hash. The correct password is dba8.pythonpython3 -c " import hashlib hash_val = '...stored_hash...' candidates = ['f09e','4dcf','87ab','dba8','752e','3961','f159'] for pw in candidates: if hashlib.md5(pw.encode()).hexdigest() == hash_val: print('Password:', pw) "Expected output
Password: dba8
What didn't work first
Tried: Pass the candidate string directly to hashlib.md5 without calling .encode()
Python 3 raises TypeError: Strings must be encoded before hashing. hashlib.md5 only accepts bytes, not str. Wrapping the candidate in .encode() converts it to a UTF-8 bytes object, which is what the script expects when it hashes the user input.
Tried: Compare the result of .digest() to the stored hash string instead of .hexdigest()
.digest() returns raw bytes (e.g. b'\xdb\xa8...'), while the stored hash in level3.py is a lowercase hex string like 'dba8...'. The comparison always fails even for the correct password. Use .hexdigest() to get the same 32-character hex string representation used in the file.
Learn more
hashlib is Python's standard library for cryptographic hashing. The call
hashlib.md5(data)creates an MD5 hash object; calling.hexdigest()on it returns the lowercase hex string. Thedataargument must bebytes, so string passwords require.encode()(which defaults to UTF-8).The loop pattern here is the essence of a dictionary attack:
- Take each candidate from the list
- Hash it with the same algorithm used to create the stored hash
- Compare the result to the target hash
- Stop when a match is found
The same logic powers tools like
hashcatandjohn(John the Ripper), which can test billions of candidates per second using GPUs. The only difference is scale - the algorithm is identical to what you are writing here.Step 3
Run the script with the found passwordObservationI noticed level3.py contained XOR decryption logic that required the correct password to unlock level3.flag.txt.enc, which suggested running the script with the confirmed candidate dba8 was the only supported path to retrieve the flag.Execute level3.py and enter dba8 when prompted. The flag is decrypted and printed.pythonpython3 level3.pybash# Enter password: dba8What didn't work first
Tried: Run level3.py and enter one of the other six candidates (e.g. f09e) to see if any extra passwords also work
The script hashes the input and compares it to one specific stored hash, so only the single matching candidate unlocks the flag. All other candidates produce a different MD5 digest and the script prints an incorrect password message. Only dba8 hashes to the stored value.
Tried: Try to decrypt level3.flag.txt.enc directly with a tool like openssl or xxd without running the script
The file is XOR-encrypted using a key derived from the password inside level3.py, not a standard cipher openssl understands. openssl will refuse or produce garbage output. The decryption logic and key schedule live inside the Python script itself, so the only supported path is running level3.py with the correct password.
Learn more
With the correct password in hand, the script's XOR decryption routine unlocks the encrypted flag. This two-step structure - hash verification then decryption - mirrors how real password-protected systems work: the password is first verified (by hashing), then used to derive a decryption key for the actual protected data.
Why MD5 is no longer secure for passwords: MD5 is fast - a modern GPU can compute billions of MD5 hashes per second. This means a dictionary or brute-force attack against MD5-hashed passwords can succeed very quickly. Modern password storage uses deliberately slow algorithms like bcrypt, Argon2, or PBKDF2, which make each hash computation expensive and slow down attacks by many orders of magnitude.
Interactive tools
- Strings ExtractorPull printable text from any binary, library, or image. ASCII and UTF-16 detection, configurable minimum length, flag-like highlight, no command line needed.
- Hex ViewerView text or raw hex bytes as a xxd-style hex dump with byte offset, hex columns, and ASCII sidebar. Highlights printable characters and null bytes.
- Hash IdentifierIdentify unknown hash types by length and prefix. Covers MD5, SHA-1, SHA-256, SHA-512, bcrypt, NTLM, and more.
Flag
Reveal flag
picoCTF{m45h_fl1ng1ng_...}
With only 7 candidates, even manual testing is feasible - but scripting it demonstrates the dictionary attack approach used in real-world password cracking at scale.