地図と窓:エージェント型スキャンがドキュメントの漏えいを認証情報の窃取へとつなげた方法
OstorlabのAgentic Deep Scanが、重大度「中」のOpenAPI仕様の露出とSSRFの脆弱性を連鎖させ、ループバック制限をバイパスしてデータベースの認証情報を抽出した過程を紹介します。
Ostorlab Agentic Deep Scanは、/static/openapi.jsonに誰でも読み取れるOpenAPI仕様を発見し、重大度を「中」と評価しました。ドキュメントであり、認証情報は含まれていないためです。その後、このドキュメントをインベントリとして読み解き、それをたどって2件のクリティカルな検出結果、有効なデータベース認証情報一式、そして偽造された管理者セッションにまで到達しました。
クロスレイヤー攻撃経路とは何か。クロスレイヤー攻撃経路とは、アプリケーションのあるレイヤーで露出したアーティファクト(ビルドファイル、静的アセット、クライアントバンドルなど)が、別のレイヤー(通常はバックエンド)の弱点に到達するために必要なものを提供してしまう経路のことです。そのアーティファクト自体は脆弱性でないことも多く、その価値は、別のものをテストする際の当て推量をなくしてくれる点にあります。
エグゼクティブサマリー(TL;DR)
この仕様に記載されたエンドポイントのうち、重要だったのは2つです。/internal/secretはアプリケーションのシークレットを返しますが、サーバー自身以外からのアクセスはすべて拒否し、直接のリクエストにはHTTP 403 Internal resource. Loopback only.と応答します。/upload_profile_picture_urlはURLを受け取り、サーバーサイドでそれを取得します。
後者の宛先を前者に向けることで、ループバック制限を破るのではなく、その条件を満たすことができました。レスポンスにはアプリケーションの署名鍵、JWTシークレット、データベースの認証情報が含まれていました。さらに、漏えいした署名鍵を使って、アプリケーションが受け入れる管理者トークンが発行されました。
テスト環境と手法
このウォークスルーは、承認を得たうえで実施した、シミュレーションによるベンチマーク演習を記録したものです。すべてのテストは、セキュリティ研究とツール評価のために公開・維持されている意図的に脆弱な銀行アプリケーションvulnbank.orgに対して、Ostorlab Agentic Deep Scanが実施しました。本番システム、実際の顧客データ、サードパーティのアセットは一切関与しておらず、以下に示す認証情報はすべて、そのサンドボックスに属するシード済みのテスト値です。
スキャナーが仕様の段階で止まる理由
シグネチャベースのツールは、既知の不正パターンのライブラリに照らして、リクエストを一つずつ評価します。この基本的な仕組みは高速かつ決定的ですが、ルールをいくら追加しても埋まらない構造的な盲点が3つあります。
| 限界 | 起きる理由 | ここで見逃すもの |
|---|---|---|
| リクエスト間で状態を持たない | エンドポイントはリクエストごとに個別に評価される | 同じファイルに記載された2つのエンドポイントが組み合わさってバイパスになること |
| 目的という概念がない | ルールが表現するのは構文であり、機能が何をするかに対して、何のためにあるかではない | プロフィール画像のアップローダーが、サーバーの立場から見ればリクエスト生成器であること |
| 制御が機能しているとテストが終わる | 正しい403は否定的な結果として記録され、そのエンドポイントは対象から外される |
その制御が、別の手段で到達できるかもしれない許可された主体を名指ししていること |
これらの動作は、個々に見ればいずれも正しいものです。仕様を取得してもシグネチャには一致しません。/internal/secretを調べると、本物の403が返ります。どちらの観察も間違ってはおらず、どちらも検出結果を生みません。
エージェントは何が違うのか
Ostorlab Agentic Deep Scanは、脆弱性を発見して検証するために、5つのステップからなる推論ループを継続的に実行します。
- 偵察:アタックサーフェスを把握する。エンドポイント、パラメーター、認証要件、露出したアーティファクト
- 仮説:観察した内容をもとに、何が問題になり得るかを推論する
- テスト:仮説を実証または反証するリクエストを送信する
- 検証:示唆的なレスポンスではなく、実際の影響を確認する
- 連鎖:この結果を、すでに見つかったものと組み合わせることで、まだ試していない経路が開けるかどうかを問う
違いを生むのはステップ2と5です。シグネチャベースのスキャナーにはステップ3があり、ステップ4も弱い形で備えています。機能が何のためにあるかについて仮説を立てることはなく、現在の結果と組み合わせるための過去の検出結果のモデルも保持しません。エージェントは、アプリケーションについて、見たもの、除外したもの、まだ説明のつかないものを記録したインベントリを継続的に保持し、新しい結果が届くたびにそれを参照します。以下のチェーンは、すべてステップ5の中で生まれたものです。
実際の動き:Agentic Deep Scanが発見したチェーン
検出結果 #1(中):OpenAPI仕様が公開アクセス可能
このアプリケーションは、公開されている静的アセットのディレクトリから、完全なOpenAPI 3.0ドキュメントを配信していました。
$ curl -s https://vulnbank.org/static/openapi.json | wc -l
1471
$ curl -I https://vulnbank.org/static/openapi.json
HTTP/2 200
content-type: application/json
認証は不要で、パスは予測可能です。1,471行にわたり、32のエンドポイントがそのスキーマとルートごとの認証要件とともに記述されていました。/static/はフロントエンドがスタイルシートや画像を置く場所であり、バックエンドの契約を置く場所ではありません。

