Fetch the Flag CTF 2022 解説:Disposable Message
Michael Aquilina
2022年11月10日
0 分で読めますFetchに参加していただき、ありがとうございました! 参加してくれた何千人ものプレイヤーの皆さん、Fetch the Flag CTFへのご参加おめでとうございます。また、チャレンジの作成、テスト、解説記事の執筆に携わったSnykerの皆さんにも心から感謝します!
この記事では、Snykの2022年Fetch the Flag CTFイベントで出題されたDisposable Messageチャレンジについて解説します。このチャレンジでは、CSSインジェクションの手法を使って脆弱なWebページを悪用し、フラグを取得します。厳格なContent Security Policy(CSP)が設定されていたため、Disposable Messageは特に難しいチャレンジでした。メッセージが閲覧時に期限切れになる仕組みを利用することが、このポリシーを回避する鍵となりました。
この記事では、このチャレンジで動作するエクスプロイトをどのように考案したかを解説します。調査から始め、そこで得た知見をもとに動作するエクスプロイトを作成するまでの過程を紹介します。エクスプロイトのコードはPythonで記述しているため、読者はある程度Pythonに慣れていることを前提とします。CSS、JavaScript、HTMLの知識も役立ちますが、ここで説明する内容の大半は解説していきます。
調査
チャレンジを開始すると、「This message has expired.(このメッセージの有効期限が切れました)」という短い説明と、Webページへのリンクが表示されます。
この説明は、Webページでメッセージを作成でき、そのメッセージはサーバーによって削除されるまで一度だけ閲覧できることを示唆しています。説明文にこの点が記載されているのは、おそらく重要な役割を持つことを示す強いヒントです(後で思い出せるようにしておきましょう)。
ページにアクセスすると、次の画面が表示されます。

ページにはメッセージを入力するテキストボックスがあります。メッセージを入力してSend!ボタンをクリックすると、次のページが表示されます。

/view/<message-id>という形式の新しいURLが表示されています(以降、このページをメッセージ閲覧ページと呼びます)。また、このメッセージをクリップボードにコピーするボタンと、Ask admin bot to visitボタンもあります。ブラウザーでメッセージ閲覧URLを開くと、作成したメッセージを確認できます。同じページにもう一度アクセスすると、Webサーバーは404ステータスコードを返します。

Ask Admin bot to visitボタンに戻りましょう。これをクリックすると、リモートの管理者ボットがブラウザーでメッセージを閲覧すると推測できます。クリック後すぐにメッセージの有効期限が切れるため、この推測はかなり確かです。また、「admin bot」という名前からも、このブラウザーには必要なフラグが含まれている可能性が高いと考えられます。
次に、ページのソースコードを確認し、注目すべき点がないか調べてみましょう。
フラグの場所
メッセージ閲覧ページには、次のHTMLコードが含まれています。
コードのコメントによると、このdiv要素にはCookieから取得した値がdata-flag属性として設定されます。ブラウザーの開発者ツールでCookieを編集し、どうなるかを確認すれば簡単に確かめられます。
フラグCookieの名前として最も自然なのは「flag」です。「flag」という名前のCookieに「SNYK{hello-world}」を設定してからメッセージ閲覧ページを再び開くと、属性に値が正しく設定されていることがわかります。

CSSインジェクション
メッセージ閲覧ページに含まれるスクリプトを見ると、次のJavaScriptコードも確認できます。
このスクリプトは、URLに「color」というクエリ文字列パラメーターがあるかどうかを確認します。存在する場合、そのパラメーターの値がstyleタグに設定されます。最後に、そのstyleタグがWebページに動的に挿入されます。
このstyleタグで注目すべきなのは、文字列フォーマットを使って作成されていることです。文字列フォーマットを使うと、styleタグから抜け出して任意のCSS値を追加できます。これはCSSインジェクションの脆弱性として知られています。
colorパラメーターにcolor=00ff00;font-size:80pxのような値を設定し、フォントサイズが指定どおりに変わることを確認すれば、CSSインジェクションに対して脆弱かどうかを簡単にテストできます。

CSSインジェクションを使うと、ユーザーのブラウザー内にあるページから情報を外部へ送信できます。この攻撃では、特定のセレクターに値が一致したときにURLへのリクエストが発生するよう、複数のCSSセレクターを作成します。
たとえば、HTMLのdiv要素のdata-flag属性を流出させたい場合、colorパラメーターに次のような値を設定できます。
上記の^=セレクターは、属性値が指定した文字列で始まる場合に、対応するURLへのリクエストを発生させます。
つまり、たとえばdivのdata-flag属性がaで始まる場合、ブラウザーはhttp//evil.com/a にアクセスします。値がbで始まる場合はhttp//evil.com/bにアクセスする、といった具合です。
evil.comのサーバーを管理していれば、どのページにアクセスがあったかを把握できます。CSSセレクターを十分な回数試せば、data-flag属性を1文字ずつ取得できます。
Content Security Policy
最新のWebブラウザーには、Webページで使用できるコンテンツを制限するContent Security Policy(CSP)という機能があります。このチャレンジには、前述のCSSインジェクション手法を直接使えないようにするCSPが設定されています。
ブラウザーの開発者ツールでページのヘッダーを確認すると、CSPを調べられます。

