こんにちは SpookiesCTF副部長の西村です。
さてさて、かなり時間が空いてしまいましたが、SECCON CTF2023国内決勝の参加記です。
補欠合格というとてもラッキーな形での参加でしたが、結果は 9位
順位は置いといて、メンバーが皆で協力しあって楽しめたのが最高でした。
それではどうぞ!
21ma
会社としては5年前に出場しておりますが、僕にとっては初めてのSECCON決勝でした。
前日より会場近くのホテルに泊まったので、ゆっくり朝食が食べれるなど、体調万全で挑むことが出来ました。
会場に入る前にすぐ真下にあるコンビニに入ると、もう強者の風格漂う人たちがぞろぞろ。テンションを上げつつ、会場へ。チーム名とdiscordアカウントを伝えていざメイン会場にイン。
机にチーム名の入った布?や各種チーム状況を伝えるディスプレイなんかあり、おーなんかかっこいい。
前日夜に物理LANケーブルがチームメンバー全員分必要ということがわかり、最年少メンバーちゅるりが朝から秋葉原で購入してきてくれてセットアップ完了。
開始前のアナウンスと開始宣言があり、いよいよスタート。
さて、本題の問題ですが僕はWEBの babywaf
を2日間ずっとやってました。 結果として解くことは出来なかったのですが、とてもシンプルな問題で、こんだけ悩ませてくれるとはという感じです。writeupを見ると、なぜそこに気づけなかったと悔しくなりましたが、それが実力ですね。
普段の大会では、深夜に集まってそれぞれの家からやることが多いので、みんなで集まって同じ画面を見ながら、同じご飯を食べながら、疲れ切った顔を見たり、解けた瞬間のドヤ顔を見たりする体験が本当に楽しかったです。
運営・サポーターの皆様ありがとうございました。
さて今年(2024)は賞金取れるランクまできっといくはずです。乞うご期待。
※2日とも美味しいランチが出ました。
ちゅるり
入社してからだるさんにお声掛けいただき、今年始めて SECCON に参加しました。私自身 CTF という営み自体初めてのものでしたが、始めて 1 年で決勝という大舞台に立つことができたのは大きな経験であり、とっても楽しかったです。
一方で、今回は悔しさの残る大会にもなりました。web 問を担当していましたが、2 日間かけても 1 問も解くことができませんでした。かなり遅いですが、writeup もどきを以下に記していこうかと思います(babywaf 1 問のみですが...)。
[Web 276] babywaf (4 solves)
Express で実装されたバックエンドと、その前段に Fastify で実装されたプロキシサーバが挟まっているような構成です。バックエンド側は次のコードのようなシンプルなコードで、JSON リクエストの中に givemeflag
キーがあればフラグを吐き出すような仕組みになっています。
const express = require("express"); const fs = require("fs/promises"); const app = express(); const PORT = 3000; const FLAG = process.env.FLAG ?? console.log("No flag") ?? process.exit(1); app.use(express.json()); app.post("/", async (req, res) => { if ("givemeflag" in req.body) { res.send(FLAG); } else { res.status(400).send("🤔"); } }); app.get("/", async (_req, res) => { const html = await fs.readFile("index.html"); res.type("html").send(html); }); app.listen(PORT);
一方で、プロキシサーバ側には WAF っぽい記述があります。
const app = require("fastify")(); const PORT = 3000; app.register(require("@fastify/http-proxy"), { upstream: "http://backend:3000", preValidation: async (req, reply) => { // WAF??? try { const body = typeof req.body === "object" ? req.body : JSON.parse(req.body); if ("givemeflag" in body) { reply.send("🚩"); } } catch {} }, replyOptions: { rewriteRequestHeaders: (_req, headers) => { headers["content-type"] = "application/json"; return headers; }, }, }); app.listen({ port: PORT, host: "0.0.0.0" });
どうやらリクエストに givemeflag
があった場合にダミーのフラグを返すような処理を行っているようです。従って普通に givemeflag
があるようなリクエストを送っても、一生フラグは得られないようです。
さて Fastify では、リクエストヘッダに与える値によって内部でのリクエストボディの扱い方が異なってきます。text/plain
で送信した場合にはリクエストは生の文字列として扱われ、application/json
で送信するとリクエストが secure-json-parse
ライブラリによってパースされ、適切なオブジェクトに変換される実装となっています。プロキシのコードの try
節内の最初のステートメントがまさにそれです。このことから text/plain
で JSON 文字列を送信すると JSON.parse
に処理が進むことに気が付きます。 その先の catch
節ではエラーを握りつぶしているので、前述の考察を踏まえると、JSON.parse
でエラーを出すことができれば WAF を回避できると考えられます。
...競技中に気がついたのはここまでです。想定解としては BOM 付きの文字列を送ると、JSON.parse
で落ちてフラグが得られる...ということだったのですが、愚かな私は競技中にそれに気がつくことなく別の解法を探しに行ってしまうのでした。
暗中模索していると、チームメンバーのてっちゃんが gzip による解法を思いつきました。Fastify(プロキシ)側は Content-Type: gzip
は処理できないにも関わらず、バックエンド側は処理することができます。一方で、プロキシ側はリクエストボディを UTF-8 でデコードしてしまうので、ここでエラーを起こさないために ASCII 範囲内の gzip データを送れば良いと考えられます。
ASCII 文字の範囲は 0x00
から 0x7f
です。また gzip では非圧縮のデータを作成することができますが、このデータブロックの中には 2 の補数表現が出てきます。従って、工夫しない限りはどんなに頑張っても ASCII 文字の範囲内で構成された gzip データを構築することは不可能です。ここで詰んでしまい、やはり競技時間中に解くことはできませんでした。この解法については blog.tyage.net さんの SECCON CTF 2023 Finals Writeup あたりにまとめられているので、そちらをお読みいただければと思います。
さいごに
実力不足で1問も解くことはできませんでしたが、問題に真正面から向き合って解いていくのは本当に楽しかったです。大学で計算機を学んでいる人間として、1 問も解けないのはとても悔しいことです。今年一年みっちりと特訓して、またあの舞台に立てたらと思います。
そして今回のチームは、私以外は社内の社員さんと専務で構成されていました。チーム内の年齢差は大きいものの、夜にはオフィスに戻って徹夜で問題に向かった体験は大学や高校の活動と遜色ないものでした。まるで青春です。今年のチームはまだどうなるかわかりませんが、どんなメンバーであれ最高の成果を出したいと思います。ありがとうございました!
てっちゃん
web専のてっちゃんです。私にとっては初めての国内決勝、スプーキーズに入って初めて知ったCTF。入部後はweb問だけを解き続けて1年半。培った力を全力開放するべく挑んだ秋葉原。
結果は1問も解けませんでした😇😇😇
ほとんど全ての時間をbabywafに費やしました。
babyだったのはどうやらわたしのようでした。
会場では各チームのスコアがなんかかっこよくディスプレイに投影されていて、問題を解くとリアルタイムにkurenaifさんがGood Job!!
してくれるのが羨ましすぎた。
2日目は絶対Good Job!!
してもらうんだという気持ちでこの気合いの入りようですよ。
まあ、昼ごはんのランチボックスがとてもおいしかったので、良しとします。
待ってろ来年!
次はGood Job!!
されたい!!
だる
部長です。だるです。zeosuttです。
締切後に急いで書いています。ごめんなさい。 s/今回は2名分/今回は2.1名分/g
でお願いします...
問題振り返り
既にwriteupは出ているはずだし、時間もないので、解いた問題について一言ずつ。
[pwnable] babyheap 1970 (5 solves)
どこをどう書き換えるかでひたすら沼っていたところ、突然stack pivotの天啓が降りてきました。とてもとても長い戦いでした。
[reversing] ReMOV (12 solves)
気付けば解いていないのは弊チームだけでした。gdbで処理を追ったらサクッと解けました。writeupを読んでアンチデバッグの存在を知りました。あれ?
感想
2日目の14時までは国内・国際合わせて唯一の0完でしたが、その後2問解けて下から4番目という結果でした。正直かなり焦っていたので、なんとか解けて本当によかったです。
解いた問題以外では物理問が印象に残っています。
どれだけ回路を切り刻んでも何も起こらなかったため、「とにかく変化が欲しい」とLEDへの電力供給を断ったりしていました。
そんな感じで、何も分からんながらも各々思考を巡らせたり勘に従ったりしてわいわいしていたところ、爆発後の退席時に「今まで見たチームの中で一番面白かった」と言われました。
チーム皆で顔を合わせ、虚ろな目で夜を徹して取り組むCTF。マジ最っっっっっっっっっっっっっっっっっっっ高でした。
また次も決勝を目指します。よろしくお願いします。