nahamcon 2025 CTF
misc
quartet
題目描述:Ah, a quartet is a collection of its unique and individuals parts... and together, this has all my favorite kinds of instruments!

quartet.z01
識別為Zip多卷分檔的第一個分卷,包含分卷頭信息,需與其他分卷一起解壓。
quartet.z02 和 quartet.z03
雖然顯示為普通數據文件(data),但它們實際上是后續的分卷。可能是由于文件頭未正確識別導致file命令誤判。
quartet.z04
作為最后一個分卷,包含Zip的中央目錄記錄,因此被識別為標準的Zip歸檔。
直接將quartet.z04重命名為quartet.zip即可正確解壓縮,得到一個quartet.jpeg
搜索quartet.jpeg的內容可以直接得到flag

Screenshot
題目描述:Oh shoot! I accidentally took a screenshot just as I accidentally opened the dump of a flag.zip file in a text editor! Whoopsies, what a crazy accidental accident that just accidented!
Well anyway, I think I remember the password was just password!
由描述可知,題目圖片是一個zip文件的二進制內容

直接文字識別并將內容以二進制寫入到文件里得到flag.zip
hex_str = ( "504b03043300010063002f02b55a00000000430000002700000008000b00666C61672e74787401990700020041450300003d42ffd1b35f95031424f68b65c3f57669f14e8df0003fe240b3ac3364859e4c2dbc3c36f2d4acc403761385afe4e3f90fbd29d91b614ba2c6efde11b71bcc907a72ed504b01023f033300010063002f02b55a00000000430000002700000008002f000000000000002080b48100000000666c61672e7478740a00200000000000010018008213854307cadb01000000000000000000000000000000000199070002004145030000504b0506000000000100010065000000740000000000"
)
with open("flag.zip", "wb") as f:
f.write(bytes.fromhex(hex_str))
print("文件已生成:flag.zip")
得到flag.zip后提取里面的flag.txt,根據題目描述“password is password”密碼就是password,輸入密碼后查看flag.txt得到flag


Read The Rules
題目描述:Please follow the rules for this CTF!
直接跳轉到規則頁面查看網頁源代碼,往下翻即可找到flag

Free Flags!
題目描述:WOW!! Look at all these free flags!!
But... wait a second... only one of them is right??
NOTE, bruteforcing flag submissions is still not permitted. I will put a "max attempts" limit on this challenge at 1:00 PM Pacific to stop participants from automating submissions. There is only one correct flag, you can find a needle in a haystack if you really know what you are looking for.

flag.txt文件如圖,包含大量的flag,在比賽的規則頁面提供了flag的格式 flag{[0-9a-f]{32}},即長度32位,字母僅出現小寫字母,編寫腳本查找符合條件的flag
import re
def find_true_flag(filename):
flag_pattern = re.compile(r'^flag\{[a-f0-9]{32}\}$')
valid_candidates = []
with open(filename, 'r') as f:
for line_num, line in enumerate(f, 1):
raw_flags = line.strip().split()
for flag in raw_flags:
if not flag_pattern.match(flag):
continue
else:
valid_candidates.append(flag)
if valid_candidates:
for i, flag in enumerate(valid_candidates, 1):
print(f"{i}. {flag}")
return valid_candidates[0]
return None
true_flag = find_true_flag('free_flags.txt')
print("正確的flag是:", true_flag if true_flag else "未找到")
Naham-Commencement 2025
題目描述:Welcome, Naham-Hacker Class of 2025! This challenge is your official CTF opening ceremony. Enjoy the CTF, play fair, play smart, and get those flags! BEGIN! ??
(True story: NahamSec originally contracted me to built the actual NahamCon site. I showed this to him as a prototype and he said "you know, let's actually move you to the CTF dev team...")
NOTE, we have noticed an odd gimmick with this challenge -- if you seem to repeatedly see a message An error occurred while processing your request., try changing how you connect to the Internet in case any provider oddities are getting in the way.

