バックエンドチームの近松です。
カンムでは、Go Conference 2025 開催前に Capture The Flag (CTF) を公開していました。皆さん、お楽しみいただけたでしょうか?
本記事では、CTF の模範解答および作問にまつわる裏話を紹介します。
作問担当者
今回の CTF では、過去に作問経験がある knee を筆頭に、今回初めて作問を担当する kshun、近松(nchika)の合計3名で作問しました。
- knee(CTO):エグゼクティブプロデューサー担当
- kshun(セキュリティチーム):作問担当
- 近松(nchika)(バックエンドチーム):作問、実装担当
余談ですが、近松は不正対策の業務を担当しているため、2025年4月〜7月の間に CTF を通してセキュリティの勉強をしてきました。例えば、ハッキングラボの作り方や HackerOne で、CTF の経験を積んできました。
その流れで、今回は作問担当として立候補しました。
想定ターゲットユーザー
CTF 初心者や未経験者を想定しました。
フラグ一覧
今回、レシピサイトに埋め込んだフラグ(注:明示的にフラグと書いていなかったものを含む)は、以下の3点です。
- 他人のレシピを閲覧(権限管理の不備)
- ログインフォームから全ユーザー名、全パスワードを取得(不適切なSQL)
- 複数のバイナリを結合し、画像を生成(観察力の要求)
「他人のレシピを閲覧」に関する解説
まず、CTF 初心者や未経験者は、ログインフォームから素直にログインするだろうと想定しました。ログイン後は、下図が示すようにダッシュボード画面からレシピ詳細画面(ぎょうざ、いくらとポテト、ピザ)を順番に確認する人が多いと想定しました。

レシピ詳細画面に遷移すると、特に API を実行できそうなボタンがありません。存在するのは、作問者が作った料理の写真と調理手順だけです。

ページから目を離してみましょう。
注意深く URL を観察すると、「レシピ詳細画面は連番を振られて管理されていそうだ」「権限の不備があれば、他のレシピ詳細ページにアクセスできそうだ」と予想できます。作問者としてはそのように予想して欲しかったので、1, 2, 3 と番号を振らずに、意図的に2, 3, 5と欠番がある状態で URL に番号を振って実装しました。
- ぎょうざ:http://localhost:8080/recipe/2
- いくらとポテト:http://localhost:8080/recipe/3
- ピザ:http://localhost:8080/recipe/5
/recipe/4にアクセスすると、さしみ料理のページに辿り着きます。

おめでとうございます🎉このページはフェイクです。
さしみ料理のページは、フラグをゲットしたと錯覚させるために用意しました。実際のところはもう一つ理由があり、料理の画像が余っていたので、フェイクページで画像を消費しようと考えました。なお、正解のページは、/recipe/13に仕込んであります。若い番号(= /recipe/4)で隠されたページを見つけたけれども、特にヒントがなくて困る人が出てくるといいな、と思いながら実装しました。
別解としては、curl などのコマンドを利用して、HTTP ステータスが404以外のページを集める方法を想定していました。私が過去に解いた CTF にはこのパターンがありました。
ログインフォームから全ユーザー情報を取得
次に怪しいのは、ログインフォームです。
CTF で自由入力できるテキストエリアを見ると、SQL インジェクションが決まる可能性を考える人がいることでしょう。そのような方達のために、今回はツール(例:sqlmap)を使わなくてもお手軽に SQL インジェクションできるログインフォームを作りました。

このログインフォーム(正確には認証処理)は凄まじく脆弱で、以下のような実装になっています。ユーザーから受け取ったユーザー名とパスワード(入力値そのまま)で DB を検索します。パスワードは、平文で DB に格納されています。怖いですね。
query := fmt.Sprintf("SELECT username, password FROM users WHERE username='%s' AND password='%s'", username, password)
このSQLクエリは、プレースホルダーを用いていません。プレースホルダーがある場合は、
の流れになります。言い換えると、ユーザーが不正な入力値を渡して SQL 構文が変わった場合、SQL 実行時にエラーとなります。
さて、SQL インジェクションの模範解答を以下に示します。例えば、ユーザー名に kanmu' OR '1'='1' --、パスワードに任意文字列を入力します。この例では、WHERE 句が`username='kanmu' OR '1'='1'となり、OR '1'='1'は常に true なので、全ユーザーの情報が取得できます。--部分でパスワード条件をコメントアウトしています。
SQL インジェクションが決まると、全ユーザーの情報が綺麗に整形されて表示されます。


なお、SQL インジェクションを問題に組み込んだ理由は、CSV に SQL を実行できる自作ライブラリ(nao1215/filesql)がサーバーでキチンと動作するかを試してみたかったからです。
バイナリ結合による画像生成
SQL インジェクションで取得した他ユーザー情報でログインすると、ステーキソースのレシピが表示されます。ステーキソースのページは、先ほど説明した正解のページ(/recipe/13)です。

