TsukuCTF2025 Writeup

2025-05-04

チーム「卍最強卍」でOptimisticと2人で参加して62位でした。問題をそれなりに残しておくつもりだったけど結構解いちゃってごめん。久々の割には善戦したんじゃなかろうか?

Osint

Casca

熱海とポルトガルで検索すると姉妹都市らしい。ほぼ決め打ちでやったらあたってウケた。記念碑をつけて適当にググると 写真が貼られたサイト が見つかる。
TsukuCTF25{2014/06/06}

Curve

曲がってるエレベーターは維持の難しさなどからあまりないことは一般に知られているのはその通り。検索すると、さすがの三菱なんですね( サイト )。
写真を見る感じ、美術館系っぽい?きがする。と思ったら、 全設置箇所まとめのページ を発見。目grepすると、神奈川県のランドマークプラザっぽい。 TsukuCTF25{tokyo-esca.com} を投げるがincorrect。あれ?
調べ直して、 TsukuCTF25{yokohama-landmark.jp} を投げてcorrect。

Power

情報がなさすぎる... ので、頑張って点字を解読する。

とーきょーと きゅーせき まさかどづか

東京都旧跡将門塚!!!!!ということで、あとは調べるだけ。
TsukuCTF25{35.6873_139.7627}

rider

写真右端にOTi... Fried Chikenという看板が見えるのでググる。一発で見つかっていい感じ( link )。ジャカルタのチェーン店らしい。
長めの道で、奥が多分T字路になっているっぽいので、それを念頭に探す。日本人のCTFなので、おそらく都市部かつ大通りだろうという予想。
めちゃくちゃ頑張って探すと、見つかる TsukuCTF25{-7.3189_110.4971}

Destroyed

わからないのでほぼ丸投げ。ある程度調べてもらったやつのリンクを眺めながらそれっぽそうなのを適当に投げていたらあたった(あの?)

Web

len_len

あー、これなんだっけ。と言いながらAI丸投げ

...
しかし、これは機能しません。実際には、JSONパーサーを使用して配列を作成する場合、配列の length は常に非負になります。
実際の解法は、配列のプロトタイプを操作して length プロパティをオーバーライドすることですが、JSONパースだけではこれは不可能です。
あ、待って!JSONで有効ではない構文を使用して JSON.parse を失敗させ、ソースコードを確認できるエラーを発生させる必要があります。
いや、実際の解法は JSON.parse が配列以外のものを返すようにすることです!
...

勝手に自分で解決してた。

❯ curl -X POST -d 'array={"length":-1}' http://challs.tsukuctf.org:28888
TsukuCTF25{l4n_l1n_lun_l4n_l0n}%    

Crypto

完全に専門外なので予定はなかったけど、MMRZの作問分があるはずなので一個ぐらいは解きたいなぁのきもちで取り組む。

a8tsukuctf

ヴィジュネル暗号の実装らしい。tsukuctfの部分が 変わらない ということは、その部分がおなじになるように設定されている、ということ。 \(p\) と \(p+k\) を26で割ったあまりが等しくないといけないので、 \(p, k < 26\) に注意すると \(k=0\) しかありえない。つまり、暗号の tsukuctf の部分は a が連続しているはず。あと、keyの文字数が足りなくなった時点で暗号文をkeyとして再利用している。これに a が連続する部分があるから、その部分は平文が残っているはず。
...ん? tsukuctf も、 a が連続する部分も8文字なんだよなぁ。これはつまり、周期が8であることを示唆している?じゃないと暗号文が ...aaaaaa aa tsukuctf... にはならないはず。実際に前から8文字で区切っていくとそこが境目になるし、これで決め打って良さそう。はじめの8文字を除いてkeyと暗号文がわかったことになるので、デコーダを書く。

ciphertext="ayb wpg uujmz pwom jaaaaaa aa tsukuctf, hj vynj? mml ogyt re ozbiymvrosf bfq nvjwsum mbmm ef ntq gudwy fxdzyqyc, yeh sfypf usyv nl imy kcxbyl ecxvboap, epa 'avb' wxxw unyfnpzklrq."

import string

cipher_only_acsii=[]

for c in ciphertext:
    if c in string.ascii_lowercase:
        cipher_only_acsii.append(c)

key = cipher_only_acsii[0:8]

def f(c, k):
    base = ord('a')
    ci = ord(c) - base
    ki = ord(k) - base
    p = (ci-ki+26)%26
    return chr((base+p))

idx = 0
ans=[]
for c in ciphertext[10:]:
    if c not in string.ascii_lowercase:
        ans.append(c)
        continue

    p = f(c, key[idx])
    ans.append(p)
    key[idx] = c
    idx+=1
    idx%=8

print(''.join(ans))
"""
❯ python3 dec.py
joy this problem or tsukuctf, or both? the flag is concatenate the seventh word in the first sentence, the third word in the second sentence, and 'fun' with underscores.
"""

ということで、flagは TsukuCTF25{tsukuctf_is_fun}
tsukuctf の部分が変わらないなぁじゃないよ。絶対作るの大変だったでしょこれ。


xortsukushift

Xor shiftの部分だけ取り出して適当なseedで試してみると、どうも280回で一周回るっぽい?で、300回の試行があるので
- 280回に渡って任意の手を出し続けて、その結果から順序を取り出す
- 十分に溜まったら、それに対応する手を出し続ける
をすればよい。
久々にpwntoolsを使ったので仕様に頭を悩ませつつ、AIに助けてもらいつつ、なんとか書き上げる。

trace = []
idx = 0
WIN = b"Tsukushi: You win!\n"
DRAW = b"Tsukushi: Draw! ...But If you wanna get the flag, you have to win 294 rounds in a row.\n"
ENDGAME = b"Tsukushi: You won 294 times in a row?! That's incredible!\n"

def janken_gen() -> int:
    global idx
    if len(trace) < 280:
        return 0
    else:
        res = (trace[idx]+1)%3
        idx += 1
        idx %= 280
        return res

from pwn import *

#io = process(['uv', 'run', 'server.py'], env={'FLAG':'STATUS_OK'})
io = remote('challs.tsukuctf.org', 30057)
while True:
    isEnd = False
    while True:
        print(io.recvuntil(b"Rock, Paper, Scissors... Go! (Rock: 0, Paper: 1, Scissors: 2): "))
        p = janken_gen()
        io.send(str(p)+'\n')
        
        if len(trace) < 280:
            # 結果をもとに相手の手を復元
            result = io.recvline()
            if result == WIN:
                trace.append(2)
            elif result == DRAW:
                trace.append(0)
                break
            else:
                trace.append(1)
                break
        else:
            io.recvline()
            result = io.recvline()
            if result == ENDGAME:
                isEnd = True
                break
    if isEnd:
        break

print(io.recvall())
❯ uv run exploit.py
...
[+] Receiving all data: Done (95B)
[*] Closed connection to challs.tsukuctf.org port 30057
b'Tsukushi: So, here is the flag. TsukuCTF25{4_xor5h1ft_15_only_45_good_45_1t5_5h1ft_p4r4m3t3r5}\n'
(xortsukushift) 

tokyo-esca.comは全設置箇所まとめのページなので当たり前である

一応ごめんなさいの顔はしてる だいたい1時間ぐらい書けてGoogleMapにぴんをおとすさぎょうをしていた