CTF
English: https://hackmd.io/@n4o847/BkUU7EPmp
リンクを開くと、Twitter のような SNS サイト "Yatter" にアクセスします。
登録したユーザーはここから "Yeet" と呼ばれる投稿をしたり、他のユーザーをフォローしたり、他のユーザーの Yeet にいいねしたりできます。
配布ファイルを読むと、このサイトの構成は大体以下の通りであることがわかります。
User
username
password
following
followers
posts
(virtual)Post
author
content
likes
/
タイムライン/register
登録ページ/login
ログインページ/@:username
ユーザープロフィール/@:username/:postId
投稿詳細/register
登録する/login
ログインする/logout
ログアウトする/post
投稿する/users/:userId/follow
フォローする/users/:userId/unfollow
フォローを外す/posts/:postId/like
いいねする/posts/:postId/unlike
いいねを外すこの問題の目的は、サーバーにある秘密のテキストファイル (ただしファイル名は不明) を読み取り、フラグを得ることです。
ユーザープロフィールページを開いたときに、タブが切り替えられることに気づきます。
/@:username |
/@:username?tab=following |
/@:username?tab=followers |
---|---|---|
この機能は以下のようなコードで実装されています。
User
モデルが持っている following
, followers
フィールドは他の User
への参照の配列、posts
フィールドは仮想的なフィールドです。
これらを実体化させるために mongoose の population という機能を使っています。
ここで express の req.query
には文字列だけでなく配列やオブジェクトを設定できることが知られています。
加えて、mongoose の populate
には引数で様々なオプションが指定できます。
よって、この部分の処理はだいぶ怪しいと言えます。
実際、ドキュメントで Model.populate
を調べると、以下のようにして MongoDB 上で JavaScript コードが呼び出せることが分かります。
しかし、MongoDB 内の JavaScript インタプリタでは色々な制限がかかっており、任意のコードを実行するのは難しいようです。
……本当にそうでしょうか?
ここからは、少し根気が要るかも知れません。
結論から言えば、mongoose の populate
関数には任意コード実行の脆弱性があり、それを探すことになります。
mongoose 内で、populate
がどのように呼び出されているのかを追っていきます。
Model.populate
は _populate
を通じて各パスに対して populate
を呼び出し、その中で _assign
を呼び出し、assignVals
を呼び出しています。
この assignVals
にだいぶ怪しい箇所があることに気づきます。
どうやら、populate
に渡されたオプションの match
プロパティが配列だったとき、sift というライブラリの関数が呼ばれるようです。
この sift は MongoDB 風のクエリをフィルタ関数に変換するライブラリですが、本来は MongoDB に BSON で送るべきものを JavaScript そのもので処理しているので、挙動が異なります。
sift が $where
を実装しているところを読むと、なんと Function
コンストラクタを使って関数を動的に生成しています。
つまり、match: [{ $where: ... }]
のようにしてコードを渡すことで、任意のコードが実行できるのです!
注意点として、Function
による実行の前に MongoDB 内の JavaScript インタプリタで実行されてしまうので、typeof process === "undefined" ? true : ...
のようにして、どちらで実行されているかを判別する必要があります。
@a
というユーザー名でユーザーが登録されていると仮定します。
この脆弱性は populate
に任意の構造のオブジェクトを渡せるときに限って発生しました。
しかし、プロトタイプ汚染が行われていた場合、他の使い方でも任意コード実行が起こり得ます。
すなわち、この脆弱性はプロトタイプ汚染ガジェットとして利用可能ということです。
例えば、データを一気に挿入する Model.insertMany
関数は、パラメータとして populate
オプションを取ります。
insertMany
内部には以下のような判定コードがあります。
よって、プログラム上 populate
オプションが指定されていなくても、Object.prototype
がプロトタイプ汚染されていた場合、任意コード実行が可能です。