TSG CTF 2025 Writeup

Dec 23, 2025

2025/12/20 16:00(JST) - 2025/12/21 16:00(JST)で開催された TSG CTF 2025 に参加した.

チームで参加し,結果は,1201 点で 489 チーム中 88 位 (Domesticチームでは26位) だった.

解いた問題についてまとめておく.

https://score.ctf.tsg.ne.jp/challenges

Web

image-compress-revenge [106 Solve (100)]

I tried making an app with Vibe coding. It’s easy and nice, isn’t it?
http://35.221.67.248:10502
image-compress-revenge.tar.gz

画像をアップロードできるWebアプリである. ファイル名はescape関数によって,サニタイズされている. 該当の文字の場合,\\をつけることでエスケープされる. しかし,適用される文字列の中にバックスラッシュ\がないことが脆弱性となる.

server.ts
import { Elysia, t } from "elysia";
import { unlink } from "fs/promises";
import { run } from "./lib/shell.ts";

const CHARS_TO_ESCAPE = "$'\"(){}[]:;/&`~|^!? \n".split("");

export function escape(source: string): string {
	let s = source;
	for (const char of CHARS_TO_ESCAPE) {
		s = s.replaceAll(char, "\\" + char);
	}
	return s;
}

const app = new Elysia()
	.get("/", () => {
		return Bun.file("./public/index.html");
	})
	.post(
		"/compress",
		async ({ body, set }) => {
			const { image, quality } = body;

			if (image.name.includes("..")) {
				throw new Error(`Invalid file name: ${image.name}`);
			}

			const inputPath = `./tmp/inputs/${escape(image.name)}`;
			const outputPath = `./tmp/outputs/${escape(image.name)}`;
			console.log(escape(image.name));

			try {
				await Bun.write(inputPath, image);

				await run(
					`magick "${inputPath}" -quality ${quality} -strip "${outputPath}"`,
				);

				const compressed = await Bun.file(outputPath).arrayBuffer();

				set.headers["Content-Type"] = image.type;
				set.headers["Content-Disposition"] =
					`attachment; filename="${image.name}"`;

				return new Response(compressed);
			} catch (error) {
				set.status = 500;
				return { error: `Failed to compress image: ${error}` };
			} finally {
				await unlink(inputPath).catch(() => {});
				await unlink(outputPath).catch(() => {});
			}
		},
		{
			body: t.Object({
				image: t.File({
					"file-type": "image/*",
					maxSize: "10m",
				}),
				quality: t.Numeric({
					minimum: 1,
					maximum: 100,
					default: 85,
				}),
			}),
		},
	);

app.listen(process.env.PORT ?? "3000", (server) => {
	console.log(
		`🦊 server is running at http://${server.hostname}:${server.port}`,
	);
});

また,Dockerfileから,FLAGは環境変数にあることがわかる.

FROM oven/bun@sha256:b5cf5ca5dc3e2a02d805802ba089401c4beabf597daabbf35a17b8e82dc2f7bc

# Install ImageMagick
RUN apt-get update && \
    apt-get install -y imagemagick && \
    rm -rf /var/lib/apt/lists/*

WORKDIR /app

# Copy package files
COPY package.json bun.lock ./

# Install dependencies
RUN bun install --frozen-lockfile

# Copy application files
COPY server.ts ./
COPY public ./public
COPY lib ./lib

# Create tmp directory
RUN mkdir -p ./tmp

ENV FLAG=TSGCTFDUMMY{}
ENV PORT=3000

CMD ["bun", "run", "server.ts"]

具体的には,ファイル名に\$FLAG.jpgを入力すると,escape関数によって$がエスケープされ,\\\$FLAG.jpgとなる.

このとき,内部動作としては,\がエスケープされていることになり,$FLAGで環境変数がよみこまれることになる.

Burp Suiteなどで実行すると,flagが得られる.

This is a image

TSGCTF{d0llar_s1gn_1s_mag1c_1n_sh3ll_env1r0nm3nt_and_r3ad0nly_15_r3qu1r3d_f0r_c0mmand_1nj3c710n_chall3ng35}

library [227 Solve (100)]

Our library’s new staff portal is up! Only the head librarian can access the restricted archives.
http://35.221.67.248:10501
library.tar.gz

Misc

Mission: Impossible [106 Solve (100)]

The security system of CIA vault room is state-of-the-art.
There are a large number of pressure-sensitive, temperature-sensitive and audio-sensitive sensors.
And the terminal is operated by voice recognition.
http://35.194.98.181:57860/
mission_impossible.tar.gz

Sudoers Maze 100pts [143 Solve (100)]

Where am I? Who am I?
nc 34.180.66.205 55655
sudoers_maze.tar.gz

SecurityProgrammingCTFwriteuptsg

しゅたるく (Stark)

目次

AlpacaHack memo

SECCON CTF 14 Quals Writeup

comments powered by Disqus