Description
No candidate list this time - a large dictionary file is provided. Read each word, hash it with MD5, and compare it to the stored hash to find the password.
Setup
Download level5.py (the checker script) and dictionary.txt (the wordlist).
Solution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1Understand the script
ObservationThe challenge ships a separate dictionary.txt rather than a hardcoded candidate list, which makes this a classic dictionary attack. Understanding level5.py's existing hash-comparison structure comes before extending it.Open level5.py. The stored MD5 hash is visible. The script needs to be extended to read each line from dictionary.txt, strip whitespace, hash it, and compare.Learn more
This challenge is a full dictionary attack - the most common real-world technique for cracking hashed passwords. Instead of a curated short list, you now have a wordlist file that must be read line by line and each entry tested against the target hash.
Wordlists (also called dictionaries) are files containing commonly used passwords, words, phrases, and variations. The most famous real-world wordlist is rockyou.txt, which contains over 14 million passwords leaked from the RockYou social gaming breach in 2009. Security professionals use it as a baseline for password auditing - if a password appears in rockyou.txt, it is considered trivially crackable.
The key difference from pw-crack-4 is that you do not know the candidate list in advance - you must read it from disk. This is the standard approach for any serious password cracking: maintain a large wordlist file and stream through it programmatically.
Step 2Modify the script to loop through the dictionary file
Observationlevel5.py already has the MD5 comparison but no file-reading loop, and Python's readlines() leaves a trailing newline on every entry. So add a loop that strips each line before hashing it.Open level5.py and add code that reads dictionary.txt line by line. Each line includes a trailing newline character - remove it before hashing. Use line[:-1] (slice off the last character) or line.strip(). When the hash matches, set user_pw to that word and break.Learn more
When Python reads a line from a file, each
lineincludes the trailing newline character (\n). In ASCII, newline is character 10. Hashing a word with the newline attached produces a completely different MD5 than hashing the word alone - so every single comparison fails if you do not remove it first.Two equivalent ways to remove the trailing newline:
line[:-1]- slice notation that takes everything except the last characterline.strip()- removes all leading and trailing whitespace including newlines
The loop to add inside level5.py looks like:
with open('dictionary.txt') as f: lines = f.readlines() for pw in lines: pw = pw[:-1] if hash_pw(pw) == correct_pw_hash: user_pw = pw user_pw_hash = hash_pw(pw) breakThe time complexity of this attack is O(n) where n is the number of words in the dictionary. Professional cracking tools add GPU acceleration and multiple worker threads -
hashcaton a modern GPU can test tens of billions of MD5 hashes per second. The fundamental algorithm is identical.Step 3Run with the correct password
ObservationWith the dictionary loop in place, the script iterates through dictionary.txt and finds '9581' as the matching MD5 preimage. Running it feeds that password into the existing XOR decryption and prints the flag.Once '9581' is identified, use it with the XOR decryption function to print the flag.pythonpython3 level5.pyExpected output
picoCTF{...}What didn't work first
Tried: Running the unmodified level5.py before adding the dictionary-reading loop
The original script never loops over dictionary.txt, so user_pw stays at its default empty value, the hash comparison never matches, and the script exits without a flag. Add the file-reading loop: iterate each line, strip the newline, hash it, and set user_pw when the hash matches.
Tried: Using line.split() instead of line[:-1] or line.strip() to remove the trailing newline before hashing
line.split() splits on all whitespace and returns a list, so pw becomes ['9581'] rather than '9581'. Passing a list to hash_pw() raises a TypeError, because hashlib.md5() wants a string or bytes. Use line[:-1] to slice off the last character, or line.strip(); both return a string with the newline gone.
Learn more
Completing this challenge demonstrates a complete understanding of the dictionary attack pipeline:
- Identify the target hash and the hashing algorithm used
- Select or obtain an appropriate wordlist
- Stream the wordlist, hash each candidate, compare to the target
- Use the recovered password to access the protected resource
The real-world defense against dictionary attacks is salting: prepending or appending a unique random value (the salt) to each password before hashing. The salt is stored alongside the hash. Even if two users have the same password, their hashes are different due to different salts. Salting also defeats precomputed hash tables (rainbow tables) because each salt requires its own precomputed table.
Modern password hashing algorithms like bcrypt and Argon2 automatically handle salting and are designed to be computationally expensive, making them far more resistant to dictionary attacks than raw MD5. If you ever design a system that stores user passwords, always use one of these purpose-built algorithms - never raw MD5 or SHA.
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{h45h_sl1ng1ng_...}
Each line read from a file carries a trailing newline - use line[:-1] or line.strip() to remove it before hashing, otherwise every comparison fails.