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

hamayanhamayan's blog

Beginner's Capsule [SECCON 2020 Online CTF]

Beginner's Capsule
warmup
Genre: Web+Misc
https://beginners-capsule.chal.seccon.jp/
beginners_capsule.tar.gz

調査

ソースコードを見る前にBurp SuiteとChromeデベロッパーツールを立ち上げて、挙動確認してみる。
試しにconsole.log(flag.#flag);としてみる。

error TS18013: Property '#flag' is not accessible outside class 'Flag' because it has a private identifier.

TypeScript3.8の新機能でハードプライベートというらしい。
ブラウザ毎に対応状況に差があるらしいが、TSはサーバ側だから特に関係ないだろう。

console.log(fs.readFileSync('flag.txt').toString());
一応やってみたけどfs.unlinkSync('flag.txt');があるので、なにも出てこない。
後々、それだけが原因でないことは分かる。

コードを実行させるときは以下のjsを使っている。
POSTでコードとCSRFトークンを渡して実行している。

const res = await fetch('/', {
  method: 'post',
  body: `code=${encodeURIComponent(code)}&token=${encodeURIComponent(token)}`,
}

ソースコードみてみる

runner.ts

const LIB = `
module.exports.enableSeccompFilter = () => {
  const {
    SCMP_ACT_ALLOW,
    SCMP_ACT_ERRNO,
    NodeSeccomp,
    errors: {EACCESS},
  } = require('node-seccomp');

  const seccomp = NodeSeccomp();

  seccomp
    .init(SCMP_ACT_ALLOW)
    .ruleAdd(SCMP_ACT_ERRNO(EACCESS), 'open')
    .ruleAdd(SCMP_ACT_ERRNO(EACCESS), 'openat')
    .load();

  delete require.cache[require.resolve('node-seccomp')];
};
`;

とある。与えられたコードの実行前に呼ばれている関数の中身であるが、openとかのカーネルコールが抑止されている。
console.log(fs.readFileSync('lib.js').toString());の応答がないのはそのせいか。

import * as cp from 'child-process';cp.exec(ls, (err, stdout, stderr) => { if (err) { console.log(err); } else { console.log(stdout); }});
適当に拾ってきたRCEコードを投げてみる。
index.ts(18,21): error TS2307: Cannot find module 'child-process' or its corresponding type declarations.
ないかー

fs.readdir('./', function (err, files) { if (err) { console.log('error'); } for (var file in files) { console.log(file); } });
これもerrorになる…ぐぬぬ

メタ読み

次回作のCapsuleでは脱tsをしている。
きっと、tsからjsへトランスパイルするときになんか脆弱な部分が出てくるんだろう。
tsconfig.jsonを詳しく読んでみる

{
  "compilerOptions": {
    "target": "ES2015", // ECMAScript Private Fieldsは使える版数
    "allowJs": true,    // jsファイルを許容するか(これは大丈夫そう)
    "esModuleInterop": true,  // あんましピンと来ないが、exportとかrequireとか統一されてないいつものアレを何とかしてくれる。requireを使わなくてもimportでやれる感じ?
    "skipLibCheck": true  // *.d.tsのチェックをスキップする。これも大丈夫そう…
  }
}

なんもないじゃん…

トランスパイル後を見てみる

TypeScript: TS Playground - An online editor for exploring TypeScript and JavaScript
ここを使ってトランスパイル後を見てみよう。

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

var flag = new Flag('ab');

これが、こうなる。

"use strict";
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, privateMap, value) {
    if (!privateMap.has(receiver)) {
        throw new TypeError("attempted to set private field on non-instance");
    }
    privateMap.set(receiver, value);
    return value;
};
var _flag;
class Flag {
    constructor(flag) {
        _flag.set(this, void 0);
        __classPrivateFieldSet(this, _flag, flag);
    }
}
_flag = new WeakMap();
var flag = new Flag('ab');

何となく_flagを上手く参照できれば、取れそうな雰囲気がある。
試しにアクセサを作ってみる。

class Flag {
  #flag: string;
  constructor(flag: string) {
    this.#flag = flag;
  }
  getFlag() {
    return this.#flag;
  }
}

var flag = new Flag('ab');

これがjsでは

"use strict";
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, privateMap, value) {
    if (!privateMap.has(receiver)) {
        throw new TypeError("attempted to set private field on non-instance");
    }
    privateMap.set(receiver, value);
    return value;
};
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, privateMap) {
    if (!privateMap.has(receiver)) {
        throw new TypeError("attempted to get private field on non-instance");
    }
    return privateMap.get(receiver);
};
var _flag;
class Flag {
    constructor(flag) {
        _flag.set(this, void 0);
        __classPrivateFieldSet(this, _flag, flag);
    }
    getFlag() {
        return __classPrivateFieldGet(this, _flag);
    }
}
_flag = new WeakMap();
var flag = new Flag('ab');

ほうほう。そういう関数が作られるのね。上の関数を参考にして、Get関数を定義して、読んでみると中身が抜き取れる。

var __classPrivateFieldGet = function (receiver, privateMap) {
    return privateMap.get(receiver);
};
console.log(eval('__classPrivateFieldGet(flag, _flag)'));

SECCON{Player_WorldOfFantasy_StereoWorXxX_CapsLock_WaveRunner}