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!
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
makeFlagfunction, 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.org
Attachments
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:
Bcryptinput truncation:bcryptcompares 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.org
The site is a client-side Next.js app embeds a Supabase anon key in its JS bundle.
Due to the Row Level Security (RLS) on the users table is misconfigured (or disabled i think), the anon role can SELECT user rows. The challenge states the flag is the admin account’s password. Extract the Supabase URL and anon key from the bundle, then query the PostgREST endpoint to read password where username='admin'. And we done.
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 documentation. But I use this thing everyday so its quite a freebie.🥀

Extract Supabase creds from the bundle.
URL:
https://dpyxnwiuwzahkxuxrojp.supabase.coAnon key (JWT):
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9....Z-rqI
Use PostgREST to query the table:
GET /rest/v1/users?select=password&username=eq.adminwith headersapikey: <anon>andAuthorization: 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 bead renditions of Perlin noise.
http://pearl.chal.imaginaryctf.org
Attachments
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.plWe 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/
openbug, 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 ← 
→ the filter ← 
From server.pl:

We have three filter stopping us:
remove a leading slash,
catfile
a regex block any path containing the dot, or directory traversal
How can we bypass these filter 🥀?
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.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/catand treats everything after the newline as a fresh absolute path.
On Unix,
File::Spec->catfile($a, $b)throws away$aif$bis absolute (starts with/).This means if we can get an absolute path through, it completely ignores the intended web root restriction.
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 likecat,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
lsto 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 4242Attachments
tldr;
It’s a timing-dependent race: we repeat the two requests (or a couples) until our /register lands right after the bot’s localhost connection.
Due to the server’s socket loop reuses the last accepted addr for all subsequent requests, so if we make the Selenium bot from /visit connect to http://127.0.0.1:8080/ (or the challenge url (dockerrrrrrrrrrr)), the next request we send is misattributed as localhost and passes the IP check in /register. We then overwrite the admin password and fetch the flag from /flag using the admin:newpassword tech.
This could be done in one sweep with burp suite as well. So choose your tools.
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 authenticatedadminat/flagndpoint.This is why I include the
app.pyabove
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
/registerendpoint 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.
No read path: There’s no endpoint to dump or recover the admin password. (yur)
use
/registerendpoint to overwrite a new one.
IP gate: /register only allows
client_addr[0] == "127.0.0.1". (if statement)Because of single shared addr: The server stores the last accepted connection’s addr in a single variable and passes that into every handler.
So if the bot (triggered by
/visitendpoint) connects to http://127.0.0.1:8080/ just before we/register, our request will be misattributed as127.0.0.1and passes the localhost check. (Yea, dont forget we have a bot)
We don't have the admin credentials
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/ (bot does not work on this instance, look at codenames-2 for working bot)
Attachments
tldr;
Summary: The challenge is vulnerable to Path Traversal via a user-controlled filename in the language field. By submitting language=/flag, os.path.join('words', f"{language}.txt") resolves to /flag.txt because a leading slash makes it an absolute path.
The server reads /flag.txt as a word list and duplicates it to 25 entries, so once the game starts every tile displays the flag.
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_gameendpoint
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)ignoresbaseand 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_gamewithlanguage=/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 1337Attachments
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;
Same core vulnerability as codenames-1: we can force the server to load an attacker-controlled "word list" from an absolute path. Those words are rendered with
innerHTMLinto the clue-giver's page, enabling HTML execution in the bot's browser.The flag is only sent when three conditions are met: a bot is present in the game AND hard mode AND you win. Our XSS payload makes the bot cooperate by either leaking the board state or directly emitting clues/guesses on our behalf (since it doesn't expose to us in the client).
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>.txtjoin. Our crafted “word” line becomes HTML, then assigned viainnerHTML, 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_gamethrough 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