Skip to main content

PW Crack 3 Beginner picoMini 2022 Solution

Crack a hashed password by testing a small set of candidates and use it to unlock the flag.

Published: April 2, 2026Updated: August 13, 2026

Description

One of seven candidate passwords matches the stored MD5 hash. Find which one.

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.

Walk me through it
  1. Step 1Read the candidate list and stored hash
    Observation
    The description mentions seven candidate passwords and a stored MD5 hash. So the file already carries everything needed for a hash comparison, with no brute-force guessing required.
    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.

  2. Step 2Test each candidate with a loop
    Observation
    With only seven candidates and a known MD5 hash in level3.py, a short Python loop over hashlib.md5 finds the match: hash each candidate and compare against 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.
    python
    python3 -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. The data argument must be bytes, 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 hashcat and john (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.

  3. Step 3Run the script with the found password
    Observation
    level3.py holds XOR decryption logic that needs the correct password to unlock level3.flag.txt.enc. So running the script with the confirmed candidate dba8 is the only supported path to the flag.
    Execute level3.py and enter dba8 when prompted. The flag is decrypted and printed.
    python
    python3 level3.py
    bash
    # Enter password: dba8
    What 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 your input and compares it against one specific stored hash, so only the matching candidate unlocks the flag. Every other candidate produces a different MD5 digest and the script reports an incorrect password. 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 with a key derived from the password inside level3.py, not a standard cipher openssl understands, so openssl refuses or returns garbage. The decryption logic and key schedule live in the Python script, so running level3.py with the right password is the only path.

    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.

Key takeaway

A dictionary attack hashes each word in a candidate list and compares the result against a stored hash, exploiting the fact that hash functions are deterministic: the same input always gives the same output. It works whenever the password space is small or predictable, which is why weak or common passwords stay dangerous even when hashed. The defense is a slow, salted algorithm like bcrypt or Argon2, which forces an attacker to spend real time and compute on every candidate rather than billions per second.

Related reading

Useful tools for General Skills

Where to go next