はまやんはまやんはまやん

hamayanhamayan's blog

Moar Horse 4 [TJCTF 2020]

Written by nthistle
It seems like the TJCTF organizers are secretly running an underground virtual horse racing platform! They call it 'Moar Horse 4'... See if you can get a flag from it!
Source
https://moar_horse.tjctf.org/

f:id:hamayanhamayan:20200527223825p:plain

薄暗い怪しいサイトが出てくる。
適当にボタンを押すと、いつものアレが流れるので、これをBGMにして解く。
馬のレースをするサイトのようだ。
なぜか、ffアドベンチャーを思い出した。

調査

  • サイト構成
    • / 特に何もできない
    • /race 所有している馬がいればレースを仕掛けられる
    • /store 馬が買える
  • Responseから得られる情報
    • cookieでtokenを入れ込んでいっている
      • いつものJWT { "user": true, "is_omkar": false, "money": 0, "horses": [ "Luke" ] }
  • 添付されているソースコードから得られる情報
    • templates/index.html
      • Money: ${{money}}
      • SSTIか?
    • templates/new_user.html
      • 最初の画面か。ボタンを押すと/flagへGETリクエストを飛ばす
    • templates/race.html
      • /do_race?horse={{horse}}くらいがきになる所か
    • templates/race_results.html
      • レースの結果が表示される
    • templates/store.html
      • はい
      • /buy_horse?id={{horse['id']}}へのリクエストが見えますね
    • horse_names.txt
      • 沢山の馬の名前がある。他にも強い馬がいるかもしれない
    • pubkey.pem
      • server.pyから参照されている。JWTの署名に使われている
      • private keyは見当たらない。JWTの書き換えは難しそうだ
    • server.py
      • 気になる部分を以下に羅列しておこう
      • validate_token関数
        • JWTにuser,is_omkar,money,horsesがあるか確認している
        • is_omkarをtrueにできれば勝ち? → いや、他で使われてないな
      • do_race関数
        • boss_speed = int(hashlib.md5(("Horse_" + BOSS_HORSE).encode()).hexdigest(), 16)
        • your_speed = int(hashlib.md5(("Horse_" + race_horse).encode()).hexdigest(), 16)
        • これでスピード計算をしている

do_race関数

@app.route("/do_race")
def do_race():
    if "token" in request.cookies:
        is_valid, data = validate_token(request.cookies["token"])
        if is_valid:
            if "horse" in request.args:
                race_horse = request.args.get("horse")
            else:
                return redirect("/race")
            owned_horses = data["horses"]
            if race_horse not in owned_horses:
                return redirect("/race?error")

            boss_speed = int(hashlib.md5(("Horse_" + BOSS_HORSE).encode()).hexdigest(), 16)
            your_speed = int(hashlib.md5(("Horse_" + race_horse).encode()).hexdigest(), 16)
            if your_speed > boss_speed:
                return render_template("race_results.html", money=data["money"], victory=True, flag=flag)
            else:
                return render_template("race_results.html", money=data["money"], victory=False)
        else:
            return redirect("/")
    else:
        return redirect("/")

ここを突破できればいいので、細かく考えていこう。

念のため、与えられてる馬で勝てる馬があるか探してみる。

import hashlib

BOSS_HORSE = "MechaOmkar-YG6BPRJM"
boss_speed = int(hashlib.md5(("Horse_" + BOSS_HORSE).encode()).hexdigest(), 16)

horses = ["Luke", "Noah", "Liam", "Michael", "Alexander", "Ethan", "William", "James", "Logan", "Benjamin", "Mason", "Elijah", "Darin", "Oliver", "Jacob", "Lucas", "Daniel", "Matthew", "Aiden", "Henry", "Joseph", "Jackson", "defund"]

for horse in horses:
    your_speed = int(hashlib.md5(("Horse_" + horse).encode()).hexdigest(), 16)
    if your_speed > boss_speed:
        print(f"win! by {horse}")
    else:
        print("lose.....")

全敗。まあ、そりゃそうか。
となると、horsesに任意の勝てる馬を埋め込んで、勝つしかないな。
JWTの書き換えできる…?

勝てる馬の名前をブルートフォースで探す。
なかなか見つからないが、*gwsqbなら勝てるようだ。

JWTを書き換える

RS256で署名されていて、公開鍵が分かっている状態であれば、
認証方式をHS256に変更してクラックするテクニックがある。
3v4Si0N/RS256-2-HS256: JWT Attack to change the algorithm RS256 to HS256
PoCコードがあるので、使用してみよう。

f:id:hamayanhamayan:20200527223839p:plain

おお。これを入れると、最強の馬が手に入る。

f:id:hamayanhamayan:20200527223848p:plain

You won!