Description
This time there are 100 candidate passwords. One of them hashes to the stored MD5 hash. Add a loop to test all 100.
Setup
Download level4.py - it contains a list of 100 candidate passwords and a stored MD5 hash.
Solution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1
Read the script structureObservationI noticed the challenge description mentioned a list of 100 candidate passwords and a stored MD5 hash, which suggested I first needed to understand how level4.py was structured before knowing where to add the loop logic.Open level4.py. There's a list of 100 candidate passwords and a target hash. The script checks one password at a time - modify it to loop through all of them.Learn more
Scaling from 7 to 100 candidates makes manual testing impractical - at 100 entries, you might still manage it manually, but the pattern clearly demands automation. This is a deliberate pedagogical progression: each pw-crack level increases the candidate pool until manual approaches are clearly infeasible, pushing you toward writing code.
Understanding the script's structure before modifying it is essential. Locate:
- Where the candidate list is defined
- Where the stored hash is defined
- Where the password is compared to the hash
- Where the decryption function is called with the correct password
This structural reading skill - understanding how code flows before changing it - is called code comprehension and is fundamental to both software development and security analysis. Never modify code you do not understand.
Step 2
Modify the script to loop through all candidatesObservationI noticed the script already held all needed variables (pw_list, correct_pw_hash, user_pw) but only checked one password, which suggested adding a for loop inside the existing script to iterate over all 100 candidates, hash each one, and set user_pw when a match is found.Open level4.py in a text editor. Add a for loop that iterates over the candidate list (pw_list), hashes each entry with MD5, and when the hash matches the stored hash sets user_pw to that entry and breaks. Then run the script and enter anything at the prompt - the loop will have already found the correct password and set it.Learn more
The key insight is to modify the existing script rather than writing a separate one. The script already has all the variables needed:
pw_list(the 100 candidates),correct_pw_hash(the target), anduser_pw/user_pw_hash(what gets passed to the decryption function). Add the loop before the password prompt so it pre-fillsuser_pwwith the correct entry.The loop to add looks like:
for pw in pw_list: if hash_pw(pw) == correct_pw_hash: user_pw = pw user_pw_hash = hash_pw(pw) breakAfter running the modified script, you can type anything at the password prompt because the loop has already determined the correct password. The decryption proceeds with the pre-filled value and prints the flag.
In real-world password auditing, this same loop logic is the core of tools like John the Ripper and hashcat. The difference is that those tools add GPU acceleration, rule-based mutations, and support for hundreds of hash algorithms. The fundamental algorithm is unchanged.
Step 3
Run the modified scriptObservationI noticed that once the loop pre-fills user_pw with the matching candidate, the XOR decryption function in level4.py would run automatically, which suggested simply executing the modified script to obtain the flag.With the correct password found, the XOR decryption function unlocks and prints the flag.pythonpython3 level4.pyExpected output
picoCTF{...}What didn't work first
Tried: Running the original unmodified level4.py and guessing passwords manually from the list
With 100 candidates there is no prompt that lets you iterate - the script checks a single hardcoded entry or waits for one input and exits. Without the loop pre-filling user_pw, the hash comparison always fails and the decryption function never runs, so you just get a wrong-password message no matter what you type.
Tried: Using hashcat or john to crack the stored MD5 hash directly instead of modifying the script
These tools crack hashes against large external wordlists, not against the 100-entry pw_list embedded in level4.py. Even if hashcat recovers the plaintext, the challenge flag is produced by the script's XOR decryption function which needs user_pw set correctly at runtime - cracking the hash offline does not run that function, so you still have no flag.
Learn more
The XOR decryption used in these pw-crack challenges is a simple symmetric cipher: the flag bytes are XORed with a key derived from the password. XOR has the property that applying it twice with the same key returns the original value -
(A XOR K) XOR K = A- making encryption and decryption the same operation.While XOR is not secure on its own for real encryption (it is trivially breakable with known-plaintext attacks), it appears frequently in CTF challenges and in obfuscated malware as a lightweight way to obscure data. Recognizing XOR-based encoding and knowing how to reverse it is a valuable skill for reverse engineering challenges.
The progression from pw-crack-1 through pw-crack-5 mirrors the real evolution of password security practices: plaintext storage, light obfuscation, small hash lookups, larger hash lookups, and finally full dictionary attacks. Each level requires a slightly more sophisticated approach.
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{fl45h_5pr1ng1ng_...}
Testing 100 MD5 hashes takes milliseconds - the same loop logic scales to millions of entries for real dictionary attacks.