web

solved. -- https://2025.imaginaryctf.org/Challenges.html

certificate

As a thank you for playing our CTF, we're giving out participation certificates! Each one comes with a custom flag, but I bet you can't get the flag belonging to Eth007!

https://eth007.me/cert/arrow-up-right

  • This one is a freebie—just grab it! Go get the script code in the browser.

  • The challenge asks for the flag that belongs to Eth007. Well, since we can't input Eth008 to generate the flag (which is the hash of Eth009) due to the check (the if statement highlighted below), but since everything is client-side, we have options.

  • We simply call the function directly. Open the console, call the makeFlag function, and we're done.

  • ictf{7b4b3965}

passwordless

Didn't have time to implement the email sending feature but that's ok, the site is 100% secure if nobody knows their password to sign in!

http://passwordless.chal.imaginaryctf.orgarrow-up-right

Attachments

passwordless.ziparrow-up-right

tldr;

  • The flag shows up on the dashboard page, but you need to be logged in to see it. Register won't help us get the password. So we have to exploit it right here and there.

  • Luckily, the site creates the first password by taking whatever email you type and sticking random stuff after it, then hashing it. That hash method only looks at the first 72 characters. If you register with a super long Gmail address like thien+aaaaaaaaaaaaaaaaaaaa...aaaaaaaaaaaaaaaaaaaa@gmail.com (len=116), the random part gets ignored.

  • Next, you can log in with the short, normal Gmail (like thien@gmail.com) and use the same long email you typed as the password to get into the dashboard and read the flag.

  • Do this manually in browser will be quicker, but you can have this solve script to verify the validity.

why is this possible

Well, two issues combine into an auth bypass:

  • Bcrypt input truncation: bcrypt compares only the first 72 bytes.

  • Mismatch between normalized email (used for storage and checks) and the raw email (used to construct the hashed password), allowing a very long raw email to pass a length check performed on a shortened normalized version.

  • Here is the flow + the code that responsible for this bypass

solution.

  • It would be quicker if you just do it in the browser but this could be a quick solve script — I was fidgeting locally via docker.

  • ictf{8ee2ebc4085927c0dc85f07303354a05}

imaginary-notes

I made a new note taking app using Supabase! Its so secure, I put my flag as the password to the "admin" account. I even put my anonymous key somewhere in the site. The password database is called, "users". http://imaginary-notes.chal.imaginaryctf.orgarrow-up-right

circle-check

supabase?

  • I mean yea that is it...the solution is as long as the tldr, not much else to say, I guess a good reference you can look up is the supabase documentationarrow-up-right. But I use this thing everyday so its quite a freebie.🥀

  • Extract Supabase creds from the bundle.

    • URL: https://dpyxnwiuwzahkxuxrojp.supabase.co

    • Anon key (JWT): eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9....Z-rqI

  • Use PostgREST to query the table: GET /rest/v1/users?select=password&username=eq.admin with headers apikey: <anon> and Authorization: Bearer <anon>.

solution.

  • ictf{why_d1d_1_g1v3_u_my_@p1_k3y???}

pearl

I used perl to make my pearl shop. Soon, we will expand to selling Perler beadarrow-up-right renditions of Perlin noisearrow-up-right.

http://pearl.chal.imaginaryctf.orgarrow-up-right

Attachments

pearl.ziparrow-up-right

the flag?

  • You can completely ignore the HTML interface—we're going straight for the URL-based attack vector since this is a path traversal challenge.

  • So...lets go straight to the problem file here — the server.pl

  • We know that the flag is live in the filesystem root due with a hashed name (via Docker)

  • The web app never references the flag directly in this file; we get to it via the path/open bug, the most obvious answer of all time...If you want the flag file...open it.

  • Code in server.pl:

  • So as long as you can get there and give it the flag path, we can peek into the content and retrieve our answer. So what is stopping us?

→ the filter ←

