Created
December 5, 2022 00:52
-
-
Save ajmeese7/3a3e19f4641455efcf3ed4dc4286fe43 to your computer and use it in GitHub Desktop.
Medium > HTB University CTF 2022 "The Magic Informer" code snippets
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { decode } from "../helpers/JWTHelper.js"; | |
const AdminMiddleware = async (req, res, next) => { | |
try{ | |
if (req.cookies.session === undefined) { | |
if(!req.is('application/json')) return res.redirect('/'); | |
return res.status(401).json({ status: 'unauthorized', message: 'Authentication required!' }); | |
} | |
return decode(req.cookies.session) | |
.then(user => { | |
req.user = user; | |
if (req.user.username !== 'admin') return res.redirect('/dashboard'); | |
return next(); | |
}) | |
.catch(() => { | |
res.redirect('/logout'); | |
}); | |
} catch(e) { | |
console.log(e); | |
return res.redirect('/logout'); | |
} | |
} | |
export { AdminMiddleware }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as dotenv from 'dotenv'; | |
import cookieParser from "cookie-parser"; | |
import path from "path"; | |
import express from "express"; | |
import nunjucks from "nunjucks"; | |
import fileUpload from "express-fileupload"; | |
import * as router from "./routes/index.js"; | |
import { Database } from "./database.js"; | |
dotenv.config({path: '/app/debug.env'}); | |
const app = express(); | |
const db = new Database('admin.db'); | |
app.use(express.json()); | |
app.use(cookieParser()); | |
app.use( | |
fileUpload({ | |
limits: { | |
fileSize: 2 * 1024 * 1024 // 2 MB | |
}, | |
abortOnLimit: true | |
}) | |
); | |
nunjucks.configure('views', { | |
autoescape: true, | |
express: app | |
}); | |
app.disable('etag'); | |
app.set('views', './views'); | |
app.use('/static', express.static(path.resolve('static'))); | |
app.use(router.default(db)); | |
app.all('*', (req, res) => { | |
return res.status(404).send({ | |
message: '404 page not found' | |
}); | |
}); | |
(async () => { | |
await db.connect(); | |
await db.migrate(); | |
app.listen(1337, '0.0.0.0', () => console.log('Listening on port 1337')); | |
})(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import path from 'path'; | |
import * as fs from 'fs'; | |
import axios from 'axios'; | |
import { Router } from 'express'; | |
import { sign } from '../helpers/JWTHelper.js'; | |
import { AuthMiddleware } from '../middleware/AuthMiddleware.js'; | |
import { AdminMiddleware } from '../middleware/AdminMiddleware.js'; | |
import { LocalMiddleware } from '../middleware/LocalMiddleware.js'; | |
import { execSync } from 'child_process'; | |
let db; | |
const router = Router(); | |
const response = data => ({ message: data }); | |
router.get('/', (req, res) => { | |
return res.render('index.html'); | |
}); | |
router.get('/register', (req, res) => { | |
return res.render('register.html'); | |
}); | |
router.get('/login', (req, res) => { | |
return res.render('login.html'); | |
}); | |
router.post('/api/register', async (req, res) => { | |
const { username, password } = req.body; | |
if (username && password) { | |
return db.getUser(username) | |
.then(user => { | |
if (user) return res.status(401).send(response('This username is already registered!')); | |
return db.registerUser(username, password) | |
.then(() => res.send(response('Account registered successfully!'))) | |
}) | |
.catch(() => res.status(500).send(response('Something went wrong!'))); | |
} | |
return res.status(401).send(response('Please fill out all the required fields!')); | |
}); | |
router.post('/api/login', async (req, res) => { | |
const { username, password } = req.body; | |
if (username && password) { | |
return db.loginUser(username, password) | |
.then(user => { | |
let token = sign({ username: user.username }); | |
res.cookie('session', token, { maxAge: 3600000 }); | |
return res.send(response('User authenticated successfully!')); | |
}) | |
.catch(() => res.status(403).send(response('Invalid username or password!'))); | |
} | |
return res.status(500).send(response('Missing parameters!')); | |
}); | |
router.get('/dashboard', AuthMiddleware, async (req, res) => { | |
if (req.user.username === 'admin') return res.redirect('/admin'); | |
return db.getUser(req.user.username) | |
.then(user => { | |
if (!user) return res.redirect('/login'); | |
return db.getFormData(user.username) | |
.then(enrollment => { | |
res.render('dashboard.html', { user, enrollment }); | |
}); | |
}) | |
.catch(e => { | |
return res.redirect('/login'); | |
}) | |
}); | |
router.post('/api/enroll', AuthMiddleware, async (req, res) => { | |
return db.getUser(req.user.username) | |
.then(user => { | |
if (!user) return res.redirect('/login'); | |
const {full_name, phone, birth_date, gender, biography} = req.body; | |
return db.updateEnrollment( | |
full_name, | |
phone, | |
birth_date, | |
gender, | |
biography ,user.username | |
) | |
.then(() => res.send(response('Your information is saved successfully!'))) | |
.catch((e) => res.status(401).send(response('Something went wrong!'))); | |
}) | |
.catch(e => { | |
return res.redirect('/login'); | |
}) | |
}); | |
router.post('/api/upload', AuthMiddleware, async (req, res) => { | |
return db.getUser(req.user.username) | |
.then(async user => { | |
if (!user) return res.redirect('/login'); | |
if (!req.files || !req.files.resumeFile) { | |
return res.status(400).send(response('No files were uploaded.')); | |
} | |
let enrollment = await db.getFormData(user.username); | |
let resumeFile = req.files.resumeFile; | |
let uploadFile = `${resumeFile.md5}.docx`; | |
resumeFile.mv(path.join('/app/uploads', uploadFile), (err) => { | |
if (err) return res.status(500).send(response('Something went wrong!')); | |
}); | |
if(enrollment.resume_file && enrollment.resume_file !== uploadFile){ | |
try { | |
fs.unlinkSync(path.join('/app/uploads', enrollment.resume_file)); | |
} | |
catch (e) { console.log(e) } | |
} | |
return db.setResume(uploadFile,user.username) | |
.then(() =>{ | |
res.send({ | |
'message': 'Resume file uploaded successfully!', | |
'filename': uploadFile | |
}); | |
}) | |
.catch(() => res.status(500).send(response('Something went wrong!'))); | |
}) | |
.catch(e => { | |
return res.redirect('/login'); | |
}) | |
}); | |
router.get('/download', AuthMiddleware, async (req, res) => { | |
return db.getUser(req.user.username) | |
.then(user => { | |
if (!user) return res.redirect('/login'); | |
let { resume } = req.query; | |
resume = resume.replaceAll('../', ''); | |
return res.download(path.join('/app/uploads', resume)); | |
}) | |
.catch(e => { | |
return res.redirect('/login'); | |
}) | |
}); | |
router.get('/admin', AdminMiddleware, async (req, res) => { | |
return res.render('admin.html', { user: req.user }); | |
}); | |
router.get('/sms-settings', AdminMiddleware, async (req, res) => { | |
return res.render('sms-settings.html', { user: req.user }); | |
}); | |
router.post('/api/sms/save', AdminMiddleware, async (req, res) => { | |
const { verb, url, params, headers, resp_ok, resp_bad } = req.body; | |
if (!(verb && url && params && headers && resp_ok && resp_bad)) { | |
return res.status(500).send(response('missing required parameters')); | |
} | |
return db.saveSMSConfig(verb, url, params, headers, resp_ok, resp_bad) | |
.then(() => { | |
return res.send(response('SMS settings saved successfully!')); | |
}) | |
.catch((e) => { | |
return res.status(500).send(response('Something went wrong!')); | |
}); | |
}); | |
router.post('/api/sms/test', AdminMiddleware, async (req, res) => { | |
const { verb, url, params, headers, resp_ok, resp_bad } = req.body; | |
if (!(verb && url && params && headers && resp_ok && resp_bad)) { | |
return res.status(500).send(response('missing required parameters')); | |
} | |
let parsedHeaders = {}; | |
try { | |
let headersArray = headers.split('\n'); | |
for(let header of headersArray) { | |
if(header.includes(':')) { | |
let hkey = header.split(':')[0].trim() | |
let hval = header.split(':')[1].trim() | |
parsedHeaders[hkey] = hval; | |
} | |
} | |
} | |
catch (e) { console.log(e) } | |
let options = { | |
method: verb.toLowerCase(), | |
url: url, | |
timeout: 5000, | |
headers: parsedHeaders | |
}; | |
if (verb === 'POST') options.data = params; | |
axios(options) | |
.then(response => { | |
if (typeof(response.data) == 'object') { | |
response.data = JSON.stringify(response.data); | |
} | |
return res.json({status: 'success', result: response.data}) | |
}) | |
.catch(e => { | |
if (e.response) { | |
if (typeof(e.response.data) == 'object') { | |
e.response.data = JSON.stringify(e.response.data); | |
} | |
return res.json({status: 'fail', result: e.response.data}) | |
} | |
else { | |
return res.json({status: 'fail', result: 'Address is unreachable'}); | |
} | |
}) | |
}); | |
router.get('/sql-prompt', AdminMiddleware, async (req, res) => { | |
return res.render('sql-prompt.html', { user: req.user }); | |
}); | |
router.post('/debug/sql/exec', LocalMiddleware, AdminMiddleware, async (req, res) => { | |
const { sql, password } = req.body; | |
if (sql && password === process.env.DEBUG_PASS) { | |
try { | |
let safeSql = String(sql).replaceAll(/"/ig, "'"); | |
let cmdStr = `sqlite3 -csv admin.db "${safeSql}"`; | |
const cmdExec = execSync(cmdStr); | |
return res.json({sql, output: cmdExec.toString()}); | |
} | |
catch (e) { | |
let output = e.toString(); | |
if (e.stderr) output = e.stderr.toString(); | |
return res.json({sql, output}); | |
} | |
} | |
return res.status(500).send(response('Invalid debug password supplied!')); | |
}); | |
router.get('/logout', (req, res) => { | |
res.clearCookie('session'); | |
return res.redirect('/'); | |
}); | |
export default database => { | |
db = database; | |
return router; | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { decode } from "../helpers/JWTHelper.js"; | |
const AuthMiddleware = async (req, res, next) => { | |
try{ | |
if (req.cookies.session === undefined) { | |
if(!req.is('application/json')) return res.redirect('/'); | |
return res.status(401).json({ status: 'unauthorized', message: 'Authentication required!' }); | |
} | |
return decode(req.cookies.session) | |
.then(user => { | |
req.user = user; | |
return next(); | |
}) | |
.catch((e) => { | |
console.log(e); | |
res.redirect('/logout'); | |
}); | |
} catch(e) { | |
console.log(e); | |
return res.redirect('/logout'); | |
} | |
} | |
export { AuthMiddleware }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Database as sqlite } from 'sqlite-async'; | |
import crypto from "crypto"; | |
const md5 = data => crypto.createHash('md5').update(data).digest("hex"); | |
export class Database { | |
constructor(db_file) { | |
this.db_file = db_file; | |
this.db = undefined; | |
} | |
async connect() { | |
this.db = await sqlite.open(this.db_file); | |
} | |
async migrate() { | |
let password = md5(crypto.randomBytes(16).toString('hex')); | |
return this.db.exec(` | |
DROP TABLE IF EXISTS users; | |
CREATE TABLE IF NOT EXISTS users ( | |
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, | |
username VARCHAR(255) NOT NULL UNIQUE, | |
password VARCHAR(255) NOT NULL, | |
verified BOOLEAN NOT NULL DEFAULT false | |
); | |
INSERT INTO users (username, password, verified) VALUES ('admin', '${password}', true); | |
DROP TABLE IF EXISTS enrollments; | |
CREATE TABLE IF NOT EXISTS enrollments ( | |
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, | |
username VARCHAR(255) NOT NULL UNIQUE, | |
full_name VARCHAR(255) NULL, | |
phone VARCHAR(255) NULL, | |
birth_date VARCHAR(255) NULL, | |
gender VARCHAR(255) NULL, | |
biography TEXT NULL, | |
resume_file VARCHAR(256) NULL | |
); | |
DROP TABLE IF EXISTS settings; | |
CREATE TABLE settings( | |
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, | |
config_key VARCHAR(2) NOT NULL, | |
config_val NUMERIC(9,6) NOT NULL | |
); | |
INSERT INTO settings (config_key, config_val) | |
VALUES | |
('sms_verb', 'POST'), | |
('sms_url', 'https://platform.clickatell.com/messages'), | |
('sms_params', '{"apiKey" : "xxxx", "toNumber": "recipient", "text": "message"}'), | |
('sms_headers', 'Content-Type: application/json\nAuthorization: Basic YWRtaW46YWRtaW4='), | |
('sms_resp_ok', '<status>ok</status>'), | |
('sms_resp_bad', '<status>error</status>'); | |
`); | |
} | |
async registerUser(username, password) { | |
return new Promise(async (resolve, reject) => { | |
try { | |
let register_sql = await this.db.prepare('INSERT INTO users (username, password) VALUES ( ?, ?)'); | |
let enrollment_sql = await this.db.prepare('INSERT INTO enrollments (username) VALUES (?)'); | |
await register_sql.run(username, md5(password)); | |
await enrollment_sql.run(username); | |
resolve(); | |
} catch(e) { | |
reject(e); | |
} | |
}); | |
} | |
async loginUser(username, password) { | |
return new Promise(async (resolve, reject) => { | |
try { | |
let stmt = await this.db.prepare('SELECT username FROM users WHERE username = ? and password = ?'); | |
resolve(await stmt.get(username, md5(password))); | |
} catch(e) { | |
reject(e); | |
} | |
}); | |
} | |
async getUser(username) { | |
return new Promise(async (resolve, reject) => { | |
try { | |
let stmt = await this.db.prepare('SELECT * FROM users WHERE username = ?'); | |
resolve(await stmt.get(username)); | |
} catch(e) { | |
reject(e); | |
} | |
}); | |
} | |
async checkUser(username) { | |
return new Promise(async (resolve, reject) => { | |
try { | |
let stmt = await this.db.prepare('SELECT username FROM users WHERE username = ?'); | |
let row = await stmt.get(username); | |
resolve(row !== undefined); | |
} catch(e) { | |
reject(e); | |
} | |
}); | |
} | |
async getFormData(username) { | |
return new Promise(async (resolve, reject) => { | |
try { | |
let stmt = await this.db.prepare('SELECT * FROM enrollments WHERE username = ?'); | |
resolve(await stmt.get(username)); | |
} catch(e) { | |
reject(e); | |
} | |
}); | |
} | |
async updateEnrollment(full_name, phone, birth_date, gender, biography, username) { | |
return new Promise(async (resolve, reject) => { | |
try { | |
let stmt = await this.db.prepare(` | |
UPDATE enrollments | |
SET | |
full_name = ?, | |
phone = ?, | |
birth_date = ?, | |
gender = ?, | |
biography = ? | |
WHERE username = ? | |
`); | |
resolve((await stmt.run(full_name, phone, birth_date, gender, biography, username))); | |
} catch(e) { | |
reject(e); | |
} | |
}); | |
} | |
async setResume(filename, username) { | |
return new Promise(async (resolve, reject) => { | |
try { | |
let stmt = await this.db.prepare('UPDATE enrollments SET resume_file = ? WHERE username = ?'); | |
resolve((await stmt.run(filename, username))); | |
} catch(e) { | |
reject(e); | |
} | |
}); | |
} | |
async saveSMSConfig(verb, url, params, headers, resp_ok, resp_bad) { | |
return new Promise(async (resolve, reject) => { | |
const smsConfig = { | |
'sms_verb': verb, | |
'sms_url': url, | |
'sms_params': params, | |
'sms_headers': headers, | |
'sms_resp_ok': resp_ok, | |
'sms_resp_bad': resp_bad | |
} | |
for(const [col_name, col_data] of Object.entries(smsConfig)) { | |
try { | |
let stmt = await this.db.prepare('UPDATE settings SET config_val = ? WHERE config_key = ?'); | |
await stmt.run(col_data, col_name) | |
} catch(e) { | |
reject(e); | |
} | |
} | |
resolve(true); | |
}); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
DEBUG_PASS=CzliwZJkV60hpPJ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const LocalMiddleware = async (req, res, next) => { | |
if (req.ip == '127.0.0.1' && req.headers.host == '127.0.0.1:1337') { | |
return next(); | |
} | |
return res.status(401).json({ message: 'Blocked: This endpoint is whitelisted to localhost only.' }); | |
} | |
export { LocalMiddleware }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment