Skip to main content

Credential Stuffing picoCTF 2026 Solution

Try a set of leaked credentials against a login service to find the one that grants access to the flag.

Published: March 20, 2026Updated: September 22, 2026

Description

Credential stuffing is the automated injection of stolen username and password pairs into website login forms, in order to fraudulently gain access to user accounts. Download the credentials dump creds-dump.txt.

Download creds-dump.txt - it contains username;password pairs from a data breach.
Launch the challenge instance. The service is a raw TCP server that prompts for a username then a password - connect with nc HOST PORT to see the prompts before scripting.
bash
wc -l creds-dump.txt
bash
head creds-dump.txt
bash
nc HOST PORT

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
  1. Step 1Inspect the credentials dump
    Observation
    The challenge ships creds-dump.txt. Check its format and delimiter before scripting anything, and connect once by hand to see what prompts the server sends.
    The dump format is one credential per line, semicolon-delimited: username;password. Connect manually with nc to confirm the server sends a 'Username:' prompt, then a 'Password:' prompt, and then either a welcome message with the flag or an error.
    bash
    head -20 creds-dump.txt
    bash
    nc HOST PORT
    What didn't work first

    Tried: Splitting each line on a colon instead of a semicolon because many credential dumps use colon-separated format.

    That split either finds no separator and leaves the line intact, or drops the whole line into the username field. Reading the first few lines shows the delimiter is a semicolon, so split on that.

    Tried: Skipping the manual nc probe and going straight to scripting, assuming the server uses HTTP POST to a /login endpoint.

    This is a raw TCP server, not an HTTP service: no URL, no content type, no JSON body. Send a POST payload and the server reads garbage and closes the connection. One netcat session shows the plain-text prompt and the real wire protocol.

    Learn more

    Credential stuffing is a cyberattack where stolen username/password pairs from one data breach are tested against other services. It works because a large fraction of users reuse passwords across multiple sites. Major breaches - RockYou (2009), LinkedIn (2012), Collection #1 (2019) - have exposed billions of credentials, creating a vast corpus that attackers maintain and trade.

    Dumps come in multiple formats: colon-separated (username:password), semicolon-separated (what this challenge uses), tab-separated, or JSON. Always inspect the first few lines before scripting so the parser uses the right delimiter. The wc -l command counts entries to understand the scale. Real-world dumps contain millions of entries; this challenge's dump is small enough to iterate sequentially in a few minutes.

    HaveIBeenPwned's Pwned Passwords API is the canonical defensive tool here, and it uses a clever trick called k-anonymity: instead of sending the password to the API, the client computes SHA-1(password), sends only the first 5 hex characters of that hash to https://api.pwnedpasswords.com/range/{prefix}, and receives back every leaked hash that starts with that prefix (roughly 500-800 hashes per prefix). The client then compares the rest of the hash locally. The full password and full hash never leave the user's machine.

    From a defensive perspective, services layer rate limiting, CAPTCHA, IP reputation checks, multi-factor authentication, and breach-password rejection at signup. See the Web Challenges and Real-World Bug Patterns post for adjacent auth bugs and the Hash Cracking for CTF post for the offensive side of password hashing.

  2. Step 2Automate credential stuffing over TCP
    Observation
    The instance is a raw TCP port, not an HTTP endpoint, and the dump holds many pairs to try. Script a socket client that walks the file and sends each username and password until the flag comes back.
    Write a Python script that opens a new socket connection per credential pair, reads the server's prompts, sends the username and password, then checks the response for the flag. Because each pair requires a full TCP handshake and the dump can be large, add a small sleep between attempts to avoid overwhelming the server. When the response contains 'picoCTF', print it and stop.
    python
    python3 << 'EOF'
    import socket
    import time
    
    HOST = "HOST"
    PORT = PORT
    
    with open("creds-dump.txt", encoding="utf-8", errors="ignore") as f:
        for line in f:
            if ";" not in line:
                continue
            try:
                username, password = line.strip().split(";", 1)
            except ValueError:
                continue
    
            print(f"Trying {username}:{password}")
            try:
                s = socket.socket()
                s.connect((HOST, PORT))
                s.recv(1024)                        # Username: prompt
                s.send((username + "\n").encode())
                s.recv(1024)                        # Password: prompt
                s.send((password + "\n").encode())
                response = s.recv(4096).decode(errors="ignore")
                s.close()
    
                if "picoCTF" in response:
                    print("\n[+] FLAG FOUND [+]")
                    print(response)
                    break
    
                time.sleep(0.5)
            except Exception as e:
                print(f"Connection error: {e}, retrying...")
                time.sleep(1)
    EOF

    Expected output

    [+] FLAG FOUND [+]
    picoCTF{d0nt_r3u5e_cr3d3nt1als_...}
    What didn't work first

    Tried: Using split(';') without a maxsplit argument and then indexing [0] and [1] to extract username and password.

    A password containing a semicolon splits into three parts, and taking the second one quietly drops the rest. The script sends a truncated password that never matches and exhausts the dump. Limiting the split to one captures the full password regardless.

    Tried: Removing the s.recv(1024) calls before each send to speed up the loop, reasoning that the data will still be sent to the server.

    The server simply reads two newline-terminated lines, so it does not care when you read its prompts. The reason to drain them is on your side: skip the recv() calls and the prompt bytes stay queued in your socket, so the read you expect to hold the login result returns 'Username: Password: ' instead and your success check never matches. Drain both prompts before sending each pair.

    Learn more

    The service is a classic interactive TCP server: it accepts a connection, sends a text prompt, reads a line, sends another prompt, reads another line, then replies. This pattern is common in CTF challenges that simulate login terminals. Unlike an HTTP web form, there is no URL to POST to - the protocol is defined purely by the text prompts and newline-delimited responses on the raw socket.

    The socket.recv(1024) calls consume the server's prompt before sending the next input. Skipping them would leave unread data in the socket buffer and confuse the state machine on subsequent reads. The split(";", 1) with maxsplit=1 is important: passwords may contain semicolons, so splitting only on the first one ensures the password field is never truncated.

    The sequential, one-socket-per-pair approach is slower than threaded approaches but simpler to reason about and less likely to flood the server. If the dump is large and speed matters, the same logic can be wrapped in concurrent.futures.ThreadPoolExecutor with a small max_workers value (2-5) to run a few probes in parallel while staying within typical CTF server limits.

    Real credential stuffing tools like Sentry MBA or Openbullet add features like proxy rotation, CAPTCHA solving services, and result categorization. This challenge simulates the core mechanic: automated testing of a stolen credential list against a live service to find the one user who reused their password.