From server.pl:

  • We have three filter stopping us:

    1. remove a leading slash,

    2. catfile

    3. a regex block any path containing the dot, or directory traversal

  • How can we bypass these filter 🥀?

    1. We already know if we want to access the filesystem root, we need some forward slash ahead, but they strip it away from us...Oh well add some more!!! I mean add a newline at the beginning.

      1. By prepending our payload with %0A (URL-encoded newline), we can inject a newline character that effectively bypasses the leading slash removal logic. The server processes %0A/bin/cat and treats everything after the newline as a fresh absolute path.

    2. On Unix, File::Spec->catfile($a, $b)arrow-up-right throws away $a if $b is absolute (starts with /).

      1. This means if we can get an absolute path through, it completely ignores the intended web root restriction.

    3. A single trailing pipe like command| doesn't match this regex and bypasses the filter (we don't need the .. sequence.

  • /bin/ directory: Contains basic system executables/commands like cat, ls, cp, etc.

  • After knowing these tricks, you can easily craft a payload to the flag. Look at this visual learners

solution.

  • Flag file will have some hash, hence we can execute an ls to see the exact file name, but we can also just have a wild card * to filter it out.

  • ictf{uggh_why_do_people_use_perl_1f023b129a22}

pwntools

i love pwntools

Instancer: nc 34.72.72.63 4242

Attachments

pwntools.ziparrow-up-right

tldr;

circle-check

the flag?

Okay, here is the details version of this writeup (the tldr simply summarize things for you).

  • The flag is stored in flag.txt, served only to authenticated admin at /flag ndpoint.

    • This is why I include the app.py above

    • Oh yea, username is admin — classic right?

  • So how can we get the password? We can't "query" it, there is no way endpoint or any exposure of the password, and heck...we don't have access to the log. Which mean we have to "force our way in" — whether bypassing the login or forgery. That is why we need to talk about the vulnerability of this challenge.

the vulnerability

  • Now that our intention is to forge the password, you can read over the code and quickly realize that there's no check whether the user already exists or not—it's just a simple dictionary assignment: accounts = {}.

  • So we can re-use the /register endpoint to overwrite the password.

  • Oh well, you see we are back again at...the filter

the filter~

Now I will lay out the conditions of what we know and what can we do.

  1. No read path: There’s no endpoint to dump or recover the admin password. (yur)

    1. use /register endpoint to overwrite a new one.

  2. IP gate: /register only allows client_addr[0] == "127.0.0.1". (if statement)

    1. Because of single shared addr: The server stores the last accepted connection’s addr in a single variable and passes that into every handler.

    2. So if the bot (triggered by /visit endpoint) connects to http://127.0.0.1:8080/arrow-up-right just before we /register, our request will be misattributed as 127.0.0.1 and passes the localhost check. (Yea, dont forget we have a bot)

  3. We don't have the admin credentials

    1. After 1 and 2 — when we set the new password when the race hits; we now have the full credentials to do mean things.

solution.

  • ictf{oops_ig_my_webserver_is_just_ai_slop_b9f415ea}

codenames-1

I hear that multilingual codenames is all the rage these days. Flag is in /flag.txt.

http://codenames-1.chal.imaginaryctf.org/arrow-up-right (bot does not work on this instance, look at codenames-2 for working bot)

Attachments

codenames.ziparrow-up-right

tldr;

circle-check

the flag?

  • Okay here is the details — In the container at /flag.txt, created by the Dockerfile.

    • Lets focus on flag1, part 2 is below.

  • Now how we able to get an rce to open the flag.txt in the filesystem?

  • There are a couple of sus areas, but only one will later prove to not just open the file, but also render it to the frontend (aka, expose the flag) — the /create_game endpoint

  • We could use this endpoint to open an existing .txt file in the filesystem. So we have to invoke this one with some parameter modified.

  • Any validation that stop us? Yes, but the backend validation only forbids a dot character and fails to normalize or restrict paths to the words/ directory. On Unix, o s.path.join(base, absolute) ignores base and returns the absolute path.

    • That's all that's stopping us. The solution is quite straightforward now.

solulu.

  • You could also achieve this in the browser. Now before we able to create a game, we need to be a user:

    • Log in or register.

    • Send POST /create_game with language=/flag.

      • Do this in the console

  • Follow the redirect to /game/<CODE>.

  • Click "Add Bot" so two players are present and the UI expose the flag.

  • Read the flag from the tiles.

  • ictf{common_os_path_join_L_b19d35ca}

codenames-2

Codenames is no fun when your teammate sucks... Flag is in the environment variable FLAG_2, and please don't spawn a lot of bots on remote. Test locally first.

