この記事は お題は不問!Qiita Engineer Festa 2024で記事投稿! - Qiita の参加記事です。
はじめに
今年もSECCON Beginnerに参加してきました!
解いた問題のうち、「misc」の問題についてどのように解いたかを書きます。
getRank
以下のWebアプリケーションからflagを取る問題でした。
import fastify, { FastifyRequest } from "fastify";
import fs from "fs";
const RANKING = [10 ** 255, 1000, 100, 10, 1, 0];
type Res = {
rank: number;
message: string;
};
function ranking(score: number): Res {
const getRank = (score: number) => {
const rank = RANKING.findIndex((r) => score > r);
return rank === -1 ? RANKING.length + 1 : rank + 1;
};
const rank = getRank(score);
if (rank === 1) {
return {
rank,
message: process.env.FLAG || "fake{fake_flag}",
};
} else {
return {
rank,
message: `You got rank ${rank}!`,
};
}
}
function chall(input: string): Res {
if (input.length > 300) {
return {
rank: -1,
message: "Input too long",
};
}
let score = parseInt(input);
if (isNaN(score)) {
return {
rank: -1,
message: "Invalid score",
};
}
if (score > 10 ** 255) {
// hmm...your score is too big?
// you need a handicap!
for (let i = 0; i < 100; i++) {
score = Math.floor(score / 10);
}
}
return ranking(score);
}
const server = fastify();
server.get("/", (_, res) => {
res.type("text/html").send(fs.readFileSync("public/index.html"));
});
server.post(
"/",
async (req: FastifyRequest<{ Body: { input: string } }>, res) => {
const { input } = req.body;
const result = chall(input);
res.type("application/json").send(result);
}
);
server.listen(
{ host: "0.0.0.0", port: Number(process.env.PORT ?? 3000) },
(err, address) => {
if (err) {
console.error(err);
process.exit(1);
}
console.log(`Server listening at ${address}`);
}
);
ソースコードを読んでいくと、ランキングで1位になればflagを取れることがわかります。
1位を取るには10 ** 255
より大きいscore
であればよいのですが、
function chall(input: string): Res {
if (input.length > 300) {
return {
rank: -1,
message: 'Input too long',
};
}
let score = parseInt(input);
if (isNaN(score)) {
return {
rank: -1,
message: 'Invalid score',
};
}
if (score > 10 ** 255) {
// hmm...your score is too big?
// you need a handicap!
for (let i = 0; i < 100; i++) {
score = Math.floor(score / 10);
}
}
return ranking(score);
}
- ユーザーの入力が300文字以内でなければならない
- 入力した数値が
10 ** 255
以上の場合、10 ** 100で除算される
ため、素直に入力しても1位を取れません。
ですが、parseInt
について調べると、10進数だけでなく16進数もパースできることがわかります。
そのため、Infinity
になるような16進数の文字列を入力として与えることで回避できます。
$ curl https://getrank.beginners.seccon.games/ -H 'Content-Type: application/json' -d '{ "input": "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" }'
{"rank":1,"message":"ctf4b{15_my_5c0r3_700000_b1g?}"}
clamre
以下のようなWebアプリケーションが与えられflagを取るものでした。
#!/usr/bin/env python3
from flask import Flask, request, render_template
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
import tempfile
import subprocess
app = Flask(__name__)
limitter = Limiter(
get_remote_address,
app=app,
default_limits=["10 per second"],
)
@app.route("/", methods=["GET"])
def index():
return render_template("index.html")
@app.route("/", methods=["POST"])
def upload_file():
if "file" not in request.files:
return "No file part", 400
file = request.files["file"]
if file.filename == "":
return "No selected file", 400
if file:
with tempfile.NamedTemporaryFile() as tmp:
path = tmp.name
file.save(path)
command = [
"clamscan",
"-z",
"--database=/var/www/flag.ldb",
"--no-summary",
path,
]
try:
result = (
subprocess.run(
command,
capture_output=True,
text=True,
)
.stdout.strip("\n")
.split(" ")
)
if len(result) == 3:
matched = result[1]
return render_template("result.html", matched=matched)
else:
return render_template("result.html", matched=None)
except Exception as e:
return f"Something went wrong: {e}", 500
return "Something went wrong", 500
if __name__ == "__main__":
app.run(host="0.0.0.0", port=3000)
ClamoraFlag;Engine:81-255,Target:0;1;63746634;0/^((\x63\x74\x66)(4)(\x62)(\{B)(\x72)(\x33)\3(\x6b1)(\x6e\x67)(\x5f)\3(\x6c)\11\10(\x54\x68)\7\10(\x480)(\x75)(5)\7\10(\x52)\14\11\7(5)\})$/
ソースコードを読んでいくと、ClamAVでflag.ldb
のルールにマッチする文字列がflagということがわかります。
そのため、flag.ldb
が何にマッチするのかを調べればflagを取れます。
flag.ldb
を解読するためにClamAVのSignaturesのドキュメントを読んでみると、PCREでルールを書けることがわかります。
あとは正規表現を解読していけばflagを入手できます。
$ cat flag
ctf4b{Br34k1ng_4ll_Th3_H0u53_Rul35}
$ curl -X POST https://clamre.beginners.seccon.games/ -F file=@flag
...
<p>Correct Flag Matched: ClamoraFlag.UNOFFICIAL</p>
...
さいごに
今年もSECCON Beginners楽しめました!
他にも解けた問題があるのでまたWriteupを書ければなと思います。
Ref
作問者のWriteup