Interactive tools
  • Hash IdentifierIdentify unknown hash types by length and prefix. Covers MD5, SHA-1, SHA-256, SHA-512, bcrypt, NTLM, and more.
  • JWT DecoderDecode JSON Web Tokens and inspect the header, payload, and signature. Useful for web exploitation challenges.
  • Flask Session DecoderDecode Flask / itsdangerous session cookies. Splits payload, decompresses zlib, parses JSON, and verifies the HMAC signature when given the secret.

Flag

Reveal flag

picoCTF{d0nt_r3u5e_cr3d3nt1als_...}

One credential pair in the dump is valid for the TCP service. The script finds it by trying each pair sequentially over a raw socket connection - no HTTP involved.

Key takeaway

Credential stuffing works because people reuse passwords, so one site's breach becomes a key to dozens of others. There is no cryptanalysis involved, only automated replay of known pairs against a live login endpoint. The defense sits at the authentication layer: reject breached passwords through a k-anonymous lookup, require a second factor, and watch login traffic for anomalies, rather than trusting any single password to stay secret.

How to prevent this

You cannot stop attackers from having leaked credentials. You can stop those credentials from working on your service.

  • Check every signup and password change against HaveIBeenPwned's Pwned Passwords API (k-anonymous, 5-char hash prefix). Reject anything in known breach corpora; force a reset on existing accounts that match.
  • Require MFA on every account, ideally TOTP/WebAuthn rather than SMS. Stuffing attacks succeed at ~0.1-2% rates against password-only logins; MFA drops effective success below 0.01%.
  • Detect stuffing patterns: many accounts hit from one IP/ASN, low success rate, distinct user-agents per attempt. Trigger CAPTCHA or step-up auth on anomalies. Cloudflare Turnstile, hCaptcha, and Vercel BotID handle this off the shelf.

Related reading

Useful tools for Web Exploitation

Where to go next