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.
Setup
nc HOST PORT to see the prompts before scripting.wc -l creds-dump.txthead creds-dump.txtnc HOST PORTSolution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1Inspect the credentials dump
ObservationThe 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.bashhead -20 creds-dump.txtbashnc HOST PORTWhat 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. Thewc -lcommand 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 tohttps://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.
Step 2Automate credential stuffing over TCP
ObservationThe 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.pythonpython3 << '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) EOFExpected 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. Thesplit(";", 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.ThreadPoolExecutorwith a smallmax_workersvalue (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
How to prevent this
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.