要約
五月祭(東大の学祭の1つ)の企画であるTSG LIVE! 8のライブCTFに赤チームの一員として参加し、椅子を温めて終わりました。
前回のライブCTF参加記(だいぶコピペしました)
TSG LIVE! とは
私の所属サークルである東大のコンピュータ系サークルTSGが、学祭などの折に実施しているプログラミング生放送です。今回で8回目になります。
今年も去年や一昨年同様に全ての企画がフルリモートでした。
参加記
椅子を温めて終わりました。
web問が1問しかなく、他の問題も後々覗いてみたものの、無理だったので泣きました。
Problem on fire
とりあえず、問題を見ます。
package.jsonにmain: "index.js"
って書いてたので、index.jsを探す……が、ない。
で、よく見たらサーバ側のコードがなく、firestore完全依存SPAでした。
import {createApp} from 'https://cdn.jsdelivr.net/npm/vue@3.2.33/dist/vue.esm-browser.js'; createApp({ data() { return { flag: '', }; }, async mounted() { const auth = firebase.auth(); const {user} = await auth.signInAnonymously(); const token = await user.getIdToken(); const uid = user.uid; const db = firebase.firestore(); await db.collection('users').doc(uid).set({admin: false}); try { const flag = await db.collection('flags').doc('flag').get(); this.flag = flag.get('value'); } catch (e) { this.flag = e.toString(); } }, }).mount('#app');
↓ firestore.rules
rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { match /users/{uid} { // 自分のユーザー情報を書き込めるのは自分のみ allow read, create: if request.auth.uid == uid && !request.resource.data.admin; // 一度作ったユーザー情報を編集できるのはadminだけ allow update: if request.resource.data.admin; } match /flags/flag { // flagを読めるのはadminだけ allow read: if get(/databases/$(database)/documents/users/$(request.auth.uid)).data.admin == true; } } }
コードをさくっと眺めてみると、ユーザを作る段階でadmin属性をfalseにしています。クライアント側で。
そしてfirestore.rulesを読む限り、adminになるとflagが読めるようです。
……ザルすぎるだろ。クライアント側でadmin属性指定できるのヤバすぎ。え、これadmin:trueにすれば解けるんです?
一応firestore.rulesを眺めてみたが、私がfirestore全然分かってないのはともかく、なんとなく読む感じマジでそれで良さそうに見えますね。
……(フラグを)奪っちゃうよ!? 奪っちゃうよ!?
ちなみに、結果的にこれが死亡フラグになりました。
ふぁぼん発狂編
で、実際にwebに公開されてるウェブサイトを軽く改竄して実行すればflagが転がりこんでくるはず……なんですが、「軽く改竄して」ってどうすればいいのか全然分からない……
このあたりの解決策がなんとなく思いつきました。
1つ目は、どうやってレスポンスを改竄するか分からず死亡。ブラウザのアドオンからはHTTPの通信に介入するAPIが(権限を与えられれば)使えるから、理論上は可能のはずです。ただ、それを可能にするアドオンを探そうとしたが、見つかりませんでした。たぶん私の探し方が悪い。
2つ目は、途中までは上手くいった気がします。wgetでfirebase SDKのスクリプトごとまるっと落としてきて、admin:falseをadmin:trueに修正したファイルを用意して、pythonのhttp.serverをコマンドラインで実行してアクセスしてみました。
location.host等が違うせいでエラーを吐かれる可能性も考えましたが、幸いそこは大丈夫だったようです。
で、結局この方法ではなぜか永遠に権限エラーを食らいました。なんか、どこかミスってたんでしょうね……
ちゃんとadmin:trueなリクエストが飛んでいってるのは確認したはずなんですが、ネットワークのやり取りが複雑すぎて把握できていなかった可能性があります。
3つ目は、なんかfirestoreに接続するときのフローが複雑で挫折しました。どう考えても暖かみのある手作業curl芸をする想定になっていません。まずSDKからアクセスする前提ですし、APIがグチャグチャです。エンドポイントのURLは長いし、なんかoptionメソッドで謎のリクエストが飛んでるし、なんかよくわからんエラーが飛んでくるし……
というわけで、90分くらいずっと試行錯誤しているうちに終わりました。
あまりに上手くいかなさすぎて、「adminをtrueにしてリクエストするのは第一段階で、もう1つか2つ何らかの攻撃手法を取らないと解けないのでは?」とすら思ってました。
……マジでadmin:trueにするだけだったってマジ?????????????????????????????
追記 [2022/05/16]
後で人のwriteup読んで気付いたんですが、コンソールで普通にリクエストを初めから(認証の段階から)やり直せばflag取れたんですね。馬鹿が代……
これだったらページ編集する必要すらない。
まとめ
これだからCTF初心者はダメなんだ
前回(TSG LIVE! 7)に続き「チームメイトはめっちゃ問題通してる、自分は1問も解けずギブアップ」が2連続となるとさすがに泣きたくなりますが、まあこれくらいで挫折してもしゃーない(関西弁で「仕方ない」の意)し、ゆるりとやっていきます。いやでもこれで通せなかったのはかなり泣きたいな……前回の「そこそこ難しい問題で、あと一歩及ばず」と違い、今回は自明問題を通せなかったので……
終わった後の感想戦で「まあ、ちょっと難しかったかもですね〜」ってめっちゃフォローされてたけど、これ解けないのマジのカスなのでカスでしたね。やるだけ問題でやるだけなのが分かってるのに「やる」ができないお前の人生。
あのときの私は「firestore使ったことなくて〜」とかほざいてましたけど、firestore使ったことないのは全然関係なくて、単純にCTFをやり慣れていないのが敗因でした。たぶん作問陣もそんなとこで詰まる人間がいると思ってない。