【 CTF実践編|第4話/全10話】プロトタイプ汚染でadmin権限を奪う

⚡ CTF実践編|第4話/全10話

プロトタイプ汚染でadmin権限を奪う

JavaScriptのオブジェクトが互いにプロパティを継承する「プロトタイプチェーン」という仕組みそのものに潜む脆弱性です。設定保存機能に渡すJSONひとつで、無関係な他のユーザーまで含めて権限を書き換えてしまいます。

🧬 プロトタイプ汚染 🔐 __proto__ 📦 JSONマージ ⭐ 難易度:★★★★☆
01

💭 前回の振り返り|検証ロジックの先にある言語そのものの罠

第3話のJWTから、もう一歩深く

🔗

第3話は「検証ロジックの実装ミス」でしたが…

第3話のalg:noneは、開発者が書いた検証ロジックの実装ミスでした。今回はさらに深く、JavaScriptという言語そのものが持つ「オブジェクトはプロトタイプから機能を継承する」という仕組みを悪用します。開発者がそのつもりがなくても、JSONを再帰的にマージする処理を書いただけで、この脆弱性が生まれてしまうことがあります。

📌

この話の前提知識

今回は実際のJavaScriptのコードを読み解く力が前提になります。

今回の標的は「やさい商事」の社内ツールにある、ユーザー設定の保存機能です。あなたの設定を保存するだけのはずの操作が、なぜか無関係な同僚の権限まで書き換えてしまう様子を、実際に手を動かして確認していきます。

02

🧬 プロトタイプ汚染とは何か|JSの継承の仕組み

「誰も書いていないはずの初期値」はどこから来るのか

JavaScriptのオブジェクトは、自分自身が持っていないプロパティを読もうとしたとき、「プロトタイプ」という別のオブジェクトを参照して値を探します。たとえばuser.isAdminを読んだとき、user自身にisAdminが無くても、userのプロトタイプにisAdmin: falseという既定値があれば、その値が返ります。同じプロトタイプを共有する全てのオブジェクトが、この既定値を一斉に参照します。

通常の状態 you(インスタンス) victim(インスタンス) 共有プロトタイプ isAdmin: false 汚染後の状態 you(インスタンス) victim(インスタンス) 共有プロトタイプ isAdmin: true⇐書き換え済み どちらのインスタンスも自分では何も持っていないのに、プロトタイプ経由でisAdminの値が決まる ⚠ プロトタイプ自体を書き換えれば、それを共有する全インスタンスに一斉に影響する
💡

あなたが触っていないオブジェクトまで変わってしまう

IDOR(第2話)やJWT偽造(第3話)は、「自分のデータ」や「自分のトークン」を書き換える攻撃でした。プロトタイプ汚染が怖いのは、汚染した本人が一度も触っていない、無関係な別のオブジェクトの挙動まで一緒に変わってしまう点です。影響範囲が「自分のデータ」を超えて広がります。

もう1つ注意したいのが、for...inのようなループは自分自身のプロパティだけでなく、プロトタイプから継承したプロパティも一緒に列挙してしまう、という点です。「このオブジェクトが持っているキーを全部処理する」つもりで書いたコードが、汚染されたプロトタイプの余分なキーまで一緒に処理してしまい、予期しない動作を引き起こすことがあります。安全に列挙したい場合はObject.hasOwn()hasOwnProperty()で「自分自身が持つプロパティか」を確認する習慣が役に立ちます。

03

📄 脆弱なマージ関数を読む

このページで実際に動いているコードです

下のコードは、ユーザーが送った設定JSONを、既存の設定に再帰的にマージ(上書き統合)する関数です。一見ふつうの処理に見えますが、keyに何が入っているかをまったく確認していません。

function merge(target, source) { for (var key in source) { if (typeof source[key] === 'object' && source[key] !== null) { if (typeof target[key] !== 'object' || target[key] === null) { target[key] = {}; } merge(target[key], source[key]); } else { target[key] = source[key]; } } return target; }

JSONの中に"__proto__"というキーを入れると、source[key]を読む際にJavaScriptの特殊な仕組みが働き、target[key]は「targetのプロトタイプそのもの」を指すようになります。つまりmerge(target.__proto__, {"isAdmin":true})のような再帰呼び出しが発生し、最終的にtarget.__proto__.isAdmin = true、すなわちプロトタイプ自体への書き込みが行われてしまいます。

危険なキー名到達する場所
__proto__オブジェクトのプロトタイプそのもの(最も直接的な経路)
constructor.prototypeコンストラクタ関数経由でプロトタイプに到達する別経路
prototype対象がコンストラクタ関数自体の場合に直接到達
04

🧪 実践チャレンジ:設定保存ラボで権限を汚染する

1つのJSONで、2人のユーザーを同時に書き換える

下のラボには、あなた(trainee)と、まったく無関係な同僚(victim)の2人のユーザーが登場します。どちらも初期状態ではisAdmin: falseです。あなたの設定保存欄にJSONを入力して送信すると、上のコードと同じmerge()関数があなたの設定に反映されます。

🎯

チャレンジの手順

① 設定JSON欄に{"__proto__":{"isAdmin":true}}と入力 → ② 「設定を保存する」を押す → ③ 2人のユーザーカードのisAdminがどちらもtrueに変わったことを確認 → ④ 「汚染を確認する」を押してチェックポイントコードを入手 → ⑤ ステージ1に入力後、「管理者パネルを開く」で最終フラグを入手。

