From 0838a0a5ce7e76aa033d3b3c5853dec2c6ae8b52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Gro=C3=9F?= Date: Sat, 14 Jul 2018 20:55:46 +0200 Subject: [PATCH] CTF Platform API basic auth Addded ctf platform Flask app with basic auth functions register, login, logout Added ctf platform database design (MySQL Workbench) Added forward engineered SQL file Added wsgi startup script Installed various python dependencies README.md Updated .gitignore Updated --- .gitignore | 14 ++++ README.md | 18 +++++ ctf_platform/api/__init__.py.py | 2 + ctf_platform/api/auth.py | 96 +++++++++++++++++++++++++++ ctf_platform/api/database.py | 50 ++++++++++++++ ctf_platform/api/flag.py | 1 + ctf_platform/api/helper_functions.py | 11 +++ ctf_platform/api/main.py | 17 +++++ ctf_platform/config.cfg | 5 ++ ctf_platform/ctf_db.mwb | Bin 0 -> 6984 bytes ctf_platform/database.db | Bin 0 -> 2048 bytes ctf_platform/schema.sql | 70 +++++++++++++++++++ ctf_platform/start_api.sh | 2 + ctf_platform/wsgi.py | 2 + requirements.txt | 15 +++++ 15 files changed, 303 insertions(+) create mode 100755 ctf_platform/api/__init__.py.py create mode 100644 ctf_platform/api/auth.py create mode 100644 ctf_platform/api/database.py create mode 100644 ctf_platform/api/flag.py create mode 100644 ctf_platform/api/helper_functions.py create mode 100644 ctf_platform/api/main.py create mode 100644 ctf_platform/config.cfg create mode 100644 ctf_platform/ctf_db.mwb create mode 100644 ctf_platform/database.db create mode 100644 ctf_platform/schema.sql create mode 100755 ctf_platform/start_api.sh create mode 100644 ctf_platform/wsgi.py 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 0000000000000000000000000000000000000000..d188c90c1208500506548af75707436a350f6cb8 GIT binary patch literal 6984 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
D z9#RyKYyYTh)AX{0A1x4ypupSb33v(R%1e5=1h}=`V^g8lYcBa&kx;_-6Ahf0Dp*aA zymGH_I;ZP34t;j}+eDOgmstrWH5cCJm7*2_i3p@gtm#*3!}dT4X_0tfO=9AH0Cbt5 zSCE&Y`E4;QP#h<;V^K*b>qLczRqIOF`uIQ=S!U6hD!i0NYO++Nbxg|C49#48XDFS( z*)dELNIJjkNA;~8i1kyfP&aOG(+8&J_G>6w@S2maC|!}VV)Py3uF^08x(NT`h{~`} z;P5nw(L0lLZZ++<6HUNym(ee4cWG!QN6>l5@5eTK71{H2yb$BYy1T<1f1kcL19Fb> zeD1o`GQV)khT1v{FLo_3E#tj=*p7b&&V9yV8+$11#Opvu3_elK`9xkEFs9LWt zp1%p?pxgZRj=o4AUQcx(bN96TdvfiZ~oviJ_hJV1q}5_LG0PvBYs;&J+p?lt_Q#p z#Y6?PkHy}IeHO=hd_nEVV~)xmJWP%9!-{IBd6hjXy6QIIkRb+baadjA9Cs*R%Q9-* zOxR~SKv~sX;m^y~SuxUF38;2ocY{`Wvi9heHFzklazY(e)|8EcRFt!C*tjQh^P9@0 z>QW2An&(f__2(0l$*FLLDv#t8b#*ShBN^yHyFjLRW|L{Tib!o|;>&G(&eq_DVNxs% z_lA(MqVx#=*V)%z_ar?l1NesW^SI0XN=emF6O8g#U;47Y{a^ExpS9!~L3Q|CO=z9BqFN15QFFCGR1UA}m9U%lr3C6&oLI-^SAcCgTAL@4@;hK7 z9tv-9@J!ENH9&=bV$T;oBV!i29V4VAtXV3*ahCMT4(@QXF=bKyU0&_uq1KKL*!N#bve1Wq?2`g3aU9~9AV7> z``@=qY{5V}PDbA!phPh$Q~BH}a0~4YPOVtMW6tb>3wpIDb11*^C?l0;z8~HDqGC55 zk>2RYHE5iEkvnArMLB>bejQD5mwEF#9tgqLgNPbEUxg(nQ#~slzshfb zJ#%kVnsdkenyNBE&Cl{#+j!$1K{>%>&HaTe?YL8mwXSRerlkwQs#TV#GVDVrdauJQrrw2yR z1|aa zqstEOM?=&0^cVd3vr>%YmttV3hYS%?%s%Q=lDf1^iNwCG2H7M|`uPTR+WF|3YmF;pZ)F$) z`1uQO(BS20-&ZI;I-hnk`Q))L{|~L^f1BV42!el2@X9Q8I@G`B_df#VZ`eD5?cVSS z^CA4t7~r2{_Wk&<2c*A!1_T7GzeU>0lH1C{-Ga-?@-$=Jah8X8D7VDK0J`C*+J1?n z6`eMsJ~OvW0%fDM{AjY+(_-r|G|hGmZdgW7?`EQjw1%O2BhZ+8IrrhHuhga1j&CF3 z?04nxPu%AYUyry{);G3pM517$_tC@_AkTF3bRz>05wPGH=+&-rG9X*4ckjm%>*pS~L-%<7}hFNeZ-klgZhkoqm|0 zHaeS=8BetQgl#oDGxNZ$!&9eHz{c(CP8_+VBTJA-q6sn zSw_Td{%X;$*lF9P%xYLw(&wscYEgsmMFDi)Q*A%6@jZ2Z*kT~87#z> zm3o;%rl1+^dW`v)Zq|bKY+Pi$q^?7ZYx2ri!MllS)IYzF{niimBLBe8(8P95MoCy0h35;(w)z;K9xSW9g+T z3OiHLvZJYYzC!dY*?G6M=ev7HM_- z!gd-S)P&O!<{5dW-fiP)svsf}Bme&krN27)=V=Q5NBO_&rGK;kvl9QsML=i@MkV`~ zF8|H=&!YS{gX1sbe@RqR1r_ZdP2|6~{BPBuKtK>hQ1j7#rwY__a<#LBID$U{)tsy# o_CO0)@JAa@2#}YH2h-sn2&WtPBgDbNO%w=r|6t}|VdIGTUs1O*YXATM literal 0 HcmV?d00001 diff --git a/ctf_platform/database.db b/ctf_platform/database.db new file mode 100644 index 0000000000000000000000000000000000000000..a7d7cc1d7537e52105c1334619752716a1163e75 GIT binary patch literal 2048 zcmWFz^vNtqRY=P(%1ta$FlJz3U}R))P*7lCU|`*8;^ zP!%Kd0tO%&1*0J_8UiGQKpZ2xxU4K=BY#O^Qch}dNoh)IUP&>GW^xX4bqsM;2yt}s zaaDkcDrn>-=B6ryxJHENC?uw&6hWEEnI)A_W