ポリシーのimg-srcセクションでは、「self」と「data''」のみが許可されていることに注目してください。つまり、使えるのは使い捨てメッセージのページと同じドメインに属するURLだけです。特に、CSSインジェクションでアクセスされたURLを確認するために、自分が所有するドメインを使うことはできません。しかし、チャレンジの使い捨てメッセージを利用すれば、この制限を回避できます。
使い捨てメッセージは一度しか閲覧できません。すでに閲覧されたメッセージにアクセスすると、404ステータスコードが返されます。そのため、CSSインジェクションのセレクターに使い捨てメッセージのURLを指定し、そのURLが閲覧されたかどうかをステータスコードで判定できます。
調査のまとめ
調査でわかった、エクスプロイトの作成に役立つ重要な情報は次のとおりです。前のセクションを完全に理解できなかった場合や、ざっと読んだだけの場合でも、以下を把握しておけば次のセクションに進めます。
メッセージ閲覧ページは、
?color=クエリ文字列パラメーターを介したCSSインジェクションに対して脆弱です。つまり、ページに任意のスタイルを設定できます。Content Security Policyでは、同じドメインのURLのみが許可されています。そのため、外部URLはブラウザーによってブロックされます。
メッセージを閲覧できるのは一度だけです。未閲覧のメッセージには200ステータスコードが返され、すでに閲覧されたメッセージには404ステータスコードが返されます。
フラグはメッセージ閲覧ページ内の
data-flag属性にあります。この値は、ブラウザーの「flag」Cookieパラメーターから設定されます。admin botのブラウザーには、正しいフラグが設定されたCookieがあると考えられます。
これらの情報を組み合わせて、エクスプロイトを作成する方法を見ていきましょう。
エクスプロイト
CSSインジェクションを使うと、data-flag属性からフラグを1文字ずつ取得できます。ただし、チャレンジと同じドメイン内のURLを使う必要があります。CSSインジェクションで文字を外部へ送信する際に重要なのは、URLにアクセスがあったかどうかを把握し、どの文字が流出したかを特定することです。
幸い、使い捨てメッセージのドメイン内で、文字ごとにメッセージを生成できます。各URLを、それぞれの文字を含むCSSセレクターと紐付けられます。
たとえば、Pythonでは次のように実装できます。
generate_payload関数では、推測が正しければ対応するURLへのリクエストが発生するCSSセレクターを作成する必要があります。この関数は次のようになります。
generate_message関数では、Send!ボタンが呼び出すPOST /newエンドポイントを使って新しいメッセージを作成します。作成後、この呼び出しで生成されたadmin bot用URLとメッセージ閲覧URLを保存します。これらのURLは、正規表現を使ってレスポンスから抽出できます。
メッセージと、それに対応するCSSセレクターのペイロードを生成したら、admin botが閲覧するメッセージをもう1つ作成します。このメッセージがペイロードを受け取り、「data-flag」属性の値が推測と一致するかどうかに応じて、各メッセージのURLへのリクエストを発生させます。
admin botを呼び出したら、ボットがページを取得してレンダリングするまで数秒待ちます。待機後、推測に関連付けたすべてのメッセージURLのステータスコードを確認し、404ステータスコードを返すものを探します。404が見つかれば、admin botがそのURLにアクセスしたとわかるため、その推測が正解だったと判断できます。
クエリ文字列のエンコード
上記のコードでは、パラメーター名とクエリ文字列の区切り文字?color=を含むクエリ文字列全体をquote_plusでエンコードしていることに気づいたかもしれません。これは、クエリ文字列を介してadmin botに渡したペイロードが無視されるためです。
この問題を回避するには、admin botにクエリ文字列をURLパスの一部だと思わせ、アクセスするメッセージ閲覧URLに含める方法があります。
たとえば、URLパスが次のようになっているとします。/admin-bot/f785781-55f4-4eca-8565-8511f12a4ffc**?color=**ffffff%7D…
この場合、admin botはクエリ文字列パラメーターを無視し、URLパス/view/f785781-55f4-4eca-8565-8511f12a4ffcにアクセスします。
しかし、クエリ文字列パラメーター全体をエンコードすると、URLパスの一部になります。
/admin-bot/f785781-55f4-4eca-8565-8511f12a4ffc**%3Fcolor%3D**ffffff%7D…
Webサーバーは、この値をデコードしてからボットに渡し、アクセスさせているようです。つまり、ボットがアクセスするページのURLは次のようになります。
/view/f785781-55f4-4eca-8565-8511f12a4ffc?color=ffffff}...
フラグ全体の取得
正しく推測する方法がわかったので、推測がフラグと完全に一致するまでこの手順を繰り返します。そして「}」に到達したら、終了です。
他のチャレンジで使われたフラグから、Snykのフラグは'SNYK{'で始まり、波括弧の中身はUUIDだとわかっています。これを利用すれば、候補の文字を「a-f」と「0-9」だけに絞れます。その結果、各段階での推測に必要な時間を大幅に短縮できます。
エクスプロイトのコード
ここまでの内容をまとめると、最終的なソースコードは次のようになります。
このコードを実行すると、SNYKフラグが1文字ずつ流出するのを確認でき、達成感を味わえます。

メッセージは消え、チャレンジは攻略済み
使い捨てメッセージのチャレンジにCSSインジェクションの脆弱性があることがわかりました。厳格なContent Security Policyによって難易度は上がりましたが、閲覧済みのメッセージが返す404ステータスコードを利用し、フラグを流出させるエクスプロイトを作成できました。
このCTF解説を楽しんでいただき、新しい発見があったなら幸いです。CTFチャレンジは実際のエクスプロイトについて学ぶ絶好の機会であり、自分のシステムを攻撃から守る力にもつながります。他のフラグをどのように見つけたか知りたい方は、Fetch the Flagの解答ページをご覧ください。



