No FA picoCTF 2026 Solution

Published: March 20, 2026

Description

Seems like some data has been leaked! Can you get the flag?

Launch the challenge instance and open the web application.

The site appears to have a login protected by two-factor authentication (2FA/OTP).

  1. Step 1Crack the leaked MD5 password hash
    Two ways to crack it: paste the hash into CrackStation (instant lookup against precomputed tables of common passwords like 'apple@123'), or run hashcat against rockyou.txt for offline brute-force. CrackStation wins on common passwords; hashcat wins on uncommon ones or when you need to keep going past the wordlist.
    bash
    # Method 1 (fastest for common passwords): paste hash into crackstation.net
    bash
    # Hash: c20fa16907343eef642d10f0bdb81bf629e6aaf6c906f26eabda079ca9e5ab67
    bash
    # Result: apple@123  (instant - it's in their precomputed table)
    bash
    bash
    # Method 2 (offline, works on any hash): hashcat with rockyou wordlist
    bash
    echo 'c20fa16907343eef642d10f0bdb81bf629e6aaf6c906f26eabda079ca9e5ab67' > hash.txt
    bash
    hashcat -m 0 hash.txt /usr/share/wordlists/rockyou.txt
    bash
    # -m 0 = MD5; -m 100 = SHA1; -m 1400 = SHA256, etc.
    Learn more

    MD5 (Message Digest 5) was widely used for password storage throughout the 1990s and 2000s, but it is completely unsuitable for this purpose today. Three properties make it dangerous for passwords:

    • Speed: A modern GPU can compute billions of MD5 hashes per second, making brute-force trivially fast
    • No salt: Without a random per-user salt, identical passwords produce identical hashes, enabling rainbow table lookups
    • Cryptographic weaknesses: MD5 is vulnerable to collision attacks (though this doesn't directly help crack passwords, it undermines trust in the algorithm)

    CrackStation maintains a precomputed lookup table (rainbow table) of MD5 and other hash types for billions of known passwords. Common passwords like apple@123 appear in leaked password databases and are in these tables. hashcat performs the same lookup dynamically against wordlists like rockyou.txt (a real credential dump from the 2009 RockYou breach containing 14 million passwords).

    Modern password storage uses purpose-built password hashing functions: bcrypt, scrypt, Argon2 (winner of the Password Hashing Competition), and PBKDF2. These are deliberately slow and memory-intensive, making brute-force attacks orders of magnitude harder. They also include per-user salts automatically, preventing rainbow table attacks.

  2. Step 2Log in and reach the 2FA page
    Log in, then read the OTP input field's max length / placeholder / pattern attribute to confirm the OTP range. If the UI gives nothing, infer from error messages on out-of-range guesses, or assume the worst case and brute the full 000000-999999 6-digit space.
    bash
    curl -c cookie.jar -d 'username=admin&password=apple@123' http://<HOST>:<PORT_FROM_INSTANCE>/login
    bash
    # Inspect the OTP form to determine the range:
    bash
    curl -b cookie.jar http://<HOST>:<PORT_FROM_INSTANCE>/2fa | grep -A2 -i otp
    Learn more

    Two-factor authentication (2FA) adds a second verification step after the password check, requiring something you have (a phone, hardware token) in addition to something you know (the password). OTP (One-Time Password) systems generate a time-based (TOTP) or counter-based (HOTP) code that is valid for a short window. The industry standard is TOTP (RFC 6238), which uses a shared secret and the current time to generate a 6-digit code that changes every 30 seconds.

    The challenge OTP range (1000-9999) is much smaller than a real 6-digit OTP (000000-999999 = 1,000,000 values). This reduction makes brute-force feasible: only 9,000 possible values exist. A real TOTP system would be effectively unbrute-forceable in a single 30-second window even with fast requests, which is why proper 2FA provides strong authentication even if the password is known.

    The curl -c cookie.jar flag saves session cookies to a file, which can be reloaded with -b cookie.jar on subsequent requests. This simulates a browser session where the server recognises you as logged in across multiple HTTP requests. Cookie management is essential for automating interactions with session-based web applications.

  3. Step 3Brute-force the OTP (1000-9999)
    max_workers=50 is an aggressive starting point. Drop to 5-10 if you see HTTP 429 or rate-limit headers; raise toward 100 if no rate limiting is in evidence. Watch the first hundred responses, then tune.
    python
    python3 << 'EOF'
    import requests
    from concurrent.futures import ThreadPoolExecutor
    
    BASE = "http://<HOST>:<PORT_FROM_INSTANCE>"
    
    session = requests.Session()
    session.post(f"{BASE}/login", data={"username": "admin", "password": "apple@123"})
    
    def try_otp(otp):
        r = session.post(f"{BASE}/verify", data={"otp": str(otp)})
        if "picoCTF" in r.text or "Correct" in r.text:
            print(f"Correct OTP is: {otp}")
            print(r.text)
            return True
        return False
    
    with ThreadPoolExecutor(max_workers=50) as ex:
        futures = {ex.submit(try_otp, otp): otp for otp in range(1000, 10000)}
        for f in futures:
            if f.result():
                ex.shutdown(wait=False, cancel_futures=True)
                break
    EOF
    Learn more

    OTP brute-forcing is only feasible when the OTP space is small and there is no rate limiting on the verification endpoint. In production 2FA systems, brute-force is prevented by: short validity windows (30 seconds for TOTP), rate limiting (lock after 5-10 failed attempts), and alert/notification systems that warn the user of repeated failed 2FA attempts.

    Python's ThreadPoolExecutor with max_workers=50 sends 50 concurrent HTTP requests at a time, dramatically reducing the total time to test 9,000 values. A single-threaded loop would take much longer; 50 threads reduces wall-clock time roughly 50x. The tradeoff is that multithreaded requests may trigger rate limiting faster, but since this challenge has no 2FA rate limiting, parallelism is pure benefit.

    This challenge illustrates why 2FA implementations must protect the OTP endpoint with the same (or stricter) rate limiting as the password endpoint. A common real-world vulnerability is an application that rate-limits password guessing but not OTP guessing, allowing an attacker who has stolen a password to brute-force the 2FA code. This is a recognised attack described in OWASP's Authentication Cheat Sheet. See the hash cracking guide for the offline-cracking side and the web bug patterns post for more on auth-flow defects.

Flag

picoCTF{n0_f4_n0_pr0bl3m_...}

Leaked MD5 hash (c20fa1...) cracks to 'apple@123'. The 2FA OTP is in range 1000-9999 - brute-force with 50 concurrent workers to find the correct OTP quickly.

How to prevent this

2FA only adds security if the OTP path is hardened the same way as the login path.

  • Use the full 000000-999999 6-digit space, generated from a CSPRNG or HOTP/TOTP secret. Never accept a 4-digit OTP for anything that protects real value.
  • Rate-limit the OTP endpoint per account (not per IP), with exponential backoff and a lockout after 5-10 failures. Brute force at 50 RPS should be impossible.
  • Bind the OTP to a single login attempt: invalidate it after one wrong guess instead of letting the attacker keep trying within the 30-second window. Notify the user on repeated failures.

Want more picoCTF 2026 writeups?

Useful tools for Web Exploitation

Related reading

What to try next