👤 あなた(trainee)
isAdmin: false
👤 鈴木(無関係な同僚)
isAdmin: false

🧬 汚染を確認しました!

チェックポイントコード:PROTO-P0LLUTED

🎉 管理者パネルに侵入成功!

最終フラグ:CTF{PR0T0_P0LLUT10N_4DM1N}

🧩 実践チャレンジ:2つのステージを繋いでフラグを掴め

ステージ1をクリアすると、ステージ2が解放されます。両方クリアした時点でスコアが確定します。

📊 ステージ進捗: 0/2|挑戦回数: 0回

1ステージ1:汚染を確認する

ラボで「汚染を確認する」を押すと表示されるチェックポイントコードを入力してください。

05

🔗 なぜプロトタイプ汚染は防ぎにくいのか

「再帰的にマージする」という普通の処理が原因になる

プロトタイプ汚染が見落とされやすい理由は、原因になっているコードが「悪意のある処理」にはまったく見えないことです。設定のマージ・オブジェクトのコピー・テンプレートへの値の差し込みなど、ごく普通の処理の中に、再帰的にtarget[key] = valueを行うコードがあれば、それだけで脆弱性になり得ます。実際に、複数の有名なJavaScriptライブラリでこの種の脆弱性が報告され、修正されてきました。

🛡️

防御側の対策

代表的な対策は3つです。①__proto__constructorprototypeというキー名を明示的にブロックリストで拒否する。②マージ先のオブジェクトをObject.create(null)で作り、そもそもプロトタイプを持たない状態にする。③JSON.parseの結果を信頼せず、許可したキーだけを1つずつ取り出してコピーする(再帰的な汎用マージ関数を避ける)。このラボの内部でも、汚染が本物の共有環境(Object.prototype)まで届かないよう、書き込み先を検査する安全策を実装しています。

このような脆弱性は、コードを1行ずつ目で追うだけでは見落としがちです。npm auditのような依存パッケージの既知脆弱性スキャナーや、ESLintのプラグインで危険なキー名への代入を静的に検出する仕組みを開発フローに組み込んでおくと、再帰マージのような「気づきにくいパターン」を早期に発見しやすくなります。

次回は暗号の話に戻ります。RSA暗号という、Webの安全な通信を支える公開鍵暗号方式が、鍵の数字が小さすぎるとどのように破られてしまうのかを体験します。

06

📝 まとめ+FAQ+次回予告

言語の仕組みそのものが攻撃対象になる

第4話では、JavaScriptのプロトタイプチェーンという基本的な仕組みを悪用し、1つのJSON送信から無関係なオブジェクトまで含めて権限を書き換える体験をしました。第1〜3話が「Webアプリの実装ミス」だったのに対し、今回は「言語そのものの仕組みの誤用」という、一段抽象的な視点を得られたはずです。

✅ 今回のまとめチェック

・オブジェクトは自分が持たないプロパティを「プロトタイプ」から継承する
・再帰的なマージ関数が__proto__キーを特別扱いしないと、プロトタイプ自体を書き換えられる
・プロトタイプ汚染は、汚染した本人が触っていない他のオブジェクトにも影響する
・対策はキー名のブロックリスト・Object.create(null)・許可キーのみのコピー
・採点ルールは前話までと同じ(ヒント-15pt、両ステージ解決でスコア確定)

Q. このラボで本当にブラウザ全体のObject.prototypeが汚染されることはありますか?

ありません。このラボは学習用に独立した専用のプロトタイプ(FakeUser.prototype)だけを操作する設計になっており、ブラウザ標準のObject.prototypeには書き込めないよう安全策を実装しています。ページを再読み込みすれば汚染状態もリセットされます。

Q. JSON.parseを使わずに直接JSオブジェクトを書いた場合も危険ですか?

コード中で直接{"__proto__": {...}}のようなオブジェクトリテラルを書いた場合は、JavaScriptエンジンがそれを特殊なプロトタイプ指定として解釈するため、今回とは少し違う挙動になることがあります。今回の脆弱性は「外部から受け取ったJSON文字列をJSON.parseした結果」を再帰的にマージする場合に特に問題になります。

Q. TypeScriptを使えばこの脆弱性は防げますか?

型チェックは「想定外のプロパティ名」を検出する助けになりますが、実行時に外部から受け取ったJSONの中身までは静的な型チェックでは防げません。実行時のキー名チェックやライブラリの対策版を使うことが必要です。

Q. 実際の被害としてはどのようなことが起こりますか?

権限フラグの書き換えによる権限昇格のほか、テンプレートエンジンの設定を汚染してサーバー側のコード実行につながった実例や、アプリケーションを異常終了させるサービス拒否につながった実例が報告されています。原因は同じ「再帰マージの`__proto__`未対策」です。

次回・第5話

RSA暗号を破る|鍵が小さすぎる落とし穴

公開鍵暗号RSAの仕組みを理解し、鍵となる合成数が小さすぎる場合に素因数分解で秘密鍵を復元する体験をします。

📚 参考情報

  • OWASP「Prototype Pollution Prevention Cheat Sheet」
  • 各種JavaScriptライブラリの脆弱性公開情報(マージ・拡張関数関連)

コメント