CITCTF2025-5WebChallengesWriteups
Omar Mohamed
Thanks for sharing!
بِسْمِ اللَّهِ الرَّحْمَنِ الرَّحِيمِ

Hello and welcome back again to another walkthrough! Mushroom is here and today I'll be covering all the web challenges from CIT CTF 2025 which I played with Lil L3ak team. We were able to get the 2nd place with all challenges solved except for one crypto challenge. Also securing +$13k in prizes!💰💎


Let's start right away 🚀
Challenges
- Breaking Authentication
- Commit & Order: Version Control Unit
- How I Parsed your JSON
- Keeping Up with the Credentials
- Mr. Chatbot
Breaking Authentication

Visiting the url, I got a login form

First thought SQL injection, and indeed it was. I simply logged in with
admin' --
and I was redirected to Admin Panel but the flag wasn't thereSince there’s a SQL injection vulnerability, the flag might be stored in the database. However, we face a challenge: there’s no visible output, only a redirect. This indicates a Blind SQL Injection — either Error-Based or Time-Based, both of which are exploitable.
To speed things up, I used SQLMap, an automated tool for SQL injection exploitation
I won't get deep into explaining the tool, you can look it up
I saw a table called
secrets
, so next let's dump its data:
CIT{36b0efd6c2ec7132}
Commit and Order Version Control Unit

As from the name.. this have something to do with git which is often used for version control. Sometimes developers mistakenly leave it out open in the wild and you can access it via
/.git/
Accessing it got me Forbidden

But accessing other core file paths like
/.git/HEAD
got me response
So it seems that the dev prevents access to directories but not the files themselves.
Now whenever you get an exposed git like this, you can dump it with tools like git-dumper. Simply run:
Now inspecting this repo we can see 2 file:
admin.php
& index.php
. index.php
got me a password:
Spoiler Alert: this pass9f3IC3uj9^zZ
will be used in another challenge
I couldn't log in using that password in this challenge.
If you're familiar with Git (which you really should be), you'd know about commits — essentially snapshots of your codebase at different points in time. You can navigate to any of these commits to see the code as it was before any updates or deletions. In our case, that's a great thing — it means we can recover deleted content this way!
You can view past commits along with their messages using:

To navigate to a specific commit, run:
and you'll see the files as they were at that point in time.
Extra: to get back to the latest commit just run:
I used another option to speed things up:
This will show differences between your version and the past version, which reveals the past code for you as well!
I eventually got the flag in the 4th commit base64 encoded:
Q0lUezVkODFmNzc0M2Y0YmMyYWJ9

CIT{5d81f7743f4bc2ab}
How I Parsed your JSON

Now that's a good one!
We've got this page

To sum things up, it is acting as a databse but gets data from json files! And we control this filename (Here a path traversal comes to mind ✨)
I tried
*
and got the following:
The url looked like:
/select?record=*&container=employees
I checked the page source but didn’t find anything useful, neither the path traversal worked, there was some filtering on the backend
One of the things an attacker loves is an Error! Errors can reveal information about the backend, and if debugging is enabled, they might even leak source code.
Add this to your Notes: Try to break things and trigger errors.
After poking around a bit, I removed the second parameter (container) and got my beautiful TypeError!

Going through it, I got out with some juicy code


The code is simple — it takes your
container
query and passes it to the clean_container_name()
function. This function extracts the part before the file extension and removes any ../
. The returned value is then passed directly to the os.path.join()
function and this path is used to read a file.For example if you passed
../secrets.txt
it will be filtered to secrets
and the final path will be containers/secrets
Now here.. did you spot the vulnerability? the code is basically removing
../
, but it doesn't do that recursively. What if you put ../
in another ../
to be ....//
, so when ../
gets deleted the final result will be ../
Simpler version: SupposeMush
is filtered → I passMuMushsh
→Mush
is deleted → result isMush
That's it! Now pass
....//secrets.txt
and ... Invalid QueryWhy is that? remeber the
os.path.splitext(n)[0]
, this gets only the first part before the extension and becomes containers/../secrets
→ secrets
You can handle that in two ways:
- 1 - Use the same technique we just used and pass a
../
in before the extension, so when it is filtered, you end up correctly:....//secrets.../txt
→../secrets.txt
- 2 - The other way is just to url encode the
.
:....//secrets%2Etxt

And you get your flag (with the same password we found earlier, you will know its use in the next chal)
CIT{235da65aa6444e27}
Keeping Up with the Credentials

This challenge, along with the next one, had the lowest solve rates (That led me to assume it would involve some tricky or advanced technique and I wasted a long time overthinking it)
The setup was a simple login form, and considering the challenge was titled “Keeping Up with the Credentials”, I tried using the credentials I had found earlier:
9f3IC3uj9^zZ
. They worked, and I was logged in… but there was no flag.While inspecting the login request in Burp Suite, I noticed something odd:

Instead of using a typical
POST
request, the form sent a GET
request with the credentials in the URL query string. I dismissed it at first, thinking maybe the developer just implemented login that way.Later, I found an interesting path:
/admin.php
. When I tried to access it, I was immediately redirected back to the login page, so I assumed access was restricted. I started testing various techniques, including HTTP request smuggling, all based on the assumption that the challenge had to be difficult due to its low solve count.Turns out… the fix was stupidly simple: I just had to change the login request from
GET
to POST
, and I was redirected to /admin.php
, where I found the flag.Not sure if this challenge was testing secure coding practices or just trolling me a bit 🙂 but either way:
CIT{7bf610e96ade83db}
And now with the last challenge - this one is good
Mr Chatbot

Visiting the url, it asks you for your name:

I was redirected to a chatting page:

Not much functionalities, I inspected the page source, it had the follwing script
It's just a simple script that makes the bot give some responses, but the interesting thing… it reflects my name! SSTI? Let's try it out
I passed
{{7*7}}
and got 7*7
back, so it was filtering. Tried multiple other payloads but all failed.In your cookies, you’re given a session token, specifically, a Flask session token. It’s similar to a JWT (JSON Web Token) but has a few key differences.
You can easily decode it using Flask-Unsign
You have your
name
and an admin
value set to 0
. Mass Assignment?I sent another request with
admin
set to 1
. This time I got a longer tokenI was successfully able to set
admin
to 1
. I tried to access the page again but no differences.We notice another value added when we did exploit the Mass Assignment:
uid
, which has what it seems like a base64 encoded value.After decoding

It has the name I passed and some bytes:
mushroomb'^\xf2\xc3 ... \xd7\x05'
. I tried hex decoding that but it didn't give me anything. On the bright side we have a reflection! my name
is thereAdd this to your Notes: Reflections are good!
Reflection helps identify injection points — where user-controlled data flows — which is critical in exploiting things like SQLi, command injection, or template injection.
To test that I passed
{{7*7}}
into name
and set admin
to 1

Bingo! It is vulnerable, I got
49
. That's our way in!I used
{{''.__class__.__mro__[1].__subclasses__()}}
in name
and got a huge token
decoding it

Next I used
{{''.__class__.__mro__[1].__subclasses__()[399]('secrets.txt').read()}}

And that's our flag!
CIT{18a7fbedb4f3548f}
Finally this writeup is over, took +4 hours man lol. 😮💨
Hopefully that was beneficial to you. Don't forget to check out my X and if you have any questions, feel free to reach out to me on Discord.
See you next time 🚀! Peace.
Tags: