# TsukuCTF 2025 Writeup 2025年5月3日 土曜日 12:00 GMT+9 ~ 2025年5月4日 日曜日 12:00 GMT+9に開催された OSINT多めのCTF, TsukuCTF 2025 に **野菜を食べ、毎食後に歯を磨く** という名前のチームで参加してきました ![team result](https://hackmd.io/_uploads/SkwK3GBexx.png) 結果は20問中14問解き2071pt、882チーム中21位でした。(参加者めっちゃ増えてるな) <figure> <img src="https://hackmd.io/_uploads/By6ZpMSgxg.png"> <figcaption>OSINT全完できたので満足!</figcaption> </figure> チームメンバーは3人で、このwriteupも3人がリアルタイムに書いています。なので一人称などが同じ人物を指すとは限りません。 - 前回: [TsukuCTF 2023 Writeup - HackMD](https://hackmd.io/@eaint/tsukuctf-2023-writeup) - その他のWriteup: [EaINT - HackMD](https://hackmd.io/@eaint) <small> <details><summary>余談1: チーム名は特にいい感じのが思いつかなかったので適当に入れたんですが一応元ネタがあって、</summary> 歴史あるハクスラとして有名なDiabloシリーズの初代で、ダンジョンの16階に到達すると禍々しいディアブロの声が聞こえるのですが、実はそれは「Eat your vegetables, and brush after every meal!(野菜を食べ、毎食後に歯を磨きましょう)」の逆再生というネタです https://diablo.fandom.com/wiki/Level_16_(Diablo_I) 知ってるわけがないので説明しました CTFとは何も関係ないです </details> </small> <small> <details><summary>余談2</summary> ![Screenshot 2025-05-03 at 17-08-32 TsukuCTF 2025](https://hackmd.io/_uploads/BkcAm7Hegl.png) 問題追加のタイミングでハイエナしたら一時的に3位になれて嬉しかったです:v: </details> </small> # 解けた問題 ## crypto - PQC0 (149 solves) <details><summary>問題</summary> > PQC(ポスト量子暗号)を使ってみました! - Files - [prob.py](https://github.com/EaINT-HQ/challenge-attachments/blob/main/2025/tsukuctf/6/prob.py) - [output.txt](https://github.com/EaINT-HQ/challenge-attachments/blob/main/2025/tsukuctf/6/output.txt) </details> 書いた人: karubabu * opensslを使ってML-KEMなアルゴリズムで秘密鍵を作る。 * 秘密鍵から公開鍵を作る。 * 公開鍵とshared.datを使ってカプセル化した共有鍵`chiphertext.dat`を作る。 * shared.datを使ってflagをAESで暗号化する。 秘密鍵とカプセル化した共有鍵と暗号化したflagは `output.txt`にある。 なので、やる事は、 * 秘密鍵とカプセル化した共有鍵を使用して、shared.datを手に入れる。 これは、`openssl pkeyutl -decap -inkey priv-ml-kem-768.pem -in ciphertext.dat -secret shared.dat` で出来る。 `priv-ml-kem-768.pem`とかいうファイルは`output.txt`のprivate keyの部分を貼り付けるだけ。 問題無く出来ているか確認したい場合は、`openssl pkey -in priv-ml-kem-768.pem -text -noout`とかやってまともな出力が返ってくるか見る。 `ciphertext.dat`は`output.txt`の`ciphertext(hex)`部分を`ciphertext.hex`あたりの名前で作ったファイルに貼り付けて、`xxd -r -p encrypted_flag.hex > encrypted_flag.dat`すると出来る。 これらの操作はopenssl3.5.0かつML-KEMのサポートが対応していないとだめっぽい。 確認は`openssl list -public-key-algorithms | grep ML-KEM`で出来る。 * shared.datと暗号化したflagをAESで復号。 prob.pyのコードを見て、なんか似たようなことを自前でもやる。unpadの16ってなんなんスかね。 ```python= from Crypto.Cipher import AES from Crypto.Util.Padding import unpad with open("shared.dat", "rb") as f: shared_secret = f.read() with open("encrypted_flag.dat", "rb") as f: encrypted_flag = f.read() cipher = AES.new(shared_secret, AES.MODE_ECB) flag = unpad(cipher.decrypt(encrypted_flag), 16) print(flag) ``` `TsukuCTF25{W3lc0me_t0_PQC_w0r1d!!!}` ## crypto - a8tsukuctf (241 solves) <details><summary>問題</summary> > 適当な KEY を作って暗号化したはずが、 ``tsukuctf`` の部分が変わらないなぁ... - Files - [enc.py](https://github.com/EaINT-HQ/challenge-attachments/blob/main/2025/tsukuctf/26/enc.py) - [output.txt](https://github.com/EaINT-HQ/challenge-attachments/blob/main/2025/tsukuctf/26/output.txt) </details> 書いた人: karubabu ヴィジュネル暗号の問題。平文の30文字から37文字目までが、暗号文にある`tsukuctf`と一致しているらしい。 ![image](https://hackmd.io/_uploads/BkkV5DEeeg.png) コメントにあったリンクを見ると、平文の30から37文字目である`tsukuctf`と暗号文の30から37文字目このような関係になっているので、ここからkeyが取得できる。 でもなんか鍵を使い回しても文章にならない?と思って調べていると、オートキーなる方法があるのを見付けた。 [あまりにも怪しいサイト](https://ja.gov-civ-guarda.pt/vigen-re-cipher) >Kasiskiによって悪用される繰り返し鍵の周期性は、実行鍵のVigenère暗号によって排除できます。このような暗号は、繰り返しのないテキストがキーに使用された場合に生成されます。 Vigenèreは実際に、オートキーと呼ばれるもので実行キーを提供するために、平文自体を連結して秘密キーワードに従うことを提案しました。 `ヴィジュネル暗号 オートキー`でググると、googleのAIが全部教えてくれた。なんでも鍵より長い平文部分は、既に暗号文にした文字を使用して暗号するというもの。 そうなるようなコードを用意する。 ```python= import string def f_key(c, p): c_val = ord(c) - ord('a') p_val = ord(p) - ord('a') k_val = (c_val - p_val + 26) % 26 return chr(ord('a') + k_val) def recover_key(ciphertext, known_plain, cipher_pos): key = [] idx = 0 for i, c in enumerate(ciphertext): if c in string.ascii_lowercase: if idx >= cipher_pos and idx < cipher_pos + len(known_plain): key_char = f_key(c, known_plain[idx - cipher_pos]) key.append(key_char) idx += 1 return ''.join(key) def decrypt(ciphertext, key): idx = 0 plaintext = [] cipher_without_symbols = [] for c in ciphertext: if c in string.ascii_lowercase: if idx < len(key): k = key[idx] else: k = cipher_without_symbols[idx - len(key)] p = f_key(c, k) plaintext.append(p) cipher_without_symbols.append(c) idx += 1 else: plaintext.append(c) return ''.join(plaintext) 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." known_plain = "tsukuctf" cipher_pos = 30 recovered_key = recover_key(ciphertext, known_plain, cipher_pos) print(f"Recovered key: {recovered_key}") plaintext = decrypt(ciphertext, recovered_key) print(f"\nDecrypted plaintext:\n{plaintext}") ``` 出てくる平文は`alo xok aqjoy 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}` :::info ところで、この問題は[ChatGPT o3](https://chatgpt.com/share/6815f396-198c-8010-b545-431b2fba6c24)が3分でやってくれるらしいですよ - _Eai_ ::: ## osint - Casca (366 solves) <details><summary>問題</summary> > 海が綺麗なこの日本の街は、かつてポルトガルのリゾート地との交流がありました。 > この写真のすぐ右側にはその記念碑が置かれています。記念碑に書かれている「式典の開催日」を答えてください。 > Format: `TsukuCTF25{YYYY/MM/DD}` > - Files - [casca.jpg](https://github.com/EaINT-HQ/challenge-attachments/blob/main/2025/tsukuctf/1/casca.jpg) </details> 書いた人: karubabu グーグル画像検索すると、関係がありそうなリンクが出てくる。 [お宮緑地・ジャカランダ遊歩道(静岡県熱海市) : 食で奏でる旅の記憶](https://gourmet-travelogue.doorblog.jp/archives/56226093.html) このサイトでは具体的な日付は分からなかったので、サイト内にある文字列で適当に検索すると以下が見付かった。 [熱海・ジャカランダそしてブーゲンビリアを楽しむ:                                お宮緑地(ジャカランダ遊歩道)(その1) | JINさんの陽蜂農遠日記 - 楽天ブログ](https://plaza.rakuten.co.jp/hitoshisan/diary/202206120001/) `TsukuCTF25{2014/06/06}` ## osint - curve (384 solves) <details><summary>問題</summary> > これは日本の有名な場所の一部です。あなたはこの写真の違和感に気づけますか? > フラグはこの場所のWebサイトのドメインです。 > 例: `TsukuCTF25{example.com}` > - Files - [curve.jpg](https://github.com/EaINT-HQ/challenge-attachments/blob/main/2025/tsukuctf/2/curve.jpg) </details> 書いた人: karubabu グーグル画像検索するとめっちゃ出てくる。 `TsukuCTF25{www.yokohama-landmark.jp}` ## osint - destroyed (204 solves) <details><summary>問題</summary> > [このTelegramの投稿](https://t.me/etozp/19319)の写真に写っている学校を特定してください。 > フラグフォーマットはその場所の座標の小数点第4位を四捨五入して、小数第3位までを`TsukuCTF25{緯度_経度}`の形式で記載してください。 > 例: `TsukuCTF25{12.345_123.456}` > **注意: この問題を解く過程で、戦争に関わる直接的な画像が表示される場合があります。** > > 23:14 GMT+9 追記: フラグを追加しました > </details> 書いた人: Eai いろいろこねくり回したんだけどうまくいかなくて、後でやり直したらさっくりできた まずTelegramの投稿はGoogle翻訳するとこんな感じ > Наслідки потрапляння ракет у гімназію Степненської громади. Після Другої світової знищене відбудовували полонені нацисти, після цієї війни відбудують рашисти. ステプノコミュニティ体育館にミサイルが命中した結果。第二次世界大戦後、破壊された地域は捕らえられたナチスによって再建され、この戦争後にはロシア人が再建することになる。 https://t.me/etozp/19319 体育館ってなんやねんとなるが、гімназіюでギムナジウム、ヨーロッパの中等教育機関という意味でこれGoogle翻訳は体育館にするらしい。中学や高校に相当するのかな? https://ja.wikipedia.org/wiki/ギムナジウム Степненської, ステップノは地名で、なんか?地域共同体?村が集まっている?よくわからなかったです {%preview https://uk.wikipedia.org/wiki/%D0%A1%D1%82%D0%B5%D0%BF%D0%BD%D0%B5%D0%BD%D1%81%D1%8C%D0%BA%D0%B0_%D1%81%D1%96%D0%BB%D1%8C%D1%81%D1%8C%D0%BA%D0%B0_%D0%B3%D1%80%D0%BE%D0%BC%D0%B0%D0%B4%D0%B0 %} https://maps.app.goo.gl/JYUeaQD7tNyx54aNA まあとにかく教育機関が爆撃されて、その場所を特定すればいいらしい karubabuが関連しそうなニュース記事を見つけてくれた {%preview https://vidbudova.zp.ua/zhyttya-pryfrontovoyi-stepnenskoyi-gromady-zaporizkoyi-oblasti-pid-chas-vijny-problemy-ta-vyklyky-fotoreportazh/ %} ![](https://vidbudova.zp.ua/wp-content/uploads/2024/05/gimnaziya-stepne2-2048x1365.webp) これって ![](https://hackmd.io/_uploads/HyQ22Xrxlx.jpg) ここだよなあ(Stepneの中央にある大きめの建物) `TsukuCTF25{47.798_35.304}` で通った --- ところで > 【OSINT/destroyed】 > 私たちのミスで、誤ったフラグを設定していました。そのため、正しいフラグを追加しました。 > [name=xryuseix] <small>[Discord - TsukuCTF 2025 #announcements](https://discord.com/channels/1027682779854549002/1027683780489646150/1368229923264925787)</small> これはどういう意味なんだ? ![](https://hackmd.io/_uploads/rkEYhyUeeg.jpg) 実は最初はLezhyne (Stepneの近く, ステプネン郡の一部)にあるこれかと思ってsubmitしてincorrect言われまくってたので方針変更してやり直したんだけど、もしかしてこれであってた?公式writeup待ち。 ## osint - rider (215 solves) <details><summary>問題</summary> > 遠くまで歩き、夕闇に消える足跡 > 煌めく街頭が、夜の街を飾る > 傍らの道には、バイクの群れが過ぎ去り > 風の音だけが残る > > 光と影の中、ふと立ち止まり思う > 私は今、どこにいるんだろう > > フラグフォーマットはこの人が立っている場所の`TsukuCTF25{緯度_経度}`です。ただし、緯度および経度は小数点以下五桁目を切り捨てたものとします。 > - Files - [rider.png](https://github.com/EaINT-HQ/challenge-attachments/blob/main/2025/tsukuctf/4/rider.png) - Hints 1. この詩に意味はありません。 </details> 書いた人: karubabu OTIと書かれた看板はインドネシアにあるフライドチキンチェーン店なので、ここの公式サイトにある店舗マップから探す。 [Kunjungi Kami - OTI Fried Chicken](https://otifriedchicken.com/kunjungi-kami/) 適当に見ていくと、OTIの看板近くに"KURO"と書かれたパンダの看板が見える場所がある。 [Google ストリートビュー](https://maps.app.goo.gl/kPJqVzL5ZiHL9B2C8) `TsukuCTF25{-7.3189_110.4971} ` ## osint - schnee (301 solves) <details><summary>問題</summary> > 素敵な雪山に辿り着いた!スノーボードをレンタルをして、いざ滑走! > フラグフォーマットは写真の場所の座標の小数点第4位を四捨五入して、小数第3位までを`TsukuCTF25{緯度_経度}`の形式で記載してください。 > 例: `TsukuCTF25{12.345_123.456}` > - Files - [schnee.jpg](https://github.com/EaINT-HQ/challenge-attachments/blob/main/2025/tsukuctf/5/schnee.jpg) </details> 書いた人: Eai とりあえずGoogle Lensに聞くと色々出てくる。スイスらしい。 [Chris Tiu | Grindelwald. Probably my favorite mountain village. Glad we chose this as our base to explore the Jungfrau region. The towering mountains... | Instagram](https://www.instagram.com/p/DAoNti2TZWd/?img_index=7) このインスタの投稿でグリンデルヴァルトにあるBuri Sportという店だと確認できるので、Google Mapで検索 [Google Maps](https://maps.app.goo.gl/sVnjAZaksPmBbSeM8) 答えをメモり忘れたけど46.6235636, 8.0399819くらいだったはず ## osint - buildings (253 solves) <details><summary>問題</summary> > あの建物が建ったら、また空が狭くなるんだろうな。 > フラグフォーマットはこの人が立っている場所の`TsukuCTF25{緯度_経度}`です。ただし、緯度および経度は小数点以下五桁目を切り捨てたものとします。 > - Files - [buildings.jpg](https://github.com/EaINT-HQ/challenge-attachments/blob/main/2025/tsukuctf/23/buildings.jpg) </details> 書いた人: karubabu ビル一つずつを画像検索すると、判別出来るビルがあるので。それを目印に探していく。 ![image](https://hackmd.io/_uploads/HyMHA5Vgxl.png) これは`ロイヤルパークス品川` そして、最も右手前にあるテカテカのビルが`Shibaura Crystal 品川` このふたつの位置が丁度良い塩梅になる位置がここ [Google street view 35.631806, 139.744194](https://maps.app.goo.gl/ANJggRUnPWi4j8j19) `TsukuCTF25{35.6318_35.6318}` ## osint - power (193 solves) <details><summary>問題</summary> > 力を感じてきた。 > > フラグフォーマットはこの人が立っている場所の`TsukuCTF25{緯度_経度}`です。ただし、緯度および経度は小数点以下五桁目を切り捨てたものとします。 > - Files - [power.jpg](https://github.com/EaINT-HQ/challenge-attachments/blob/main/2025/tsukuctf/28/power.jpg) </details> 書いた人: Eai 右側をGoogle LensするだけでデイリーポータルZの記事が出てきて将門塚であることがわかる。 {%preview https://dailyportalz.jp/kiji/shomonduka-tenji %} > ![image](https://hackmd.io/_uploads/rJprPMrgel.png) 細かい位置も書いてあるのでGoogle Mapsで当たりをつけてac https://maps.app.goo.gl/Pcg5iFRwGyFdV5oP6 ![image](https://hackmd.io/_uploads/ryyNuzHgxe.png) ここらへん `TsukuCTF25{35.6872_139.7627}` ## osint - hidden_wpath (14 solves) <details><summary>問題</summary> > 新しいブログサイトを作成したので、ぜひ見に来てください! > ちなみに隠されたページがあります😎 > > 注意点: ツールの使用は許可されていますが、短期間で大量にリクエストを送信しないでください。隠されたページのリンクは100文字以上あり、かつ推測不可能です。 > > [http://challs.tsukuctf.org:9000](http://challs.tsukuctf.org:9000) > - Hints 1. OSINTだけではなく、Webの知識が必要かもしれません。 2. 特定のページを開くと、フラグが手に入る処理が設定されています。 </details> 書いた人: fuyu 与えられたリンクを開くとブログが出てくる、WordPressなのでまずはREST APIを叩いてみることにした http://challs.tsukuctf.org:9000/wp-json/wp/v2/posts が特に得られるものはなかった http://challs.tsukuctf.org:9000/feed えあいに教えてもらった `/feed` (RSS) でも特に得られるものはなかった... 次に管理画面にアクセスしてみると下記のエラーが表示された http://challs.tsukuctf.org:9000/wp-admin/ ``` Notice: Function _load_textdomain_just_in_time was called <strong>incorrectly</strong>. Translation loading for the <code>404-solution</code> domain was triggered too early. This is usually an indicator for some code in the plugin or theme running too early. Translations should be loaded at the <code>init</code> action or later. Please see <a href="https://developer.wordpress.org/advanced-administration/debug/debug-wordpress/">Debugging in WordPress</a> for more information. (This message was added in version 6.7.0.) in /var/www/html/wp-includes/functions.php on line 6121 Warning: Cannot modify header information - headers already sent by (output started at /var/www/html/wp-includes/functions.php:6121) in /var/www/html/wp-includes/pluggable.php on line 1450 Warning: Cannot modify header information - headers already sent by (output started at /var/www/html/wp-includes/functions.php:6121) in /var/www/html/wp-includes/pluggable.php on line 1453 ``` `404-solution` というプラグインがインストールされていてエラーになっているらしい https://ja.wordpress.org/plugins/404-solution/ が、いったんスルーしてしまい wpscan の結果を眺めたり、いろいろAPIを叩いてみたものの何も得られるものがなかった `404-solution` が怪しそうなので、どうにかしてバージョンを取得できないか悩んでいたところ http://challs.tsukuctf.org:9000/?p=4 にアクセスするとリダイレクトが行われるが、その際レスポンスヘッダに `X-Redirect-By: 404solution-2.33.0/404-solution.php` が含まれているのをkarubabuが見つけてくれた ![image](https://hackmd.io/_uploads/S1toKMSgxl.png) このバージョンを元に該当しそうな脆弱性を探してみたところ以下を見つけた [NVD - CVE-2023-52146](https://nvd.nist.gov/vuln/detail/CVE-2023-52146) ログファイルから機密情報が漏洩するというもの `2.33.1` で修正されたとの事なのでソースコードの差分を見ると、怪しい箇所を見つけることができた https://github.com/aaron13100/404solution/commit/036b8a8d42a5af7387476febe2a9e443219aeb4b#diff-2bdde04f717ad63269d3eaf27462799a3b9ddc12559023b00f5417c7af7fe6c7L339 デバッグログのファイル名が `abj404_debug.txt` 固定なので誰でもアクセスすることができてしまっていたということらしい <details> <summary> 追いかけた処理 </summary> getDebugFilePath https://github.com/aaron13100/404solution/blob/2.33.0/includes/Logging.php#L338 getFilePathAndMoveOldFile https://github.com/aaron13100/404solution/blob/2.33.0/includes/Logging.php#L367 ABJ404_PATH https://github.com/aaron13100/404solution/blob/develop/404-solution.php#L34 plugin_dir_path https://developer.wordpress.org/reference/functions/plugin_dir_path/ </details> 処理を追うことで下記のURLにデバッグログが配置される事が分かったのでアクセスする http://challs.tsukuctf.org:9000/wp-content/uploads/temp_abj404_solution/abj404_debug.txt デバッグログを見ることで、隠しページのURLが分かるのでアクセスするとGoogle ドキュメントにリダイレクトされてそこにフラグがある http://challs.tsukuctf.org:9000/this_is_a_secret_page_Gl4VzyIIKfwPK7xVcjY8RzpgFCOlXKdgmBFLmksxNF2nF3olLNwQLcnYMLGboSG5x4K7BqpdPdXQJBjMjcHmqIG7fTAbMKDn5rdo https://docs.google.com/document/d/16K84AlbPOBsGpP14qfpOxBQVlc4J5-Aq4QhbVf6OYlg `TsukuCTF25{b3_c4r3fu1_w17h_w0rd9r355_91u61n5}` 昔 WordPress をいろいろ触ってたのが役にたったかもしれない問題だった ## tsukushi - welcome (882 solves) <details><summary>問題</summary> > TsukuCTFの[公式Discord](https://discord.gg/xNgh3a6Ynp)の「announcements」チャンネルにフラグが記載されています。 > Flag Format: `TsukuCTF25{}` > </details> `TsukuCTF25{welcome_to_TsukuCTF_2025!}` ## web - len_len (451 solves) <details><summary>問題</summary> > `"length".length` is 6 ? > > Connection Info: > curl http://challs.tsukuctf.org:28888 - Files - [len_len.zip](https://github.com/EaINT-HQ/challenge-attachments/blob/main/2025/tsukuctf/27/len_len.zip) </details> 書いた人: Eai ```js const array = JSON.parse(sanitized); if (array.length < 0) { // hmm...?? return FLAG; } ``` みそはここであり、パースされたオブジェクトのlengthが0より小さい必要がある ESの仕様として、Array.lengthがnegativeな値になることはない > Every Array has a non-configurable "length" property whose value is always **a non-negative integral Number** whose mathematical value is strictly less than 2**32. > [ECMAScript® 2026 Language Specification](https://tc39.es/ecma262/multipage/ordinary-and-exotic-objects-behaviours.html#sec-array-exotic-objects) ではArrayではなく、たまたまlengthというプロパティを持っているObjectだったら? ```bash curl -X POST -d 'array={"length":-1}' http://challs.tsukuctf.org:28888 ``` `TsukuCTF25{l4n_l1n_lun_l4n_l0n}` ## web - flash (170 solves) <details><summary>問題</summary> > 3, 2, 1, pop! > > [http://challs.tsukuctf.org:50000/](http://challs.tsukuctf.org:50000/) > - Files - [flash.zip](https://github.com/EaINT-HQ/challenge-attachments/blob/main/2025/tsukuctf/30/flash.zip) </details> 書いた人: Eai なんかフラッシュ暗算みたいなことさせられるうえに途中から問題が見えなくなる。ナメてる? とりあえずコードを読んで、 ```python with open('./static/seed.txt', 'r') as f: SEED = bytes.fromhex(f.read().strip()) ``` このseedがわかっちゃえばどうにでもなるよな〜と思いながら http://challs.tsukuctf.org:50000/static/seed.txt にアクセスしてみたらなんか普通にseedがある。 :thinking_face::question: どうにでもなるのでどうにでもする。適当に一回実行してsession_idとともにChatGPTに投げる: [LCG 数値計算](https://chatgpt.com/share/68179f7c-82dc-8010-bf96-8ff3df88e773) 暗算の答えがわかったので回答欄に入れてフラグゲット `TsukuCTF25{Tr4d1on4l_P4th_Trav3rs4l}` --- フラグにパストラバーサルと書いてあることからもわかるように、本来はnginxのパストラバーサルを使う想定だったらしい。 そういえば前に見たな [スラッシュの有無だけでセキュリティにとんでもない大穴が空いてしまうNginxのありがちな設定ミスについて実例を踏まえて専門家が解説 - GIGAZINE](https://gigazine.net/news/20230708-nginx-alias-traversal/) 今回のnginx.conf: ```nginx.conf server { listen 80; server_name localhost; location / { proxy_pass http://web:5000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } location /images { alias /var/www/flash/static/images/; } } ``` たしかに`location /images`になっている ## web - YAMLwaf (71 solves) <details><summary>問題</summary> > YAML is awesome!! > > ``` > curl -X POST "http://challs.tsukuctf.org:50001" -H "Content-Type: text/plain" -d "file: flag.txt" > > (mirror) > curl -X POST "http://20.2.250.108:50001" -H "Content-Type: text/plain" -d "file: flag.txt" > ``` > - Files - [YAMLwaf.zip](https://github.com/EaINT-HQ/challenge-attachments/blob/main/2025/tsukuctf/29/YAMLwaf.zip) </details> 書いた人: fuyu `flag.txt` を読み込む事ができれば、フラグを取得できるが `flag` や `/` といった文字列を含むリクエストは送れないようになっている YAMLの仕様の穴(エイリアス機能とか)を突いてフラグを取得するのかなとも考えたけど前述の処理でこれらも塞がれていた お手上げ状態だったので何かヒントがないかと検索してみたところ、下記の記事を見つけることができた [CTF: Best Web Challenges 2022 | XS-Spin Blog](https://blog.arkark.dev/2022/12/17/best-web-challs/#%E3%82%B7%E3%83%B3%E3%83%97%E3%83%AB%E9%83%A8%E9%96%80-simplewaf---corctf-2022) `fs.readFileSync` はパラメータとして `<string> | <Buffer> | <URL> | <integer>` を受け付けていて `URL` のような形のオブジェクトを渡すことでファイルパスにURLエンコーディングが使えるようになるらしい [File system | Node.js v23.11.0 Documentation](https://nodejs.org/api/fs.html#fsreadfilesyncpath-options) ``` curl -X POST "http://20.2.250.108:50001" -H "Content-Type: text/plain" -d 'file: href: "x" origin: "x" protocol: "file:" hostname: "" pathname: "%66lag.txt"' ``` `TsukuCTF25{YAML_1s_d33p!}` 参考にした writeup がなかったらこの問題を解くことができなかった writeup を残してくれた先人に大感謝... # 解けなかった問題 ## crypto - xortsukushift (34 solves) <details><summary>問題</summary> > つくし君とじゃんけんしよう。負けてもチャンスはいっぱいあるよ! > フラグフォーマットは `TsukuCTF25{}` です。 > > Connection Info: > nc challs.tsukuctf.org 30057 - Files - [server.py](https://github.com/EaINT-HQ/challenge-attachments/blob/main/2025/tsukuctf/8/server.py) </details> 書いた人: karubabu 300回もチャンスがあるので、299回くらい負けて情報をとってseedを特定し、最後に294連勝する…みたいな問題だと思うのだけれど、さっぱり分からずどうにもならなかった。 そもそも`rng = xor_tsuku_shift(seed=secrets.randbits(64))`ってことはseedは2^64くらいあるってことだし、普通に探しても見付かるようなものではないきがする。何か上手い方法があるのかしら。 chatgptに聞くと、"まず12回ほど負けてパターンを取得した後、2^32個のseedから候補を100から1000個くらい集めて、失敗しながら候補を狭めていく"という方法とそのコードが貰えた。 ```python= from pwn import * from multiprocessing import Pool, cpu_count import itertools context.log_level = 'error' # 必要に応じて変更可 class xor_tsuku_shift: def __init__(self, seed): self.a = seed def shift(self): self.a ^= (self.a << 17) & 0xFFFFFFFFFFFFFFFF self.a ^= (self.a >> 9) & 0xFFFFFFFFFFFFFFFF self.a ^= (self.a << 18) & 0xFFFFFFFFFFFFFFFF return self.a & 0xFFFFFFFFFFFFFFFF def janken_result(player, enemy): return (player - enemy + 3) % 3 HOST = "challs.tsukuctf.org" PORT = 30057 def determine_tsukushi_hand(my_hand, result_line): for tsukushi in range(3): if janken_result(my_hand, tsukushi) == 0 and "Draw" in result_line: return tsukushi elif janken_result(my_hand, tsukushi) == 1 and "win" in result_line: return tsukushi elif janken_result(my_hand, tsukushi) == 2 and "lose" in result_line: return tsukushi return None def observe_hands(r, num_observe=16): observed = [] for i in range(num_observe): r.recvuntil(b"--- Round") r.recvline() r.sendline(b"0") r.recvuntil(b"Tsukushi:") result_line = r.recvline().decode().strip() tsukushi_hand = determine_tsukushi_hand(0, result_line) if tsukushi_hand is None: return None observed.append(tsukushi_hand) if "Draw" in result_line or "lose" in result_line: continue return observed def check_seed(args): seed, observed = args rng = xor_tsuku_shift(seed) for h in observed: if rng.shift() % 3 != h: return None return seed def find_candidate_seeds_parallel(observed, search_bits=36, max_candidates=1000): print(f"[*] Searching candidate seeds (2^{search_bits})...") candidates = [] with Pool(processes=cpu_count()) as pool: seed_range = range(2**search_bits) args = zip(seed_range, itertools.repeat(observed)) for result in pool.imap_unordered(check_seed, args, chunksize=10000): if result is not None: candidates.append(result) if len(candidates) >= max_candidates: break return candidates def select_correct_seed(r, candidates, skip, additional_hands=10): all_observed = [] for i in range(skip + additional_hands): r.recvuntil(b"--- Round") r.recvline() r.sendline(b"0") r.recvuntil(b"Tsukushi:") result_line = r.recvline().decode().strip() tsukushi_hand = determine_tsukushi_hand(0, result_line) if tsukushi_hand is None or "lose" in result_line or "Draw" in result_line: return None all_observed.append(tsukushi_hand) for seed in candidates: rng = xor_tsuku_shift(seed) if all(rng.shift() % 3 == h for h in all_observed): return seed return None def win_the_game(r, seed, skip): rng = xor_tsuku_shift(seed) for _ in range(skip): rng.shift() for i in range(skip, 294): r.recvuntil(b"--- Round") r.recvline() tsukushi = rng.shift() % 3 my_hand = (tsukushi + 1) % 3 r.sendline(str(my_hand).encode()) r.recvuntil(b"Tsukushi:") result_line = r.recvline().decode().strip() print(f"[Round {i:03}] {result_line}") if "win" not in result_line: print("[!] Unexpected result. Failed.") return print(r.recvall(timeout=5).decode()) def main(): while True: r = remote(HOST, PORT) r.recvuntil(b"tries.") observed = observe_hands(r, num_observe=16) if observed is None: r.close() continue print(observed) candidates = find_candidate_seeds_parallel(observed, search_bits=36) true_seed = select_correct_seed(r, candidates, skip=len(observed)) if true_seed is None: r.close() continue win_the_game(r, true_seed, skip=len(observed) + 10) r.close() break if __name__ == "__main__": main() ``` でもこれ実行して1時間経った今でも候補すら出揃わない。そこまで時間がかかるとserverとの接続が切れてると思うし、やっぱり駄目そう。なんなのよほんと。seed値御用意しなさいよ。 ## crypto - PQC1 (21 solves) <details><summary>問題</summary> > 今度は秘密鍵を最初の 128 バイトしかあげません! > - Files - [prob.py](https://github.com/EaINT-HQ/challenge-attachments/blob/main/2025/tsukuctf/7/prob.py) - [output.txt](https://github.com/EaINT-HQ/challenge-attachments/blob/main/2025/tsukuctf/7/output.txt) </details> ## crypto - PQC2 <details><summary>問題</summary> > 今度は秘密鍵の最初の 294 バイトをもらいます! > - Files - [prob.py](https://github.com/EaINT-HQ/challenge-attachments/blob/main/2025/tsukuctf/25/prob.py) - [output.txt](https://github.com/EaINT-HQ/challenge-attachments/blob/main/2025/tsukuctf/25/output.txt) </details> ## pwn - easy_kernel (12 solves) <details><summary>問題</summary> > If you're new to kernel challenges, check out [this guide](https://pawnyable.cafe/linux-kernel/). > You can download the handouts from [here](https://drive.google.com/file/d/1dxELzSknpJ6MsAZhnPXw1_8M-_DHMJsL/view?usp=sharing). > The flag is in /dev/sdb. Good luck and have fun! > > Connection Info: > nc challs.tsukuctf.org 19000 </details> 書いた人: fuyu Kernel Exploitation系ははまったく分からず手も足も出なかった この領域は勉強が必要だなと感じた 以下は考えていたこと、やってみたことを今後の備忘録となるように書いていく ただ知識ゼロから初めたので理解が間違っている部分が大半かもしれない 与えられたソースコード類のなかに `rootfs.ext3` があるのでこれをマウントすることでローカル環境に簡単にプログラムを配置することができた ``` sudo mount rootfs.ext /ctf ``` 新しくビルドしたときは仮想マシンを再起動する必要があった、またビルドして仮想マシンを再起動しても変更が反映されない事が何度かあったマウントオプションの設定を変える必要があるのかもしれない `run.sh` に `-gdb` オプションを追加することでGDBでカーネルデバッグをすることができた (おそらく)これで最低限の準備は整ったのでカーネルモジュールのソースコードを眺めながら脆弱な箇所を見つける作業を始めた ソースコードを読むと `module_ioctl` の中でいろいろやっていて、この辺りの処理の脆弱性を突くことで特権昇格ができそうだと考えた 初めの方は問題文にある[ハンズアウト](https://pawnyable.cafe/linux-kernel/index.html)を参考に `open()` した `fd` に対して `read()` や `write()` を実行したりしてみたけどこれは意味がなかった ```c int fd = open("/dev/vuln", O_RDWR); char buf[0x800]; memset(buf, 'A', 0x800); // 意味がなかったコード write(fd, buf, 0x800); ``` これは `file_operations` という構造体に、デバイスファイル( `/dev/foo` )への操作(システムコール?)が行われたときどういった処理を呼び出すかが設定されているが `ioctl` のみ設定されているため なので、`ioctl` 経由で操作ができるように下記のようなコードを書いた ```c // from src/vuln.c typedef struct { size_t size; char *data; } request_t; #define CMD_ALLOC 0xf000 #define CMD_WRITE 0xf001 #define CMD_SIZE 0xf002 int fd; int cmd_alloc() { request_t *null_request_ptr = NULL; return ioctl(fd, CMD_ALLOC, &null_request_ptr); } int cmd_write(char *data, size_t size) { request_t req = { .size = size, .data = data }; return ioctl(fd, CMD_WRITE, &req); } int cmd_free() { request_t *null_request_ptr = NULL; return ioctl(fd, CMD_FREE, &req); } int main() { fd = open("/dev/vuln", O_RDWR); if (fd == -1) return 1; printf("[+] alloc: %x\n", cmd_alloc()); printf("[+] write: %x\n", cmd_write("Hello", 6)); printf("[+] free: %x\n", cmd_free()); } ``` C言語やLinuxプログラミングに慣れていないので普通に動かすにも時間が結構時間が掛かってしまった これで `ioctl` 経由で呼び出すことができるようになったはず... 実装した `cmd_write` で Buffer Overflow を試してみるも特に何も起こらないようだった ```c char buf[0x30]; memset(buf, 'A', 0x30); cmd_write(buf, 0x20); // 何も起こらない? cmd_write(buf, 0x30); // 失敗する(これは書き込めないように実装されている) ``` 次は Heap Overflow を起こしてみることにした またハンズアウトを参考に explot コードを書いてみたがうまく動作させることができなかった 最初、ハンズアウトを理解せずコピペしてしまい `/dev/ptmx` (擬似端末マスタ)をたくさん開いてなんかゴニョゴニョする!と思ってたがこれは間違いだった ```c spray[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY); ``` このスプレー部は破壊したいオブジェクトのサイズに合わせて変更する必要があった [ここ](https://ptr-yudai.hatenablog.com/entry/2020/03/16/165628)に破壊に使えそうなオブジェクトの一覧がある 今回は `0x20` なので `seq_operations` が該当しそう (おそらく)正しいスプレーコードは下記になる ```c spray[i] = open("/proc/self/stat", O_RDONLY | O_NOCTTY); ``` ただスプレー部分を直してもコピペコードでは特権昇格を行うことはできず、実際にカーネルのメモリを確認してどうなっているかデバッグしていく必要がありそうだった ハンズアウトを読みながら、IDAやgdbを使いブレークポイントを設定してメモリを見て...というのをいろいろ試してみたもののうまくいかず、夜遅くということもあり眠くなってしまい時間切れとなってしまった IDAで調査をするときにアセンブリと格闘する必要があったけど、CTFの参考になればと数日前からアセンブリを書いて遊んでたのでアセンブリを読むのがあまり苦痛ではなくなってたのも良かった(ただ6502だったのでx86とは大きく違った) ## pwn - new_era (4 solves) <details><summary>問題</summary> > When you cannot control RIP, what will you do? > You can download the handouts from [here](https://drive.google.com/file/d/1OiWp82a0jWXphuN6J5NBQZVjm3dM_N8z/view?usp=sharing). > The flag is in /dev/sdb. Good luck and have fun! > > Connection Info: > nc challs.tsukuctf.org 19002 </details> ## pwn - xcache (5 solves) <details><summary>問題</summary> > Looks easy but... > You can download the handouts from [here](https://drive.google.com/file/d/1X0l5e8tbustKC0mFu8-jje8Zh4tnOVuc/view?usp=sharing). > The flag is in /dev/sdb. Good luck and have fun! > > Connection Info: > nc challs.tsukuctf.org 19001 </details>