Instancer: nc 34.72.72.63 1337

Attachments

codenames.ziparrow-up-right

circle-info

UPSOLVE — This challenge ruined my day and dropped it well below the safety line. It's going to haunt me for quite some time. REEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE

tldr;

circle-check

the flag?

  • The key difference between part 1 and 2 is the flag location—instead of a file, it's now stored in an environment variable. The codebase remains essentially the same:

  • Now where is the sus part in this codebase? It is the same wl_path, but instead of opening an existing file, we now have control over which file we're going to "open."

  • So our goal is simple, since the conditions for reading the environment variable require winning the game in hard mode, we need to cheat our way to victory. There's absolutely no way we could win through legitimate gameplay.

the vulnerability

Previously in part 1, we only exploited the /create_game endpoint. However, since there's nothing else valuable in the filesystem root, we need to read from a different path to gain an advantage.

A different operation within the application—profile creation at the /register endpoint — provides exactly what we need:

The profile creation will also create a .txt file, but due to weak validation (a single replacement that strips forward slashes), we can inject a malicous payload to our advantage here.

  • What it means is every literal “/” is removed. There is no further normalization or allow‑list. This matters later because it blocks us from smuggling an absolute path via the username itself, but it does not stop us from abusing the completely different language parameter during /create_game, the parameter is still the language path we can freely choose (if exist).

  • Therefor — If we place a file whose contents will later be send into the bot’s browser, we can execute script in the bot’s context and gain access to state (colors, team assignments) that the guesser view cannot see. The language parameter lets us make the server open an attacker‑controlled file because an absolute path starting with “/” bypasses the intended words/<lang>.txt join. Our crafted “word” line becomes HTML, then assigned via innerHTML, and runs in the bot’s clue‑giver page.

Here is a reminder of the /create_game endpoint ( + some debug statements of mine )

solulu?

We will test simply with a simple XSS payload:

  • Now register a NEW user, this registration is only for us to drop the payload file to the filesystem, username:password (the password doesnt matter)

  • Logout, log in with your default player user.

  • Create the game with the path to app/profiles/<PAYLOAD>

  • Add bot and...WAIT, remember when I mention there is a validation at the username — a single replacement of any "/". Our payload will crash out and get an internal server error because the file payload is now <svg onload=alert(1)>fk<svg>.txt, since the slash is gone. I was testing this on the actual instance, so I was confused a little bit, I couldn't see the log, so I decided to spin up a docker for this. 🥀

solulu.

  • Work around it by using a tag that doesn’t need an explicit closing tag (e.g. or a self‑contained <svg/onload=...>), and invoke /create_game through the browser console:

Minimum XSS works! Now for the actual payload. Register with this payload, logout, login with your player account, and create a game using that payload username. Our goal is to gain advantages to win the game, so we'll exploit everything we can:

Well, filenames must be under 255 characters, which I struggled with initially. Later I learned you can encode the payload to base64, which is quite neat. But here is the result.

Keep guessing, we have an equivalent to infinite guess. If it says you lost, refresh the page or open a new tab and paste the same code URL repeatedly. Press everything within the grid until you succeed.

  • It's not immediately straightforward to understand what you have access to and the exact conditions needed to win. There isn't a single payload that guarantees immediate victory, but some strategic clicking and persistence will get you there.

  • ictf{insane_mind_reading_908f13ab}

failure management system.

Some amusing mishaps occurred:

My original approach was to investigate whether I could become a "bot" myself to see the secret and the board state, since I was fixated on the BOT_SECRET_PREFIX for a considerable amount of time. The philosophy was simple: if you want the flag, you have to win the game. If you can't beat them (hard mode), join them. However, after reconnaissance, I realized that brute-forcing the password was impossible due to the randomly generated secrets.

Next, when attempting to create a minimum viable XSS payload, I didn't pay proper attention to how the onerror handler functioned, leading to me crash out about whether my approach was correct (in game).

Look at all of these attempts that I tried mate — pain peko

  • left (scratch of all the failed xss payload), right (mount folder docker because I want to debug it, running a fresh installation took surprisingly long, and I dont have that patience in me (at the time)

  • ultimately got it working and realized what im missing, it clicks and we got the flag.

Last updated