diff --git a/.gitignore b/.gitignore index 34ba226..0f4f0ea 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,17 @@ venv/ #Ignore editor specific files .vimrc .vscode/ +*.bak + +#ignore Python specific files +*.pyc +__pycache__/ + +instance/ + +.pytest_cache/ +.coverage +htmlcov/ + +dist/ +build/ diff --git a/README.md b/README.md index 2245a79..ed80e64 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,23 @@ # Minzkrauts home-made CTF +## Requirements + - NGINX + - Python, Virtualenv + +Create and use python Virtualenv with Python 3 +`virtualenv -p python3 venv` +`source venv/bin/activate` + +Install all Python requirements with +`pip install -r requirements.txt` + +Move all NGINX configs to /etc/nginx/sites-avaliable and activate/symlink them. +## Platform API +Navigate into `ctf_platform`. +Run `flask init-db` to initialize the database if neccessary. +Start the Flask app with gunicorn using `./start_api.sh` +The API server will be listening on port `4910` +Configure NGINX reverse proxy to point at `http://127.0.0.1:4910` ## Letsencrypt certificates Navigate to certbot location and create certificate files with ``` diff --git a/ctf_platform/api/__init__.py.py b/ctf_platform/api/__init__.py.py new file mode 100755 index 0000000..cf6d380 --- /dev/null +++ b/ctf_platform/api/__init__.py.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python +import main \ No newline at end of file diff --git a/ctf_platform/api/auth.py b/ctf_platform/api/auth.py new file mode 100644 index 0000000..de1e561 --- /dev/null +++ b/ctf_platform/api/auth.py @@ -0,0 +1,96 @@ +from flask import Blueprint, g, redirect, request, session, url_for, jsonify +from flask import current_app as app +from werkzeug.security import check_password_hash, generate_password_hash +from .database import get_db +from .helper_functions import build_response_dict +import functools +bp = Blueprint('auth', __name__, url_prefix='/auth/') + + + +@bp.before_app_request +def load_logged_in_user(): + user_id = session.get('user_id') + + if user_id is None: + g.user = None + else: + db = get_db() + db.execute( + 'SELECT user_id, username, active, registered, info FROM user WHERE user_id = %s', (user_id) + ) + g.user = db.fetchone() + app.logger.info('USER ACCESS %s' % (g.user)) + +#@auth.login_required decorator +def login_required(view): + @functools.wraps(view) + def wrapped_view(**kwagrs): + if g.user is None: + api_response = build_response_dict(False, 'access', 'Access denied', reason='Login required') + return jsonify(api_response) + return view(**kwagrs) + + return wrapped_view + +@bp.route("/register", methods=['POST']) +def register(): + api_response = {} + error = None + username = request.form.get('username') + password = request.form.get('password') + db = get_db() + if not username: + error = 'Username required!' + elif not password: + error = 'Password required!' + else: + db.execute('SELECT user_id FROM user WHERE username = %s', (username)) + if db.fetchone(): + error = "Username already exists!" + if error is None: + password_hash = generate_password_hash(password) + db.execute('INSERT INTO user (username, password) VALUES (%s, %s)', (username, password_hash)) + g.db.commit() + api_response = build_response_dict(True, 'register', 'Registration successful') + else: + api_response = build_response_dict(False, 'register', 'Registration failed', reason=error) + return jsonify(api_response) + +@bp.route("/login", methods=['POST']) +def login(): + api_response = {} + error = None + username = request.form.get('username') + password = request.form.get('password') + if not username: + error = 'Username required!' + elif not password: + error = 'Password required!' + else: + db = get_db() + db.execute("SELECT * FROM user WHERE username = %s", (username)) + db_user = db.fetchone() + if db_user is None or not check_password_hash(db_user['password'], password): + error = 'Username or password incorrect!' + if error is None: + session.clear() + session['user_id'] = db_user['user_id'] + session['user_name'] = db_user['username'] + api_response = build_response_dict(True, 'login', 'Login successful') + app.logger.info("{} logged in successfully!".format(username)) + else: + api_response = build_response_dict(False, 'login', 'Login failed', reason=error) + app.logger.info("{} failed to login ({})!".format(username, error)) + + return jsonify(api_response) + +@bp.route('/logout', methods=['GET']) +def logout(): + if 'user_name' in session: + app.logger.info("{} logged out!".format(session['user_name'])) + session.clear() + api_response = build_response_dict(True, 'logout', 'Logout successful') + else: + api_response = build_response_dict(False, 'logout', 'Logout failed', reason='No active session') + return jsonify(api_response) \ No newline at end of file diff --git a/ctf_platform/api/database.py b/ctf_platform/api/database.py new file mode 100644 index 0000000..9081d79 --- /dev/null +++ b/ctf_platform/api/database.py @@ -0,0 +1,50 @@ +import pymysql, click, re +from flask import current_app as app +from flask import g +from flask.cli import with_appcontext + +def get_db(): + if 'db' not in g: + mysql = pymysql.connect(host=app.config['MYSQL_DATABASE_HOST'], + user=app.config['MYSQL_DATABASE_USER'], + password=app.config['MYSQL_DATABASE_PASS'], + db=app.config['MYSQL_DATABASE_DB'], + cursorclass=pymysql.cursors.DictCursor) + g.db = mysql + g.db_cursor = mysql.cursor() + return g.db_cursor + +def close_db(e=None): + db = g.pop('db', None) + + if db is not None: + db.close() + +def init_db(schema): + db = mysql = pymysql.connect(host=app.config['MYSQL_DATABASE_HOST'], + user=app.config['MYSQL_DATABASE_USER'], + password=app.config['MYSQL_DATABASE_PASS']) + statement = "" + for line in open(schema): + if line.strip().startswith('--'): # ignore sql comment lines + continue + if not line.strip().endswith(';'): # keep appending lines that don't end in ';' + statement = statement + line + else: # when you get a line ending in ';' then exec statement and reset for next statement + statement = statement + line + try: + db.cursor().execute(statement) + except Exception as e: + print("[WARN] MySQLError during execute statement \n\tArgs: '{}'".format(str(e.args))) + statement = "" + +@click.command('init-db') +@click.argument('schema') +@with_appcontext +def init_db_command(schema): + init_db(schema) + click.echo('Initialized the database.') + +def init_app(app): + app.teardown_appcontext(close_db) + app.cli.add_command(init_db_command) \ No newline at end of file diff --git a/ctf_platform/api/flag.py b/ctf_platform/api/flag.py new file mode 100644 index 0000000..4b4a1e6 --- /dev/null +++ b/ctf_platform/api/flag.py @@ -0,0 +1 @@ +#Flag blueprint \ No newline at end of file diff --git a/ctf_platform/api/helper_functions.py b/ctf_platform/api/helper_functions.py new file mode 100644 index 0000000..51c78e9 --- /dev/null +++ b/ctf_platform/api/helper_functions.py @@ -0,0 +1,11 @@ +def build_response_dict(success, action, message, reason=None): + response_dict = {} + if success: + response_dict['data'] = { action: 'success', 'message': message} + if reason is not None: + response_dict['data']['reason'] = reason + elif not success: + response_dict['error'] = { action: 'failed', 'message': message} + if reason is not None: + response_dict['error']['reason'] = reason + return response_dict \ No newline at end of file diff --git a/ctf_platform/api/main.py b/ctf_platform/api/main.py new file mode 100644 index 0000000..a204814 --- /dev/null +++ b/ctf_platform/api/main.py @@ -0,0 +1,17 @@ +from flask import Flask, jsonify +import os, logging, sys + +def create_app(): + app = Flask(__name__) + app.config.from_pyfile('../config.cfg') + app.secret_key = os.urandom(24) + from . import auth, database + database.init_app(app) + app.register_blueprint(auth.bp) + + @app.route("/") + def home(): + info = { 'data' : { 'version': '1.0' }} + return jsonify(info) + + return app \ No newline at end of file diff --git a/ctf_platform/config.cfg b/ctf_platform/config.cfg new file mode 100644 index 0000000..0df8215 --- /dev/null +++ b/ctf_platform/config.cfg @@ -0,0 +1,5 @@ +MYSQL_DATABASE_HOST = 'localhost' +MYSQL_DATABASE_USER = 'ctf_user' +MYSQL_DATABASE_PASS = '' +MYSQL_DATABASE_DB = 'ctf_main' +DEBUG = True \ No newline at end of file diff --git a/ctf_platform/ctf_db.mwb b/ctf_platform/ctf_db.mwb new file mode 100644 index 0000000..d188c90 Binary files /dev/null and b/ctf_platform/ctf_db.mwb differ diff --git a/ctf_platform/database.db b/ctf_platform/database.db new file mode 100644 index 0000000..a7d7cc1 Binary files /dev/null and b/ctf_platform/database.db differ diff --git a/ctf_platform/schema.sql b/ctf_platform/schema.sql new file mode 100644 index 0000000..1f7651f --- /dev/null +++ b/ctf_platform/schema.sql @@ -0,0 +1,70 @@ +-- MySQL Workbench Forward Engineering + +SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0; +SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0; +SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='TRADITIONAL,ALLOW_INVALID_DATES'; + +-- ----------------------------------------------------- +-- Schema ctf_main +-- ----------------------------------------------------- +DROP SCHEMA IF EXISTS `ctf_main` ; + +-- ----------------------------------------------------- +-- Schema ctf_main +-- ----------------------------------------------------- +CREATE SCHEMA IF NOT EXISTS `ctf_main` DEFAULT CHARACTER SET utf8 ; +USE `ctf_main` ; + +-- ----------------------------------------------------- +-- Table `ctf_main`.`challenge` +-- ----------------------------------------------------- +CREATE TABLE IF NOT EXISTS `ctf_main`.`challenge` ( + `challenge_id` INT NOT NULL, + `challenge_name` VARCHAR(45) NOT NULL, + `challenge_description` VARCHAR(45) NULL, + `challenge_flag` VARCHAR(45) NOT NULL, + `challenge_score` INT NOT NULL, + PRIMARY KEY (`challenge_id`)) +ENGINE = InnoDB; + + +-- ----------------------------------------------------- +-- Table `ctf_main`.`user` +-- ----------------------------------------------------- +CREATE TABLE IF NOT EXISTS `ctf_main`.`user` ( + `user_id` INT NOT NULL AUTO_INCREMENT, + `username` VARCHAR(45) NOT NULL, + `password` VARCHAR(160) NOT NULL, + `active` TINYINT NOT NULL DEFAULT 1, + `registered` DATETIME NULL DEFAULT NOW(), + `info` VARCHAR(500) NULL, + PRIMARY KEY (`user_id`), + UNIQUE INDEX `user_id_UNIQUE` (`user_id` ASC)) +ENGINE = InnoDB; + + +-- ----------------------------------------------------- +-- Table `ctf_main`.`solved_challenges` +-- ----------------------------------------------------- +CREATE TABLE IF NOT EXISTS `ctf_main`.`solved_challenges` ( + `user_id` INT NOT NULL, + `challenge_id` INT NOT NULL, + `submission_timestamp` DATETIME NOT NULL, + PRIMARY KEY (`user_id`), + INDEX `fk_challenge_id_idx` (`challenge_id` ASC), + CONSTRAINT `fk_user_id` + FOREIGN KEY (`user_id`) + REFERENCES `ctf_main`.`user` (`user_id`) + ON DELETE NO ACTION + ON UPDATE NO ACTION, + CONSTRAINT `fk_challenge_id` + FOREIGN KEY (`challenge_id`) + REFERENCES `ctf_main`.`challenge` (`challenge_id`) + ON DELETE NO ACTION + ON UPDATE NO ACTION) +ENGINE = InnoDB; + + +SET SQL_MODE=@OLD_SQL_MODE; +SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS; +SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS; diff --git a/ctf_platform/start_api.sh b/ctf_platform/start_api.sh new file mode 100755 index 0000000..e004b31 --- /dev/null +++ b/ctf_platform/start_api.sh @@ -0,0 +1,2 @@ +gunicorn wsgi:app -b localhost:4910 + diff --git a/ctf_platform/wsgi.py b/ctf_platform/wsgi.py new file mode 100644 index 0000000..18a66d2 --- /dev/null +++ b/ctf_platform/wsgi.py @@ -0,0 +1,2 @@ +from api import main +app = main.create_app() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index e16e2cc..87a6eb7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,20 @@ +asn1crypto==0.24.0 certifi==2018.4.16 +cffi==1.11.5 chardet==3.0.4 +click==6.7 +cryptography==2.2.2 +Flask==1.0.2 +Flask-MySQL==1.4.0 +gunicorn==19.9.0 idna==2.7 +itsdangerous==0.24 +Jinja2==2.10 +MarkupSafe==1.0 +mysqlclient==1.3.13 +pycparser==2.18 +PyMySQL==0.9.2 requests==2.19.1 +six==1.11.0 urllib3==1.23 +Werkzeug==0.14.1