嘗試sql注入未果,查看網頁源代碼

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(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;
}
function c(s) {
return btoa(s);
}
document.addEventListener('DOMContentLoaded', function () {
const x1 = "dqxqcius";
const x2 = "YeaTtgUnzezBqiwa2025";
const x3 = "ZHF4cWNpdXM=";
const k = "nahamcon";
const f = document.getElementById('loginForm');
const u = document.getElementById('username');
const p = document.getElementById('password');
const s = document.getElementById('spinner');
const d = document.getElementById('result');
f.addEventListener('submit', function (e) {
e.preventDefault();
const q = u.value;
const w = p.value;
const q1 = a(q);
const w1 = b(w, k);
if (q1 !== x1 || w1 !== x2) {
d.textContent = "Access denied. Client-side validation failed. Try again.";
d.className = "error";
d.style.display = "block";
return;
}
s.style.display = "block";
d.style.display = "none";
const g = new FormData();
g.append('username', q);
g.append('password', w);
fetch('/login', {
method: 'POST',
body: g
})
.then(h => h.json())
.then(z => {
s.style.display = "none";
d.style.display = "block";
if (z.success) {
console.log("?? Server authentication successful!");
d.innerHTML = `
<p>${z.message}</p>
<p class="flag">????${z.flag}????</p>
`;
d.className = "success";
} else {
console.log("? Server authentication failed");
d.textContent = z.message;
d.className = "error";
}
})
.catch(err => {
console.error("?? Network error:", err);
s.style.display = "none";
d.style.display = "block";
d.textContent = "An error occurred while processing your request.";
d.className = "error";
});
});
});
閱讀網頁源代碼可知
函數a對用戶名進行ROT16加密。已知加密后的用戶名x1為"dqxqcius"。解密ROT16:每個字母逆處理(即ROT10),得到原用戶名"nahamsec"。

函數b使用Vigenère加密,密鑰k為"nahamcon"。已知加密后的密碼x2為"YeaTtgUnzezBqiwa2025"。Vigenère解密:使用密鑰k解密后,得到原密碼"LetTheGamesBegin2025"。

將得到的賬戶密碼輸入到主頁面即可得到flag

The Oddyssey
題目描述:Remember reading The Odyssey in high school? Well I sure don't, because I never did my homework. But I really wanted to get back into the classics and give it a fair shake. The problem is I have a fourth grade reading level and that book is waaaaaay too long.
To solve this, I made a server that reads out tiny chunks of The Odyssey, one at a time, so I can take my time reading it! How is Odysseus gonna get himself out of this one?
連接到題目后每次輸出一小段內容,每輸入一次回車則再輸出一段,故編寫腳本自動觸發檢查內容是否包含flag
from pwn import *
import sys
import time
def main():
server_ip = "challenge.nahamcon.com"
server_port = 31574
output_file = "output.txt"
try:
conn = remote(server_ip, server_port)
print(f"Connected to {server_ip}:{server_port}")
with open(output_file, "w", encoding="utf-8") as f:
count = 0
while True:
try:
conn.sendline()
data = conn.recv(timeout=5).decode("utf-8", errors="ignore")
if not data:
print("Connection closed by server")
break
f.write(data)
f.flush()
print(f"[Chunk {count}] Received {len(data)} bytes")
if "flag{" in data.lower():
print("\n?? FLAG FOUND IN LAST RESPONSE!")
break
count += 1
time.sleep(0.1)
except EOFError:
print("\nServer closed the connection")
break
except Exception as e:
print(f"\nError occurred: {str(e)}")
break
except ConnectionRefusedError:
print("Connection refused. Check server IP/port")
except KeyboardInterrupt:
print("\nUser interrupted")
finally:
if 'conn' in locals():
conn.close()
print(f"All responses saved to {output_file}")
if __name__ == "__main__":
main()

