LoginSignup
3
0

この記事は お題は不問!Qiita Engineer Festa 2024で記事投稿! - Qiita の参加記事です。

はじめに

今年もSECCON Beginnerに参加してきました!

解いた問題のうち、「misc」の問題についてどのように解いたかを書きます。

getRank

以下のWebアプリケーションからflagを取る問題でした。

main.ts
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を取るものでした。

server.py
#!/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)
flag.ldb
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

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0