ステーキソースのレシピ詳細画面の下部には、フラグが示されています。このフラグボタンを押すと、flag.zipのダウンロードが始まります。

flag.zipは、パスワードがかかっています。ここでのパスワード突破方法は、以下の2パターンを想定していました。
- SQL インジェクション結果を利用(zip ユーザーのパスワードが、
flag.zipのパスワード) - ZIP パスワードをツールでクラック(例:John the ripper の利用)
解答者が zip ユーザーと zip ファイルを結びつけられない可能性を考え、ツールで簡単にクラックできる脆弱なパスワード("qwerty123456")としました。 zip ファイルを展開すると、以下のファイルが入っています。
- apple
- garlic
- mirin
- onion
- soy_sauce
- README.md
README には、以下の文章が書かれています(読みやすいように改行を加えました)。
$ cat README.md ## Go Conference 2025 ノベルティの受け取り方 本ディレクトリ内にある食材情報の中から、 ノベルティ(ステーキソース)に含まれる隠し味を カンム社員に伝えてください。 見事正解された方には、ノベルティを進呈します。
この README を読み、「ステーキソースのページ(/recipe/13)に何かヒントが書かれているな」と推測してもらうことを期待しました。ステーキソースの調理手順を確認すると、以下の食材が使われています。

この調理手順を読むと、「zip ファイルに入っていたファイル(食材名称が英語で書かれたファイル)」と「調理手順に書かれている食材」が一致しています。しかし、何をすれば良いか分からないで詰まってしまった人がいると思われます。ヒントは、調理手順に書かれている「全てが混ざり合い」の部分で、この文章はファイルを結合すると何かが表れることを示唆しています。
もう一つのヒントは、ファイル自体から情報を得ることです。バイナリアンであればバイナリを覗いたかもしれませんが、作問者としては file コマンドの利用を想定しました。以下のように file コマンドを実行すると、どうやら PNG ファイルが含まれていることが分かります。
$ file apple apple: data $ file garlic garlic: data $ file mirin mirin: data $ file onion onion: PNG image data, 100 x 272, 8-bit/color RGBA, non-interlaced $ file soy_sauce soy_sauce: Clarion Developer (v2 and above) help data
ここまでの流れで、「ステーキソースの調理手順通りにファイルを連結すると、PNG 画像になりそうだ」と予想できます。もう一度、ステーキソースの調理手順を確認して、ファイルを結合してみましょう!
$ cat onion apple garlic soy_sauce mirin > restored.png
隠し味(フラグ)が現れました!お疲れ様でした!

作問にまつわる裏話
CTF の作問は、kshunが「問題の答えは、ステーキソースの原材料のうちのどれかにしておく」と発言したことをキッカケに進んでいきました。

この発言を受けて、ノベルティ(ステーキソース)の隠れていない隠し味を答えにしようと、近松は考えました。

その後は、おおよそ以下のような流れです。
- kshun :「バイナリファイルから画像(フラグ)を作ればよい」と発案
- 近松:「ファイル名が原材料名のバイナリファイルを複数渡して、結合させる」と発案
- 近松:分割したファイルをシェルスクリプトに埋め込む案を実装
- 近松:シェルスクリプトでは可搬性の問題があるので、Go で再実装
バイナリファイルを分割してから結合し直す案は、「料理は混ぜる工程がある」、「ファイルを食材に見立てよう。分割ファイルを結合して戻すことは、分霊箱(horcrux)が実証している」と考えて、発案しました。ここで分霊箱に発想が至った理由は、私自身分かりません。
初期実装でシェルスクリプトを利用した理由は、画像を5分割してから Go 製のバイナリに埋め込んで、取り出すのが大変だったからです。「バイナリを操作するコマンド」や「バイナリからファイルを取り出す binwalk コマンド」を使って、正確にファイルを取り出せないので「これは殆どの人が対応できない」と判断しました。そこで、シェルスクリプトに base64 エンコードしたバイナリを埋め込んで、解答者に取り出してもらう方法で検証を進めました。
しかし、途中で「シェルスクリプトでは、Linux、Mac で動作保証するのが大変。Windows で保証するのはもっと厳しい」と気づきました。この辺りの大変さは、Software Design 2024年12月号 第1特集第4章に書いてあります(著者:近松)。
可搬性であれば、Go を採用すべきです。しかし、Go 製のバイナリから分割ファイルを取り出せません。どうにか binwalk でファイルを抽出できないかと考えた時に、「zip にまとめれば取り出せる」と気づきました。ここからの実装はとても早く、 Go であれば Web アプリも簡単に実装できるので、フラグが想定より増えました。そう、SQL インジェクションや権限不備のフラグは、近松がノリで実装しました。ノリノリで実装した結果、バイナリに埋め込んだレシピ画像が多すぎて、binwalk で zip ファイルを取り出せなくなりました。泣く泣く、ダウンロードボタンを設けて、zip ファイルをダウンロードする仕様にしました。
最後に
カンムでは一緒に働けるメンバを募集しています。私と一緒に CTF を作問しましょう。