図1:重大度は「中」と評価されており、それ自体としては正しい評価です。検出結果の説明自体が、すでにその帰結を指摘しています。この仕様は「/sup3r_s3cr3t_adminのような、本来なら大規模なクローリングをしなければ発見できない機密性の高いエンドポイントの存在を明らかにしてしまう」のです。
これを列挙すると、1,471行が構造化されたアタックサーフェスに変わりました。その中には、アプリケーションがどこからもリンクしていないルートも含まれています。
| カテゴリ | エンドポイント |
|---|---|
| 認証 | /login, /register, /api/v1/forgot-password |
| 取引 | /transfer, /transactions/{account_number}, /check_balance |
| 管理 | /sup3r_s3cr3t_admin, /admin/create_admin, /admin/delete_account/{user_id} |
| 内部 | /internal/config.json, /internal/secret, /latest/meta-data/* |
| ファイルアップロード | /upload_profile_picture, /upload_profile_picture_url |

図2:インベントリとして解析された仕様。価値があるのは個々のパスではなく、アタックサーフェス全体を一度に把握できることです。
明白な仮説と、その正しい反証。/internal/secretは自らの存在を示していたため、エージェントはそれをリクエストしました。
$ curl -s -i https://vulnbank.org/internal/secret
HTTP/2 403
{"error": "Internal resource. Loopback only."}
拒否され、しかもその拒否は正しいものでした。このエンドポイントはリクエストの送信元を確認し、マシン自身にしか応答しません。エンドポイントを個別にテストするツールにとっては、ここで行き止まりです。仮説は反証され、制御は健全なので、次に進みます。
検出結果 #2(クリティカル):プロフィール画像のアップロードを介したSSRF
実りのある問いは、ループバックのチェックをどう破るかではなく、誰ならその条件を満たせるか、そしてその主体に行動させることができるか、でした。
ループバックとはサーバーのことです。インベントリのうち、ファイルアップロードに分類された1行は、要求に応じてサーバーに外向きのリクエストを発行させる機能を記述していました。
$ curl -X POST https://vulnbank.org/upload_profile_picture_url \
-H "Authorization: Bearer <JWT>" \
-H "Content-Type: application/json" \
-d '{"image_url": "http://127.0.0.1:5000/internal/secret"}'
これでリクエストの送信元は127.0.0.1になります。ループバックのチェックは通過します。回避されたのではなく、本当に条件が満たされたのです。

図3:この検出結果は、コードブロックの1行目に自らの出所を記録しています。# From OpenAPI spec analysisです。
{{
"secrets": {{
"app_secret_key": "secret123",
"jwt_secret": "secret123",
"env_preview": {{
"DB_HOST": "db",
"DB_NAME": "vulnerable_bank",
"DB_USER": "postgres",
"DB_PASSWORD": "postgres"
}},
"DEEPSEEK_API_KEY": "sk-e2719..."
}}
}}

図4:記録されたとおりの一連の流れ。登録、ピボット、窃取。ペイロードは要約されず、原文のまま取得されています。
検出結果 #3(クリティカル):保護されていない/internal/secretを介した機密データの露出
示唆的なレスポンスが一つあるだけでは検出結果にはなりません。そこで、この経路を再実行し、挙動が再現可能であり、値が本物であることを確認しました。
検証試行1:
/upload_profile_picture_urlを介したhttp://127.0.0.1:5000/internal/secretへのSSRFリクエストが、シークレットのペイロード全体を返した 検証試行2:認証情報を確認。App Secret=secret123、JWT Secret=secret123、DB認証情報=postgres/postgres
2行目こそが、レスポンスを影響の記述へと変えるものです。そして実行はペイロードを読むだけでは終わりませんでした。漏えいしたjwt_secretを使って{"user_id": 1, "username": "admin", "is_admin": true}を主張するトークンに署名し、それを、ステップ1で仕様が明らかにしていた管理者用ルートに提示しました。
$ curl -X GET https://vulnbank.org/sup3r_s3cr3t_admin \
-H "Authorization: Bearer <token forged with secret123>"
HTTP/2 200 # full admin panel
シークレットは単に通信途中で観察されたのではありません。アプリケーションが受け入れる認証情報の発行に使われました。これが、漏えいした文字列と認証バイパスとの違いです。

図5:説明は依存関係をはっきりと述べています。「Combined with the SSRF vulnerability」(SSRFの脆弱性と組み合わせることで)。

図6:制御とそのバイパスを一つの画面で。403は本物です。それを破ったのは、そのチェックへの攻撃ではなく、同じ仕様に記載された2つ目のエンドポイントでした。
チェーンを読み解く
| ステップ | 検出結果 | 重大度 | 提供元 |
|---|---|---|---|
| 1 | OpenAPI仕様が公開配信されている | 中 | デプロイ時のミス |
| 2 | プロフィール画像URLの取得を介したSSRF | クリティカル | 仕様に記載されたエンドポイント |
| 3 | /internal/secretの内容の窃取 |
クリティカル | 同じ仕様に記載された標的に向けたSSRF |
| 4 | 漏えいしたJWTシークレットによる管理者セッションの偽造 | #3の影響 | ステップ3の署名鍵を、ステップ1の管理者用ルートに対して使用 |
その大部分は機械的な作業です。静的ファイルの取得、JSONの解析、パスの列挙、それぞれへのリクエスト送信とステータスの記録は、いずれも推論なしに自動化できます。
機械的でないステップは、1行目と2行目の間にあります。1行目はインベントリです。3行目は目的です。2行目はそのどちらでもありません。それは、「ファイルアップロード」に分類された機能が、サーバーの立場から見ればリクエスト生成器であること、そしてリクエスト生成器こそがループバック制限の求めるものであることに気づくことです。
仕様のどこにもそうは書かれていません。ドキュメントは/upload_profile_picture_urlを画像のアップロードとして記述しています。それがこの機能の目的だからです。これをピボットとして読むには、無関係な2つの行を同時に念頭に置き、一方が他方に何をできるかを問う必要があります。
重大度は実際にはどこにあるのか
2件のクリティカルがそこから派生している以上、仕様の露出の重大度を引き上げたくなるものです。しかしそれは誤った判断であり、エージェントもその判断はしませんでした。
この仕様に記載されたエンドポイントの大半は問題ありませんでした。それらを列挙しても何も得られなかったのは、その制御が機能していたからです。/internal/secret自体も、直接のアクセスには正しく403を返していました。仕様がSSRFを生み出したわけではなく、ループバックのチェックを弱めたわけでもありません。
変わったのはコストです。未知のAPIのアタックサーフェスを手探りで探す作業が、完全な地図をもとにした的を絞った作業に変わり、2つのエンドポイントの関係が一目でわかるようになりました。
この違いが修復の方針を左右します。仕様を削除しても、SSRFは依然として機能し、ループバックのバイパスも依然として機能し、/internal/secretは内部から到達できるあらゆるものに依然としてデータベースの認証情報を渡します。このドキュメントは公開されるべきではありません。しかし、それを修正して直るのは発見のしやすさであり、露出そのものではありません。
よくある質問
露出したOpenAPI仕様はなぜセキュリティリスクになるのか。認証情報が含まれていることはまれで、そのため単体で「中」より高く評価されることはほとんどありません。リスクは、インターフェースからリンクされていないルートも含めたエンドポイントの完全なインベントリが、パラメーターのスキーマやエンドポイントごとの認証要件とともに公開されてしまう点にあります。これにより、未知のAPIのアタックサーフェスを手探りで探す作業が、既知の地図をもとにした的を絞った作業に変わり、本来なら大規模なクローリングをしなければ見つからないエンドポイント間の関係が見えるようになります。
SSRFはどのようにしてループバック専用の制限をバイパスするのか。バイパスするのではなく、条件を満たすのです。ループバックに制限されたエンドポイントは、リクエストの送信元を確認し、サーバー自身にしか応答しません。サーバーサイドリクエストフォージェリ(SSRF)の脆弱性があると、攻撃者はサーバーが取得するURLを指定できるため、その結果として生じるリクエストは本当に127.0.0.1から発信されます。制御は正しく評価を行い、それを許可します。そのため、同じアプリケーション内のいずれかのエンドポイントがユーザーから渡されたURLを取得する場合、ループバック制限だけでは不十分なのです。
重大度の低い検出結果がクリティカルな検出結果につながる場合、エスカレーションすべきか。通常はその必要はありません。今回の場合、仕様の露出がSSRFを生み出したわけでも、ループバックのチェックを弱めたわけでもなく、2つの脆弱性はそれぞれ独立して存在していました。変わったのは発見のコストです。仕様の露出を解消してもクリティカルな検出結果はどちらも塞がらず、これをエスカレーションすると、悪用可能な欠陥ではなく露出のほうに修復の焦点がずれてしまいがちです。
エージェント型スキャンとシグネチャベースのスキャンの違いは何か。シグネチャベースのスキャナーは、エンドポイントを個別に評価し、ある結果と次の結果を結び付ける記憶を保持しません。仕様を取得しても一致はなく、/internal/secretを調べると正しい403が返り、否定的な結果として正確に記録されます。どちらの観察も間違ってはいません。検出結果は、同じファイルに記載された2つのエンドポイントの関係の中にしか存在せず、それを見つけるにはリクエストをまたいでアタックサーフェスのモデルを保持する必要があります。
このテストは本番システムに対して実施されたのか。いいえ。すべてのテストは、セキュリティ研究とツールのベンチマークのために公開されている意図的に脆弱なアプリケーションvulnbank.orgに対して実施されました。示されている認証情報は、そのサンドボックス内のシード済みのテスト値です。
まとめ
スキャナーは今後も、通信上で明らかにおかしく見えるものを見つけ続けるでしょうし、そうあるべきです。公開配信されたAPI仕様は、まさにルールが低コストで捕捉できる種類の設定ミスです。ルールにできないのは、その仕様を間取り図として読み、異なるセクションに異なる目的で記述された2つのエンドポイントが組み合わさることで、単体では完璧に機能している制御を迂回する経路になることに気づくことです。
それが、エージェント型テストが実際にもたらす変化です。より多くのパターンを見つけることではなく、アプリケーションを十分に記憶に保持し、その各部分が互いにどう作用するかを見抜くことです。
Ostorlab Agentic Deep Scanで、同じ推論ループを自社のアタックサーフェスに対して実行してみてください。