SECCON Beginners 2024 Writeup misc（getRank, clamre）


この記事は [お題は不問！Qiita Engineer Festa 2024で記事投稿！ - Qiita](https://qiita.com/official-events/16baee61b1d8bd4aac5a) の参加記事です。

https://qiita.com/official-events/16baee61b1d8bd4aac5a

## はじめに

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

https://www.seccon.jp/13/seccon_beginners/content.html

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

## getRank

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

```ts: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`であればよいのですが、

```ts
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`](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/parseInt)について調べると、10進数だけでなく16進数もパースできることがわかります。
そのため、[`Infinity`](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Infinity)になるような16進数の文字列を入力として与えることで回避できます。

```console
$ 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を取るものでした。

```python: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でルールを書けることがわかります。

https://docs.clamav.net/manual/Signatures/LogicalSignatures.html#pcre-subsignatures

あとは正規表現を解読していけばflagを入手できます。

```console
$ 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

https://qiita.com/xryuseix/items/ed5fad16a990d27edac4

https://feneshi.co/ctf4b2024writeup/