こんにちは、デジタルペンテスト部(DP部)のst98です。
皆さんは、CTF(Capture The Flag)で出題するために問題を作られたことはおありでしょうか。そもそもCTFとは何かについては、たとえば「CTFの紹介と始め方 - うさぎ小屋」のようなブログ記事を参照ください。この記事では、私がCTFで問題を作るときに、どんな流れで、どんなことに注意しているかについて書いていきたいと思います。
なお、こうするのが望ましいというベストプラクティス集ではなく、「st98はこう思っているらしい」程度の記事*1として、ひとつのやり方と受け取っていただければと思います。また、私が得意な分野がWebであることから、特にWebカテゴリ(Web問)の作問に偏った内容となっています。記事中や記事の最後で参考になる記事をいくつか紹介しますので、CTFにおける作問だけでなく運営をどうすればよいか知りたい、ほかの方の見解も知りたいというような場合はそちらも参照ください。
作問の流れ
問題のコンセプトを決める
その問題で何をテーマとするかをまず決めるということが、作問にあたって重要となります。「ある程度Webの知識を持っているが、CTFを遊んだことはない人でも解けるものにする」のように大まかな方向性を決めて深掘りしていくのもよいでしょう。あるいは、それを飛ばして「ゲームのガチャを題材として、レースコンディションによって、所持している石で本来可能な回数以上にガチャを引く問題にする」や「個人的に気になっているのでGodotを使う」といったように、題材とするアプリケーションから考えたり、どういった脆弱性や技術を使うかというところから出発してもよいかもしれません。
私の場合は、後々問題として完成させられるかどうかは別として*2、普段からScrapboxに問題のアイデアを書き溜めています。「Array.prototype.at
を使う問題」や「DOM Clobberingでpolyfillを潰す」のようなタイトルを付けつつ、問題として仕立て上げるならばどうするかをまとめています。なんとなく思いついたアイデアを書き留めたり、情報収集をする中でCTFの問題に利用できそうなトピックがあればメモをしたりといった様子です。CTFを遊ぶ中で、もう少し制約を厳しくしても解けるのではないか、この問題では使えないテクニックだったけれども、作問に役立てるのではないかと思うことが多々あり、そのようなネタを書き留めることもあります。
必要になった際にこのメモを参照して、アイデアに肉付けしていき実装するという形で問題をよく作っています。ふさわしいネタがなければ、HackTricksやPayloadsAllTheThingsのようなテクニック集であったり、SECCON CTFやCakeCTFといった過去問を公開しているCTFのリポジトリであったりを眺めつつ、新しいネタを考えることもあります。
CTFの作問に限らない内容となりますが、「作問、動画作成の風景 - くれなゐの雑記」というブログ記事ではどうやってこのような「ネタ」を集めて問題や動画にできるかが述べられています。やはり、普段からネタを集めておく*3といざ作問するというときに困らないかと思います。
実装する
ここまでで決めたコンセプトをもとに、問題の実装をしていきます。まずは題材としたい攻撃手法が自然に使える状況や、題材としたい脆弱性が自然に生まれうる状況とはどういうものかを考えます。作りたいサービスや使いたい技術が先に決まっている場合は、そこからどうCTFの問題にできるか、つまり逆にそのサービスや技術に関連したどんな脆弱性が起こりうるかを考えます。
たとえば、XSS(クロスサイトスクリプティング)を題材とした問題を作りたいとしましょう。XSSはユーザ入力を適切に無害化せず出力することで起こりうるので、あるユーザの入力が別のユーザからも見られるような機能がほしくなります。ユーザ同士で特殊な記法のメッセージを送受信できるチャットサービスを作った上で、メッセージのレンダリング処理にXSSを仕込むのはどうでしょうか。
どんなサービスにどんな脆弱性を埋め込むかが決まって、ぼんやりとしていたコンセプトが徐々に固まってきました。もう少し具体的に、どのような脆弱性を使って、どのような手順で攻撃をすればフラグが得られるようにするかという流れ(想定解法)を考えます。
先程の例を用いると、XSSを題材にするということで、ユーザが送信したメッセージを受け取ってWebブラウザで閲覧するクローラが必要になります。フラグの置き場所について、このクローラがログインしているユーザは、自分自身にメッセージを送ることでメモ代わりにしており、その中のひとつにフラグがあるということにしておきましょう。
したがって、攻撃者(つまり参加者です)が行うべきアクションは、クローラが使用しているユーザ宛に、このフラグの含まれるメッセージを窃取するペイロードを送りつけることになります。どんな形でXSSを埋め込むかはまだ決まっていませんが、「特殊な記法」が使えるチャットということで、リンクを張れるようにする記法を <a>
タグへ変換する際にリンク先のURLが javascript:
スキームである場合にも許容してしまい、これをクローラがクリックしてしまうためにXSSが発生するというのはどうでしょうか。
Webアプリケーションの場合は「ガワ」を作らなければなりません。あまり問題の背景やフロントエンドに凝りすぎても、ただコードが肥大化し、特にクライアントサイドが重要な問題では本質部分を見つけづらくするだけで参加者としては余計なお世話ですが、とはいえプレーンなHTMLでは味気ないでしょう。参加者の邪魔をしない範囲でそれっぽい「ガワ」を作りたいものです。
私は数十行程度の短いCSSを書い(て使いまわし)たり、「部内イベント用CTFの作問をした話 - 天才クールスレンダー美少女になりたい」というブログ記事で紹介されているように、CSSファイルを読み込ませるだけでそれっぽい見た目にしてくれるクラスレスCSSフレームワークを使ったりして(ごまかして)います。
さて、ここまでの作業で、何をどのように実装すべきかがはっきりしてきたはずです。先程から使っているチャットサービスの例では、作るべきものとして以下の3つの要素があります:
- バックエンド: ユーザ登録やログインといった機構を持ちつつ、ユーザからのメッセージの送信を受け付けて、メッセージを受信したユーザから要求があればその内容を返せるようにする
- フロントエンド: ユーザ登録、ログイン、メッセージの送信・表示などの処理をいい感じにバックエンドサーバとの通信で実現するHTML, CSS, JavaScriptを適当に用意する。「特殊な記法」のメッセージをレンダリングする処理も、XSSを含む形で必要とする
- クローラ: 自分自身にフラグをメッセージとして送っている、ある特定のユーザとして振る舞う。このユーザが参加者からメッセージを受け取ると、送られてきたメッセージの表示ページをWebブラウザで開き、メッセージ中にリンクがあればクリックする
これらを実装していくことで、問題のプロトタイプが出来上がります。もっとも、ある程度組み上がったところで、試しに上述の攻撃の流れを試してみたところ考えていたよりも面白くない、簡単すぎると思うこともあります。より面白くするには解法にどんな修正を入れればよいだろうかと手を入れていくことも往々にしてあります。満足するまで調整を繰り返しましょう。満足したら、必要があれば、Dockerでデプロイできるよう Dockerfile
や compose.yaml
を用意する、デプロイの手順をまとめておくなど、問題サーバでデプロイ可能な形に仕上げておきます。
問題としての体裁を整える
問題を出題する前に、問題名や問題文、問題に添付するファイル、フラグといった、問題の本質部分ではないものの出題に必要な情報を決めておく必要があります。それぞれ見ていきましょう。
問題に添付するファイル
まず問題に添付するファイルですが、これはWebアプリケーションをデプロイするようなWeb問では、ほとんどの場合はソースコードが当てはまるでしょう。ソースコードを提供しない(ファイルを添付しない)ブラックボックスなWeb問も考えられますが、そうすべき強い理由がない限りは避けるべきであると個人的には考えています。Web問ではできるだけソースコードを添付したい*4ところです。
サーバサイドに主なロジックがある問題では、ブラックボックスであれば(ランダムに、あるいは勘に基づいて行った)入力と出力の対応をもとにバックエンドのロジックを推測しなければなりません。それはそれで得られるものがありますが、ブラックボックスである必要性がないのであれば、本質ではない作業に時間を費やしてほしくはありません*5。最初からソースコードを提供することで、その問題で何をやってもらいたいか、あるいは学んでもらいたいかと考えている部分に集中してほしいと考えています。
ソースコードに Dockerfile
や compose.yaml
といったファイルを同梱することで、フラグ以外は問題サーバと同じ環境を参加者が容易に用意できるようになります。参加者の手元で細かくデバッグできるようになるほか、フラグの入手以外はローカルで試してもらえるようにして問題サーバの負荷を少しでも減らしたいという打算的な目的もあります。デプロイ用のファイル群をそのまま配布するのが楽ですし、私も基本そのようにしていますが、フラグをダミーのものに置き換えておくのを忘れないようにしましょう。
問題名や問題文
問題名と問題文ですが、正直なところを言うと、私はいずれもなくてもよい(なんでもよい)ものの、あると嬉しい雰囲気作りのための「フレーバーテキスト」程度にとどめて、これらを確認せずともソースコードを読めば解けるようにするのが理想だと思っています。
もっとも、ソースコードを読まないとどういうサービスか、どこにフラグがあるか分かりにくいということも考えられます。「誰でも投稿できるブログサービスを作りました」や「管理者が投稿した秘密の記事を閲覧できるでしょうか」のように、対象のWebアプリケーションがどんなものかという説明や、その問題の目的を紹介するといった形で、補助的な情報を付け足すとよいかもしれません。
フラグ
フラグはどんな内容でもよいかと思います。しかし、問題名や問題文などに含まれる要素から推測可能なものにしてしまう、解法を含んでしまうということは避けるべきでしょう。前者は問題を解かずに「エスパー」によってポイントが得られてしまう可能性があるので当然として、後者は想定していない簡単な解法が発見されたときに困る*6ということが理由です。どう気をつけても意図しないバグを埋め込んでしまうことは避けられませんが、本来の解法がフラグに含まれていなければ、対応として非想定解法を修正した「リベンジ問」を追加でリリースするという選択肢が増やせます*7。
例外的に、問題の性質からフラグのフォーマットに厳しい制約を設ける必要がある、あるいは設けた方が好ましい場合も存在します。たとえばXS-Leaksとよばれる攻撃手法を使った問題において、処理に長い時間がかかったかどうかをもとに、1文字ずつフラグを特定するようなものが考えられます。特にオンラインのCTFでは多数のユーザが同じサーバにアクセスするために、ローカルでは解けているものの、本番のリモートでは負荷のために不安定になってしまうこともあります。あとはフラグを得るだけという状況で、ほぼ問題サーバ側の事情で解ききれないというのはつらいものです。比較的短めの、小文字とアンダースコアのみというように文字種を絞ったフラグにして、またそのフォーマットを問題文やソースコードで提示しておくとよいでしょう。推測し得ない範囲で、文章として成り立つ(フラグであると確信できる)フラグにしておくのもよいでしょう。
CTF Advent Calendar 2021で公開された「【文学】Flagを読む」というフラグに着目した記事があり、実際に出題された問題のフラグがいくつか紹介されています。問題の要素にちなんだダジャレであったり、完全にランダムに生成したhexの文字列であったり、問題とはまったく関係のないYouTubeの動画IDであったり、繰り返しになりますがなんでもよいわけです*8。
テストする
できあがった問題に不備がないか確認します。
- 参加者が与えられた情報だけでその問題を解くことができ、配布する情報やファイルに抜けがないか
- 「エスパー問」とよばれるような、推測に推測を重ねなければ解けない理不尽な問題でないか
- 似た解法の過去問が存在していないか*9
- いわば「天然」の想定していない脆弱性が存在しており、それを使うことで簡単に問題を解くことができてしまったり、ほかの参加者が解くことができないよう妨害できてしまったりしないか
といった点が確認したい事項です。
参加者に与えられるものと同じだけの情報から、たとえばそのCTFの運営に携わっているほかのメンバーに参加者と同じ視点でレビューしてもらうとよいでしょう*10。作問者はアイデアの段階から長い時間その問題と向き合っているわけですから、何も知らない、あるいは自分よりはその問題に関する情報を持たない第三者の視点を一度入れてみることで、見つかる問題もあるかと思います。
どの程度の難易度か、より客観的に評価してもらう*11ことで、CTF中で時間とともに徐々に問題をリリースしていく場合のスケジュールの組み立てや、各問題の点数が固定である静的スコアリングが採用されている場合には配点のための指標として使えるかもしれません。
ほか、この段階で自動でフラグの取得まで行ってくれるエクスプロイトを書いておいたり、簡単にでもフラグを得るまでの手順を文章にまとめておくと、後々「公式writeup」を書いたり、参加者から「問題サーバが正しく動いていない」という問い合わせがあった際にそれが事実か確認したりといったときに便利です。
レビューで特に改善すべき点が見つからなければそのまま、改善すべき点があれば修正して出題しても問題ないところまでクオリティを上げることができれば、問題の完成です。
CTFで出題する
出題の準備が整いました。問題用のサーバで問題をデプロイ*12し、CTFdのようなスコアサーバに問題を登録し、公開時刻になれば出題しましょう。一度問題が公開されると、スコアサーバの回答ログと問題サーバのアクセスログなどを照らし合わせて状況を監視しつつ、あるいは参加者からの問い合わせがあれば返信しつつ、ちゃんと問題が解かれるだろうか、簡単すぎる非想定の解法がないだろうかとCTFの終了まで緊張の時間が続きます。
作問の過程から実際に出題するまで、とにかく悩む時間ばかりであるわけですが、その分参加者から受け取る感想やwriteupは非常に嬉しいものとなります。思ってもいなかった発想から解かれていると、作問者にとっても勉強となりますし、ある種の嬉しさもあります*13。
writeupを書く
ここでいうwriteupとは、いわば作問者による「公式writeup」になります。上述のSECCON CTFやCakeCTFなど、競技の終了後に作問者が自らwriteupを公開するケースが最近ではよくあります。参加者からすると、その問題が解けた解けなかったにかかわらず、通常のwriteupと同様に、自分が試した方法以外でどのようなアプローチがあるかを知ることができるという点で勉強になりますし、またどんな意図で問題が作られたのかや、CTFや問題の裏話のような内容があると読み物として面白いわけです。あると嬉しいですね。
参考になる記事
良い問題を作るという観点では、以下のような記事やドキュメントが参考になります。上の2つでは、作問に限らずCTFの運営にあたって気をつける点もまとまっています。
- [bit.ly/ctf-design] CTF Design Guidelines - Google ドキュメント
- docs/suggestions-for-running-a-ctf-ja.md at master · scryptos/docs
- CTFの問題を作るときに気をつけること - 雑記
また、もう少し具体的な内容であったり、実際に作問するにあたってのエピソードであったりも記載されていることから、以下のようなブログ記事も参考になります。
「CTF 開催記」のようなキーワードで検索してヒットするような、CTFの運営側の記録も参考になります。以下のブログ記事は、特に私が好きな(読み物としての面白さもある)開催記です。
おわりに
以上、私がCTFで作問するにあたってどういった流れで行っているか、どういった点を気をつけているかということをお話ししてきました。重ねてになりますが、あくまでst98という人のやり方として捉えていただきたいと思います。一方で、長々と述べてきた中で何かひとつでもお役に立てば幸いです。
最後に、CTFは参加者側でもとても楽しく、様々なものを学べるゲームではありますが、CTFを開催する運営側として問題を作るのもまた別の楽しみがあります。これまでCTFの作問をされたことがない方もぜひ、問題作りに挑戦してみてください。
*1:いわゆる「ポエム」
*2:プロトタイプを作ってみたら、そもそも最初から問題として破綻していたことに気づくということもたまにあります
*3:一点弊害があり、あらゆるものがネタに見えてくるため、面白いものを見つけても「作問に使えそうだから共有はしないでおこう」とSNSでの共有をためらうようになります
*4:最近はPwn問でもバイナリとあわせてソースコードを配布する風潮が高まっています
*5:私も参加者側であれば、わざわざCTFでそんな作業に時間を費やしたくありません
*6:過去の事例としてCODE BLUE CTF 2017 - CODE BLUE SnippetやTSG CTF 2019 - Obliterated Fileが挙げられます
*7:選択肢を増やすというだけで、どのような対応をすべきであるかはその時々で、たとえばどれだけその別解法が簡単であるかや、どれだけのチームに解かれているかといった状況によって変わるでしょう
*8:「なんでもよい」が一番困るわけですが。いくつか案を挙げます: 昨日の晩ごはんの感想、今聞いている曲の好きなフレーズ、猫を飼うとしたらどんな名前にするか
*9:もちろん、CTFのコンセプトによってはかえって過去問の復習となるような問題が奨励されることもあるでしょう
*10:もっとも、高難度やニッチな問題であればチーム内には作問者しか解ける人がいないという状況もあり得ます
*11:私の場合はという注釈付きになりますが、作問者の考える難易度はまったくもって参考になりません。作問者の主張を鵜呑みにせず、バイアスのない誰かに評価してもらいたいところです
*12:問題の公開まで外部からはアクセスできないようにしておきましょう。CTFプレイヤーは狡猾ですから、crt.shで問題が公開されるであろうサブドメインを特定してきたり、ポートスキャンでサービスのポートを当ててきたりします
*13:zer0pts CTF 2021 - Simple Blogという問題で、DOM Clobberingを想定解法としていたところDangling Markup Injectionで解かれたときには、驚きつつも面白いなあと嬉しさがありました。素直にそう思えたのは、まったく想定していなかったことに加えて、本来想定していた難易度とさほど変わらない非想定解法であったことも手伝っているとは思いますが