よっこいのブログ

某有線系企業の社内SE

yokkoi's blog

ctf4b 2020 writeup

ctf4b 2020 writeupとなります。

解けた問題はemo-emo-encode, R & B, Spyとなります。

 

emo-emo-encode

🍣🍴🍦🌴🍢🍻🍳🍴🍥🍧🍡🍮🌰🍧🍲🍡🍰🍨🍹🍟🍢🍹🍟🍥🍭🌰🌰🌰🌰🌰🌰🍪🍩🍽

上記の絵文字列が与えられるので、Unicode文字列に変換します。

\U0001F363\U0001F374\U0001F366\U0001F334\U0001F362\U0001F37B\U0001F373\U0001F374\U0001F365\U0001F367\U0001F361\U0001F36E\U0001F330\U0001F367\U0001F372\U0001F361\U0001F370\U0001F368\U0001F379\U0001F35F\U0001F362\U0001F379\U0001F35F\U0001F365\U0001F36D\U0001F330\U0001F330\U0001F330\U0001F330\U0001F330\U0001F330\U0001F36A\U0001F369\U0001F37D\U0000000D\U0000000A

各文字の共通部分を排除

63746634627B73746567616E306772617068795F62795F656D3030303030306A697D

16進数→Shift-JIS変換

ctf4b{stegan0graphy_by_em000000ji}

R&B

BQlVrOUllRGxXY2xGNVJuQjRkVFZ5U0VVMGNVZEpiRVpTZVZadmQwOWhTVEIxTkhKTFNWSkdWRUZIUlRGWFUwRklUVlpJTVhGc1NFaDFaVVY1Ukd0Rk1qbDFSM3BuVjFwNGVXVkdWWEZYU0RCTldFZ3dRVmR5VVZOTGNGSjFTMjR6VjBWSE1rMVRXak5KV1hCTGVYZEplR3BzY0VsamJFaGhlV0pGUjFOUFNEQk5Wa1pIVFZaYVVqRm9TbUZqWVhKU2NVaElNM0ZTY25kSU1VWlJUMkZJVWsxV1NESjFhVnBVY0d0R1NIVXhUVEJ4TmsweFYyeEdNVUUxUlRCNVIwa3djVmRNYlVGclJUQXhURVZIVGpWR1ZVOVpja2x4UVZwVVFURkZVblZYYmxOaWFrRktTVlJJWVhsTFJFbFhRVUY0UlZkSk1YRlRiMGcwTlE9PQ==
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)

ZIPファイルの中には上記文字列と暗号化スクリプト
とりあえず文字列の先頭が"R"の場合はROT13で復号,"B"の場合はbase64でデコードすれば解けそうなので,手動でやってみたら解けました。

ctf4b{rot_base_rot_base_rot_base_base}

正解のフラグからrot→base→rot→base→rot→base→baseの順番で暗号化されてるっぽいですね。 (暇があればスクリプトかいてみます)

Spy

Arthur
Barbara
Christine
David
Elbert
Franklin
George
Harris
Ivan
Jane
Kevin
Lazarus
Marc
Nathan
Oliver
Paul
Quentin
Randolph
Scott
Tony
Ulysses
Vincent
Wat
Ximena
Yvonne
Zalmon
import os
import time

from flask import Flask, render_template, request, session

# Database and Authentication libraries (you can't see this :p).
import db
import auth

# ====================

app = Flask(__name__)
app.SALT = os.getenv("CTF4B_SALT")
app.FLAG = os.getenv("CTF4B_FLAG")
app.SECRET_KEY = os.getenv("CTF4B_SECRET_KEY")

db.init()
employees = db.get_all_employees()

# ====================

@app.route("/", methods=["GET", "POST"])
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))

# ====================

@app.route("/challenge", methods=["GET", "POST"])
def challenge():
    t = time.perf_counter()
    
    if request.method == "GET":
        return render_template("challenge.html", employees=employees, sec="{:.7f}".format(time.perf_counter()-t))

    if request.method == "POST":
        answer = request.form.getlist("answer")

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

# ====================

if __name__ == '__main__':
    db.init()
    app.run(host=os.getenv("CTF4B_HOST"), port=os.getenv("CTF4B_PORT"))

問題文から上記ファイルをゲットして,URLを確認してみるが最初は何をすればいいかわからず..
悩んでいるうちにページに表示されている"It took 0.0000858 sec to load this page."が怪しいんじゃね?という発想になりソースを確認する。
すると,DB内に存在するユーザの場合はauth.calc_password_hash(app.SALT, password)の関数が呼び出され,負荷がかかりアクセス時間が伸びる仕組みだと判明。
なのでこれまた手作業で全ユーザのアクセス時間を調べ上げ、下記のリストを取得。

Arthur 0.0003552 
Barbara 0.0002938
Christine 0.0003799
David 0.0002633
#Elbert 0.6839784
Franklin 0.0002328
#George 0.4655279
Harris 0.0003274
Ivan 0.0003797
Jane 0.0003581 
Kevin 0.0003291 
#Lazarus 0.4179981 
#Marc 0.6877503
Nathan 0.0003937 
Oliver 0.0003495 
Paul 0.0003279 
Quentin 0.0002408 
Randolph 0.0003148 
Scott 0.0002579
#Tony 0.4931895 
Ulysses 0.0002482 
Vincent 0.0002678 
Wat 0.0004867 
#Ximena 0.4124623 
#Yvonne 0.3969579 
Zalmon 0.0002404 

コメントアウトされているユーザが時間かかっているユーザだとわかったので、このシステムを使っているユーザはこれで間違いないはず...
こちらをChallengeページに入力してFlagゲット。  

ctf4b{4cc0un7_3num3r4710n_by_51d3_ch4nn3l_4774ck}

これはサイドチャンネルアタックっていうんですね。

まとめ

上記3問は2~3時間で解けたが、それ以外の問題は時間かけても解けず...
悔しい結果となったので精進したいと思います。
個人的に年々難易度が上がっている気が...