ENKI RedTeam CTF

開発者が参加してみたハッキング(RedTeam)大会レビュー

ハッキング大会に参加した。セキュリティを勉強している友人が参加するというので、じゃあ自分も?5年目のエンジニアとしての経験があるし、と軽い気持ちで飛び込んでみた。

はじめに

参加した大会は ENKI RedTeam CTF だ。(リンク) 本格的なレビューの前に、セキュリティ用語に馴染みのない方のために簡単に整理しておこう。(参加した私自身も用語を知らなかった)

RedTeam: セキュリティの脆弱性を検証するために「攻撃者」の立場から侵入シナリオを実行するチーム。反対に防御側は「BlueTeam」と呼ぶ。 CTF(Capture The Flag): 直訳すると「旗取り」。システムの弱点を突いて隠された特定の文字列(Flag)を見つけるとポイントが得られるハッキングコンテストだ。 ENKI: 国内でもかなり規模のあるサイバーセキュリティ企業。ホワイトハッカー採用のためにこの大会を開催したとのこと(上位〜30位まで採用機会が提供されるらしい)。

大会名をまとめると、ENKIが主催した侵入(ハッキング)コンテストで、誰が一番うまくFLAG(文字列)を見つけられるかで順位を決める、ということだ。

大会スタート

正直なところ、セキュリティという分野にはあまり触れてこなかった。情報セキュリティの授業はドロップしたし、セキュリティの資格試験も筆記試験にギリギリ合格したものの実技を諦めた。 セキュリティは学ぶべきことが他の分野とは桁違いに多い。前日に一日詰め込んでも意味がない、と割り切って早めに寝て万全の状態で臨むことにした。

大会はオンラインで、土曜日の午前11時〜日曜日の午前11時、計24時間にわたって行われた。 私は11時のスタートと同時にまず問題の種類を把握した。

1つ目の種類はクライアント/サーバーのソースコードと攻撃対象のWebサイトURLが提供されるタイプ。 2つ目の種類は何の情報もなくWebサイトのURLだけが与えられるタイプ。(このタイプはWebサイトを通じて攻撃対象サーバーのroot権限にアクセスしてフラグを取得しなければならなかった。)

全ての種類を把握した後、切り札であるClaude Codeを複数のターミナルに立ち上げた。 戦略的にネットで見つけた senior-security-engineer というSKILLも適用した。 素早い自分の指とClaudeの明晰な頭脳があれば、怖いものなしだった。

4つのターミナルに4つの問題を並べ、Claudeに頭脳を委ねた私は「早く解きすぎたらどうしよう?」という謎の自信をつまみにコーヒーを一杯飲んだ。

Claudeに失望する

私の役割は4つのターミナルでYを押すことだった。1時間ほどYを押し続けていると、何かがおかしいと気づき始めた。 いろいろな試みはしているものの突破口となる脆弱性は見つけられず、特定のケースにはまって無意味な試みを繰り返すばかりだった。

さらに悪いことに、Claude Codeのセッション制限にも達してしまった。残されたのはGeminiの無料プランと自分の頭だけだった。

自力で解く

自分が多少知っている分野の問題を一つ選んで深く掘り下げた。

問題のサービスはWebを提供するNextフロントエンドと、外部に公開されていないFlask + Redisバックエンドに分離された構成だった。 ソースコードの分析はほぼ完了した。結論は、NextをFlaskの特定のエンドポイントへの内部リクエストを送るように仕向ける必要があるということだった。 でも、どうやって?ソースコード上で内部呼び出しを行う箇所はどれも利用できそうになかった。

ここで詰まったまま6時間が経過し、午後5時になった。

ClaudeはGODだ

Claudeのセッション制限が解除され、自分が把握した内容を共有してどうすれば解決できるかを一緒に考えた。

そうして会話をしていると、可能性がありそうな提案が出てきた。 Nextが提供する next-image 機能を悪用しようというアイデアで、結果的にこれが解決策となった。

Nextはイメージを最適化するために、内部ファイルシステムまたは外部の特定エンドポイントから画像を取得し、解像度に合わせてリサイズする。 この際、https://nextservice.com/_next/image?url=https://img.com/cat.png の形式で外部画像を取得させることができ、?url= クエリパラメータに内部エンドポイント(127.0.0.1:5000/get-image)を呼び出させるように誘導すればいい。

このアイデアを見た瞬間「これは解けた」と思ったのだが、実際に ?url=http://127.0.0.1/get-image の形式で呼び出すとNextのポリシーによってブロックされた。 調べてみると、Nextは127.0.0.1localhostへの内部呼び出しを自前でブロックしているらしい。

ここでもClaudeもんに「ダメだった」と文句を言ったら、DNS Rebindingという手法を紹介してくれた。 URLを検査するタイミングと実際にデータを取得しようとして接続するタイミングの間でDNSの応答値を変えることで、IPベースの検査をすり抜ける手法だ。

function fetchImage(url) {
  const host = url.getHost();
  if (host === '127.0.0.1') { // この時点では正常な公開IP
    return null;
  }
 
  const image = fetch(url); // この時点では127.0.0.1
  return optimize(image);
}

Nextは上記のような形式で呼び出しをブロックするため、URL検査では正常な公開IPで通過させ、fetch()するタイミングで127.0.0.1に解決させればいい。

この方法を使って最終的にFLAGを取得した。問題を解き始めて8時間後、午後7時のことだった。

FLAGを手に入れたとき、久しぶりに震えるほどのドーパミンを感じた。一つの問題に8時間も向き合ったのはいつぶりだろう? 解けたのはClaudeの貢献が大きいが、Claudeの示唆を理解して正確に実行した自分にも少しは功績があると思う。

成績

死闘の末、1問解いた。 (スコアボードには2問解いたように見えるが、1問はDiscordに参加するだけで解いたことにしてくれる。)

scoreboard

感想

その後もう一問チャレンジしてみた。ただ、どうしても一つのリンクが繋がらずに進めなかった。

感じたのは、中途半端な知識では問題を一問解くことすら難しいということだ。

それでも、一問でも解けたことへの達成感はある。

Disclaimer

ENKIが公開した正解の解法(writeup)および他の問題はこちらのリンクから確認できます。

本レビューは個人的な学習経験を記録するために書いたものです。本記事に著作権やその他の問題がある場合は wichan.dev@gmail.com までご連絡ください。