Credential Stuffing picoCTF 2026 Solution

Published: March 20, 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 1
    Inspect the credentials dump
    Observation
    I noticed the challenge provided a file named creds-dump.txt described as a credential dump, which suggested I should examine its format and delimiter before scripting, and that connecting manually with nc would reveal exactly what prompts the TCP 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.

    The split produces either no separator match (line stays unsplit) or incorrectly places the entire line in the username field. Running head -20 creds-dump.txt first reveals the actual delimiter is a semicolon, so the parser must use split(';', 1) to correctly separate username from password.

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

    The service is a raw TCP server, not an HTTP service - there is no URL, no Content-Type header, and no JSON body. Sending an HTTP POST payload causes the server to read garbage and return an error or close the connection immediately. Connecting with nc first shows the plain-text 'Username:' prompt that reveals the true 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 2
    Automate credential stuffing over TCP
    Observation
    I noticed the challenge instance exposes a raw TCP port rather than an HTTP endpoint, and the dump contains many credential pairs to test, which suggested writing a Python socket script to iterate through creds-dump.txt and send each username and password to the server sequentially until the flag appeared in the response.
    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{cr3d_stuf_succ3ss_...}
    What didn't work first

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

    If any password contains a semicolon, split(';') produces three or more parts and password = parts[1] silently drops everything after the second semicolon. The script then sends a truncated password that never matches, causing it to exhaust the entire dump without finding the flag. Using split(';', 1) with maxsplit=1 always captures the full password regardless of embedded semicolons.

    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 is stateful: it waits for a prompt to be consumed before advancing. Without reading the 'Username:' prompt the script sends the username into a buffer the server has not yet cleared, causing the server to interpret the username as a second connection command or drop the input entirely. Both prompts must be drained before sending the next line or the server's state machine gets out of sync and all subsequent pairs fail.

    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
  • 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{cr3d_stuf_succ3ss_...}

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 most users reuse passwords across multiple services, so a breach at one site becomes a master key for dozens of others. The attack is purely mechanical: no cryptanalysis is required, just automated replay of known pairs against a live authentication endpoint. Defenses must operate at the authentication layer itself, combining breach-password rejection (via k-anonymous APIs like HaveIBeenPwned), MFA, and anomaly detection on login traffic patterns rather than relying on the secrecy of any single password.

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

Want more picoCTF 2026 writeups?

Useful tools for Web Exploitation

What to try next