Imaginary CTF Writeup
Total of 7 challenges and I solved three challenges spending 8 hours. Challenges are very good.
Passwordless Challenge
Challenge summary
This challenge is a Node.js/Express web application using an in-memory SQLite database and bcrypt for password hashing. The app implements a “passwordless” style registration where a temporary password is generated at signup but is not actually delivered to the user. Sessions are managed via express-session and several protections are in place (rate limiting, email normalization, and password hashing).
Application analysis
- Stack: Node.js, Express, SQLite (in-memory), bcrypt, express-session, crypto for randomness.
- Registration:
- User submits an email.
- The server normalizes the email and checks its length (max 64 chars).
- A temporary password is generated by concatenating the (un-normalized) supplied email with random bytes, then the result is bcrypt-hashed and stored.
- Email delivery of the temporary password is not implemented.
- Authentication:
- User submits email + password.
- The email is normalized and compared,
bcrypt.compareSync(password, user.password)is used against the stored hash. - On success the session is regenerated and the user object is stored on the session.
- Sessions:
- Managed with express-session and a randomly generated secret.
- Dashboard route checks the session to restrict access.
- Logout destroys the session.
- Rate limiting:
- POST requests to /user and /session are limited to 10 requests per minute per IP.
normalizeEmail('johnotander+foobar@gmail.com')=>'johnotander@gmail.com'- plus-tagging is removed and the local-part is canonicalized; domain is lowercased. Use the normalized form consistently for validation, storage, and lookups.
Key Observations
- The email from the request body is first normalized
|
|
- A length check is performed on the normalized email (nEmail)
- The password is generated using the original (non-normalized) email combined with random bytes.
|
|
- The initialPassword is then hashed with bcrypt.
**Here, bcrypt has a flaw in most of it’s implemenatations, bcrypt.hash() take input of upto 72 bytes and everying after that is truncated. **
solution
- I used this email for registration
ctfuser+aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@gmail.comAfter normalization, email becomes ctfuser@gmail.com and it will pass length check. - make a curl POST request to /user endpoint
|
|
- Now, our password is the 1st 72 chars of the email which will be
ctfuser+aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa - By logging into the application got the flag
ictf{8ee2ebc4085927c0dc85f07303354a05}
Codenames-1 Challenge
This challenge has two flags, one flag at /flag.txt and The second flag is set as an environment variable (FLAG_2).
Codenames-1 backend analysis (rephrased)
- The backend is a Flask app using Flask-SocketIO for real-time gameplay. User profiles are stored as JSON files, and wordlists live in a words folder (one .txt per supported language, e.g., words/en.txt).
- Key components:
- Imports & setup: Flask, SocketIO, Werkzeug password utilities, and filesystem prep for profiles/wordlists.
- Profiles: load_profile/save_profile manage per-user JSON files (username, hashed password, wins, bot flag).
- Routes: /register, /login, /logout, /lobby, /create_game, /join_game, /game/<code>, /add_bot — create_game selects a language and loads the corresponding words file.
- SocketIO: join, give_clue, make_guess handle real-time game flow, team assignment, bot logic, and flag reveal conditions in hard mode.
- Main: app runs via SocketIO for live interactions.
Security observation: wordlist loading and language parameter
- The app chooses a wordlist file based on a user-supplied language value (e.g., language=en -> words/en.txt). The code attempts to block traversal by rejecting values containing a dot, but this check is insufficient.
- Reason: rejecting “.” blocks simple relative traversal tokens like “../”, but an attacker can submit an absolute path (which may not contain a dot) to bypass the filter and point the app at arbitrary files.

solution
- Pass
/flagvalue in the language parameter. - you will get the flag in game grids as shown below.
Imaginary-notes Challenge
Accessed the application from the browser.
It was observed that a request to Supabase API was made directly from the browser as shown below.
Modified this request to get the password of the admin (password of admin is the flag.)