SNAD
題目描述:No, it's not a typo. It's not sand. It's SNAD. There's a difference!
簡單看了一下,是一個類似于畫板的頁面,查看響應包。
HTTP/1.1 200 OK
X-Powered-By: Express
Access-Control-Allow-Origin: *
Accept-Ranges: bytes
Cache-Control: public, max-age=0
Last-Modified: Fri, 23 May 2025 18:05:31 GMT
ETag: W/"18e7-196fe5255f8"
Content-Type: application/javascript; charset=UTF-8
Content-Length: 6375
Date: Sat, 24 May 2025 02:48:07 GMT
Connection: keep-alive
Keep-Alive: timeout=5
const requiredGrains = 7, targetPositions = [{ x: 367, y: 238, colorHue: 0 }, { x: 412, y: 293, colorHue: 40 }, { x: 291, y: 314, colorHue: 60 }, { x: 392, y: 362, colorHue: 120 }, { x: 454, y: 319, colorHue: 240 }, { x: 349, y: 252, colorHue: 280 }, { x: 433, y: 301, colorHue: 320 }], tolerance = 15, hueTolerance = 20; let particles = [], grid = [], isMousePressed = !1, colorIndex = 0, flagRevealed = !1, targetIndicatorsVisible = !1, gravityStopped = !1; function getRainbowColor() { return color("hsb(" + (colorIndex = (colorIndex + 5) % 360) + ", 100%, 90%)") } function getSpecificColor(e) { return color("hsb(" + e + ", 100%, 90%)") } async function retrieveFlag() { let e = document.getElementById("flag-container"); e.style.display = "block"; try { let t = particles.filter(e => e.settled).map(e => ({ x: Math.floor(e.x), y: Math.floor(e.y), colorHue: e.colorHue })), o = await fetch("/api/verify-ctf-solution", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ particleData: t }) }), i = await o.json(), r = e.querySelector(".loading"); r && r.remove(), i.success ? (e.querySelector("p").textContent = "SNAD!", document.getElementById("flag-text").textContent = i.flag) : (e.querySelector("p").textContent = i.message, document.getElementById("flag-text").textContent = "", setTimeout(() => { e.style.display = "none", flagRevealed = !1 }, 3e3)) } catch (l) { console.error("Error retrieving flag:", l), document.getElementById("flag-text").textContent = "Error retrieving flag. Please try again."; let s = e.querySelector(".loading"); s && s.remove() } } function injectSand(e, t, o) { if (isNaN(e) || isNaN(t) || isNaN(o)) return console.error("Invalid parameters. Usage: injectSand(x, y, hue)"), !1; o = (o % 360 + 360) % 360; let i = new Particle(e, t, { colorHue: o, settled: !0, skipKeyCheck: !0, vx: 0, vy: 0 }); particles.push(i); let r = floor(e), l = floor(t); return r >= 0 && r < width && l >= 0 && l < height && (grid[l][r] = !0), i } function toggleGravity() { gravityStopped = !gravityStopped, console.log(`Gravity ${gravityStopped ? "stopped" : "resumed"}`) } class Particle { constructor(e, t, o = {}) { this.x = void 0 !== o.x ? o.x : e, this.y = void 0 !== o.y ? o.y : t, this.size = o.size || random(2, 4), void 0 !== o.colorHue ? (this.colorHue = o.colorHue, this.color = getSpecificColor(o.colorHue)) : (this.color = getRainbowColor(), this.colorHue = colorIndex), this.vx = void 0 !== o.vx ? o.vx : random(-.5, .5), this.vy = void 0 !== o.vy ? o.vy : random(0, 1), this.gravity = o.gravity || .2, this.friction = o.friction || .98, this.settled = o.settled || !1, o.skipKeyCheck || this.checkSpecialGrain() } checkSpecialGrain() { keyIsDown(82) ? (this.color = getSpecificColor(0), this.colorHue = 0) : keyIsDown(79) ? (this.color = getSpecificColor(40), this.colorHue = 40) : keyIsDown(89) ? (this.color = getSpecificColor(60), this.colorHue = 60) : keyIsDown(71) ? (this.color = getSpecificColor(120), this.colorHue = 120) : keyIsDown(66) ? (this.color = getSpecificColor(240), this.colorHue = 240) : keyIsDown(73) ? (this.color = getSpecificColor(280), this.colorHue = 280) : keyIsDown(86) && (this.color = getSpecificColor(320), this.colorHue = 320) } update(e) { if (this.settled || gravityStopped) return; this.vy += this.gravity, this.vx *= this.friction; let t = this.x + this.vx, o = this.y + this.vy; (t < 0 || t >= width || o >= height) && (o >= height && (o = height - 1, this.settled = !0), t < 0 && (t = 0), t >= width && (t = width - 1)); let i = min(floor(o) + 1, height - 1), r = floor(t); if (i < height && !e[i][r]) this.x = t, this.y = o; else { let l = max(r - 1, 0), s = min(r + 1, width - 1); i < height && !e[i][l] ? (this.x = t - 1, this.y = o, this.vx -= .1) : i < height && !e[i][s] ? (this.x = t + 1, this.y = o, this.vx += .1) : (this.x = r, this.y = floor(this.y), this.settled = !0) } let c = floor(this.x), a = floor(this.y); c >= 0 && c < width && a >= 0 && a < height && (e[a][c] = !0) } draw() { noStroke(), fill(this.color), circle(this.x, this.y, this.size) } } function setup() { createCanvas(windowWidth, windowHeight), resetGrid(), document.addEventListener("keydown", function (e) { "t" === e.key && (targetIndicatorsVisible = !targetIndicatorsVisible), "x" === e.key && toggleGravity() }), window.injectSand = injectSand, window.toggleGravity = toggleGravity, window.particles = particles, window.targetPositions = targetPositions, window.checkFlag = checkFlag } function resetGrid() { grid = []; for (let e = 0; e < height; e++) { grid[e] = []; for (let t = 0; t < width; t++)grid[e][t] = !1 } flagRevealed = !1; let o = document.getElementById("flag-container"); o.style.display = "none" } function draw() { if (background(30), isMousePressed && mouseX > 0 && mouseX < width && mouseY > 0 && mouseY < height) for (let e = 0; e < 3; e++) { let t = new Particle(mouseX + random(-5, 5), mouseY + random(-5, 5)); particles.push(t) } if (targetIndicatorsVisible) for (let o of (stroke(255, 150), strokeWeight(1), targetPositions)) noFill(), stroke(o.colorHue, 100, 100), circle(o.x, o.y, 30); let i = []; for (let r = 0; r < height; r++) { i[r] = []; for (let l = 0; l < width; l++)i[r][l] = !1 } for (let s of particles) { s.update(grid), s.draw(); let c = floor(s.x), a = floor(s.y); c >= 0 && c < width && a >= 0 && a < height && (i[a][c] = !0) } grid = i, checkFlag(), fill(255), textSize(16), text("Particles: " + particles.length, 10, height - 20) } function checkFlag() { if (flagRevealed) return; let e = 0, t = []; for (let o of targetPositions) { let i = !1; for (let r of particles) if (r.settled) { let l = dist(r.x, r.y, o.x, o.y), s = min(abs(r.colorHue - o.colorHue), 360 - abs(r.colorHue - o.colorHue)); if (l < 15 && s < 20) { i = !0, t.push({ targetPos: `(${o.x}, ${o.y})`, targetHue: o.colorHue, particlePos: `(${Math.floor(r.x)}, ${Math.floor(r.y)})`, particleHue: r.colorHue, distance: Math.floor(l), hueDifference: Math.floor(s) }); break } } i && e++ } e >= 7 && (flagRevealed = !0, console.log("\uD83C\uDF89 All positions correct! Retrieving flag..."), retrieveFlag()) } function mousePressed() { isMousePressed = !0 } function mouseReleased() { isMousePressed = !1 } function keyPressed() { ("c" === key || "C" === key) && (particles = [], resetGrid()) } function windowResized() { resizeCanvas(windowWidth, windowHeight), resetGrid() }
代碼中定義了7個目標位置(targetPositions),每個位置有坐標和色調(colorHue):
targetPositions = [
{ x: 367, y: 238, colorHue: 0 },
{ x: 412, y: 293, colorHue: 40 },
{ x: 291, y: 314, colorHue: 60 },
{ x: 392, y: 362, colorHue: 120 },
{ x: 454, y: 319, colorHue: 240 },
{ x: 349, y: 252, colorHue: 280 },
{ x: 433, y: 301, colorHue: 320 }
];
需要將7個沙粒放置在這些位置附近(誤差小于15像素),且顏色色調誤差小于20。
驗證條件
函數 checkFlag() 會檢查是否滿足以下條件:
沙粒的坐標與目標位置的距離 < tolerance(15像素)。
沙粒的色調與目標色調的差值 < hueTolerance(20)。
直接通過瀏覽器控制臺注入沙粒,避免手動操作誤差:
targetPositions.forEach(pos => {
injectSand(pos.x, pos.y, pos.colorHue);
});
執行后會自動生成7個符合要求的沙粒,觸發 retrieveFlag() 函數向服務器驗證。
Cryptoclock
題目描述:Just imagine it, the Cryptoclock!! Just like you've seen in the movies, a magical power to be able to manipulate the world's numbers across time!!
題目給了一個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()
發送與 Flag 長度相同的全零數據(\x00 字節),服務器會用相同種子生成的密鑰進行加密。由于 0 XOR key = key,響應內容即為密鑰。
將加密后的 Flag 與獲取的密鑰進行 XOR 運算,即可解密
交互exp如下
import socket
from time import time
def exploit():
HOST = '題目服務器地址'
PORT = #端口
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
# 接收歡迎信息并解析加密 Flag
data = s.recv(1024).decode()
encrypted_flag_hex = data.split('The encrypted flag is: ')[1].split('\n')[0].strip()
encrypted_flag = bytes.fromhex(encrypted_flag_hex)
flag_length = len(encrypted_flag)
zero_payload = b'\x00' * flag_length + b'\n'
s.send(zero_payload)
key_response = s.recv(1024).decode()
key_hex = key_response.split('Encrypted: ')[1].split('\n')[0].strip()
key = bytes.fromhex(key_hex)
flag = bytes([a ^ b for a, b in zip(encrypted_flag, key)])
print(f"Flag: {flag.decode()}")
if __name__ == '__main__':
exploit()
The Martian
題目描述:Wow, this file looks like it's from outta this world!
下載后直接丟到虛擬機中對文件進行處理

拆分后查看圖片即為flag

Puzzle Pieces
題目描述:Well, I accidentally put the important data into a bunch of executables.
It was fine, until my cat stepped on my keyboard and renamed them all!
Can you help me recover the important data?
NOTE, the password for the archive is nahamcon-2025-ctf
根據題目描述所給密碼解壓縮后發現是很多個可執行文件,執行后輸出疑似flag的碎片,按照修改時間對flag碎片進行排序即可得到flag

浙公網安備 33010602011771號