내 친구가 이 부족한 툴을 만들었다; 쿠키 값을 훔칠수 있겠어? 링크를 보내주면 내가 전달할수 있어.
this dumb tool
툴이라고 소개된 url로 접속하게 되면 2가지 기능을 마주하게 된다.
- Link Shortener
직역하면 링크 단축키라고 한다.
URL 풀주소를 입력해주면 web-utils.dicec.tf를 도메인으로 사용하는 URL주소를 건네준다.
ex) http://www.google.com -> http://web-utils.dicec.tf/~~
실제로 단축 주소를 입력해주면 해당 사이트로 이동하는 것을 확인 할수있다.
https://web-utils.dicec.tf/view/pEr848xN
- Pastebin
값을 입력하면 해당 데이터 값을 출력(?) 해주는 URL을 건네준다.
ex) test -> https://web-utils.dicec.tf/~~
마찬가지로 실제 단축 주소로 이동해보면 입력한 값을 그대로 출력해주는 페이지를 마주할 수 있다.
https://web-utils.dicec.tf/view/TRTO7I1W
I can pass it along
web/Babier CSP에서 나왔던 Admin Bot인 것 같다. URL을 입력해주면 Admin Bot 접속해주는 방식인 것 같다.
우리는 이 문제의 목표를 찾았다.
쿠키 값을 얻기 위해서 어떠한 URL 주소를 찾아내 입력해 주어야 한다.
다음으로 제공된 app.zip 파일을 열어 분석을 진행해 보았다.
아래는 dumb tool에 관한 구조이다.
-app
| + modules
| - database.js
|
| + Public
| - view
| - style
| - index
| + pastes
| - index (pastebin)
| - script
| - style
| + links
| - index (link shortener)
| - script
| - style
|
| + routes
| - api
| - view
|
| - Dockerfile
| - index.js (server)
| - package.jsaon
크게 server -> linkshortener -> pastebin 순으로 진행하겠다.
const fastify = require('fastify')();
const path = require('path');
fastify.register(require('fastify-static'), {
root: path.join(__dirname, 'public'),
redirect: true,
prefix: '/'
});
fastify.register(require('./routes/api'), {
prefix: '/api/'
});
fastify.register(require('./routes/view'), {
prefix: '/view/'
});
const start = async () => {
console.log(`listening on ${await fastify.listen(3000, '0.0.0.0')}`)
}
start()
index.js (server)의 코드이다.
코드를 통해 route를 사용하여 api, view를 정의한 것을 확인 할수 있다.
linkshortener와 pastebin을 확인하기 전에 route을 먼저 보도록 하겠다.
server -> route -> linkshortener -> pastebin
순서대로 api, view이다.
const database = require('../modules/database');
module.exports = async (fastify) => {
fastify.post('createLink', {
handler: (req, rep) => {
const uid = database.generateUid(8);
const regex = new RegExp('^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'
});
database.addData({ type: 'link', ...req.body, uid });
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' }
}
}
}
});
fastify.post('createPaste', {
handler: (req, rep) => {
const uid = database.generateUid(8);
database.addData({ type: 'paste', ...req.body, uid });
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' }
}
}
}
});
fastify.get('data/:uid', {
handler: (req, rep) => {
if (!req.params.uid) {
return;
}
const { data, type } = database.getData({ uid: req.params.uid });
if (!data || !type) {
return rep
.code(200)
.header('Content-Type', 'application/json; charset=utf-8')
.send({
statusCode: 200,
error: 'URL not found',
});
}
rep
.code(200)
.header('Content-Type', 'application/json; charset=utf-8')
.send({
statusCode: 200,
data,
type
});
}
});
}
module.exports = async (fastify) => {
fastify.get(':id', {
handler: (req, rep) => {
rep.sendFile('view.html');
}
})
}
route의 api 소스코드를 분석해보면 8개의 랜덤 값을 생성하는 것과 url이 https:// 로 시작되는지 확인하는 과정을 볼 수 있다.
당장은 눈에 보이는게 없어 이 정도 정보만 얻고 넘어가도록 하겠다.
server -> route-> linkshortener -> pastebin
Public/link/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>
html소스코드를 보고 script.js 분석이 필요하다고 느꼈다.
Public/link/script.js
(async () => {
await new Promise((resolve) => {
window.addEventListener('load', resolve);
});
document.getElementById('url-form').addEventListener('submit', async (e) => {
e.preventDefault();
const url = document.getElementById('url-input').value;
const res = await (await fetch('/api/createLink', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
data: url
})
})).json();
if (res.error) {
return;
}
document.getElementById('output').textContent =
`${window.origin}/view/${res.data}`
});
})();
index.html에서 입력 받은 값을 api/createLink로 전달하여 특정 값을 전달 받는 것을 확인 할 수 있다.
아까 지나오면서 본 그 랜덤 8자리 값인 것으로 보인다.
server -> route-> linkshortener -> pastebin
Public/pastes/index.html
<!DOCTYPE html>
<html>
<head>
<title>Pastebin</title>
<link rel="stylesheet" href="style.css">
<script src="script.js"></script>
</head>
<body>
<div class="parent">
<div class="content">
<form id="text-form">
<textarea id="text-input" placeholder="Paste..." spellcheck="false" rows="5"></textarea>
<input id="submit-button" type="submit" value="Shorten"/>
<div id="output" class="display"></div>
</form>
</div>
</div>
</body>
</html>
마찬가지로 script.js를 확인해 보아야 할 것 같다.
(async () => {
await new Promise((resolve) => {
window.addEventListener('load', resolve);
});
document.getElementById('text-form').addEventListener('submit', async (e) => {
e.preventDefault();
const text = document.getElementById('text-input').value;
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}`
});
})();
같은 내용이다. index.html에서 받은 URL을 api/createPaste에 전달하여 값을 받는다. 가장 뒷부분에 사용되고 이는 아까 랜덤 값 8개라 지칭하였던 그 값이다.
위에서 살펴 봤던 route/api를 다시 한번 살펴봐야 될 것 같다.
server -> route-> linkshortener -> pastebin -> route
const database = require('../modules/database');
//linkshortener에서 사용됨
module.exports = async (fastify) => {
fastify.post('createLink', {
handler: (req, rep) => {
const uid = database.generateUid(8); // 8자리의 랜덤 값을 뽑아냄(database.js 분석 결과)
const regex = new RegExp('^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'
});
database.addData({ type: 'link', ...req.body, uid });
rep //랜덤한 8자리 값을 반환
.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' }
}
}
}
});
//createPaste에서 사용됨
fastify.post('createPaste', {
handler: (req, rep) => {
const uid = database.generateUid(8); // 8자리의 랜덤 값을 뽑아냄
database.addData({ type: 'paste', ...req.body, uid });
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' }
}
}
}
});
fastify.get('data/:uid', {
handler: (req, rep) => {
if (!req.params.uid) {
return;
}
const { data, type } = database.getData({ uid: req.params.uid });
if (!data || !type) {
return rep
.code(200)
.header('Content-Type', 'application/json; charset=utf-8')
.send({
statusCode: 200,
error: 'URL not found',
});
}
rep //랜덤한 8자리 값을 반환
.code(200)
.header('Content-Type', 'application/json; charset=utf-8')
.send({
statusCode: 200,
data,
type
});
}
});
}
server -> route-> linkshortener -> pastebin ->route -> modules
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 = {
getData: db.prepare(`SELECT data, type FROM data WHERE uid = ?;`),
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);
},
generateUid: (length) => {// 값 8개 가챠
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('');
}
}
소스코드 분석을 통해 당장 얻을 수 있는 정보는 다 얻어냈다.
'Challenge > CTF' 카테고리의 다른 글
[2021 hspace] Baby_Crypto (0) | 2021.05.07 |
---|---|
[DiceCTF] web/Web Utils (2) (0) | 2021.03.09 |
[DiceCTF] web/Missing Flavortext (0) | 2021.02.16 |
[DiceCTF] web/Babier CSP (0) | 2021.02.09 |
CYBRICS CAPTURE THE FLAG (0) | 2020.07.28 |