Imaginary CTF Writeup

Total of 7 challenges and I solved three challenges spending 8 hours. Challenges are very good.

Passwordless Challenge

image 1

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

image 1
  • The email from the request body is first normalized
1
const nEmail = normalizeEmail(req.body.email);
  • A length check is performed on the normalized email (nEmail)
  • The password is generated using the original (non-normalized) email combined with random bytes.
1
const initialPassword = req.body.email + crypto.randomBytes(16).toString('hex');
  • 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.com After normalization, email becomes ctfuser@gmail.com and it will pass length check.
  • make a curl POST request to /user endpoint
1
curl -X POST http: //passwordless.chal.imaginaryctf.org/user -d "email=ctfuser%2Baaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@gmail.com"
  • 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

image 1

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. image 1

solution

  • Pass /flag value in the language parameter.
  • you will get the flag in game grids as shown below.
image 1

Imaginary-notes Challenge

image 1

Accessed the application from the browser. image 1 It was observed that a request to Supabase API was made directly from the browser as shown below. image 1 Modified this request to get the password of the admin (password of admin is the flag.) image 1