diff --git a/.gitignore b/.gitignore index db4561e..3c1bade 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,6 @@ docs/_build/ # PyBuilder target/ + +*.db +*.log \ No newline at end of file diff --git a/CTFd/__init__.py b/CTFd/__init__.py new file mode 100644 index 0000000..80f3589 --- /dev/null +++ b/CTFd/__init__.py @@ -0,0 +1,76 @@ +from flask import Flask, render_template, request, redirect, abort, session, jsonify, json as json_mod, url_for +from flask.ext.sqlalchemy import SQLAlchemy +from flask.ext.mail import Mail, Message +from logging.handlers import RotatingFileHandler +from flask.ext.session import Session +import logging +import os +import sqlalchemy + +def create_app(subdomain, username="", password=""): + app = Flask("CTFd", static_folder="../static", template_folder="../templates") + with app.app_context(): + app.config.from_object('CTFd.config') + + if subdomain: + app.config.update( + SQLALCHEMY_DATABASE_URI = 'mysql://'+username+':'+password+'@localhost:3306/' + subdomain + '_ctfd', + HOST = subdomain + app.config["HOST"], + SESSION_FILE_DIR = app.config['SESSION_FILE_DIR'] + "/" + subdomain, + DEBUG = True + ) + + from CTFd.models import db, Teams, Solves, Challenges, WrongKeys, Keys, Tags, Files, Tracking + + db.init_app(app) + db.create_all() + + app.db = db + # app.setup = True + + mail = Mail(app) + + Session(app) + + from CTFd.views import init_views + init_views(app) + from CTFd.errors import init_errors + init_errors(app) + from CTFd.challenges import init_challenges + init_challenges(app) + from CTFd.scoreboard import init_scoreboard + init_scoreboard(app) + from CTFd.auth import init_auth + init_auth(app) + from CTFd.admin import init_admin + init_admin(app) + from CTFd.utils import init_utils + init_utils(app) + + return app + + +# logger_keys = logging.getLogger('keys') +# logger_logins = logging.getLogger('logins') +# logger_regs = logging.getLogger('regs') + +# logger_keys.setLevel(logging.INFO) +# logger_logins.setLevel(logging.INFO) +# logger_regs.setLevel(logging.INFO) + +# try: +# parent = os.path.dirname(__file__) +# except: +# parent = os.path.dirname(os.path.realpath(sys.argv[0])) + +# key_log = RotatingFileHandler(os.path.join(parent, 'logs', 'keys.log'), maxBytes=10000) +# login_log = RotatingFileHandler(os.path.join(parent, 'logs', 'logins.log'), maxBytes=10000) +# register_log = RotatingFileHandler(os.path.join(parent, 'logs', 'registers.log'), maxBytes=10000) + +# logger_keys.addHandler(key_log) +# logger_logins.addHandler(login_log) +# logger_regs.addHandler(register_log) + +# logger_keys.propagate = 0 +# logger_logins.propagate = 0 +# logger_regs.propagate = 0 diff --git a/CTFd/admin.py b/CTFd/admin.py new file mode 100644 index 0000000..3bf8e2c --- /dev/null +++ b/CTFd/admin.py @@ -0,0 +1,397 @@ +from CTFd import render_template, request, redirect, abort, jsonify, url_for, session +from CTFd.utils import sha512, is_safe_url, authed, admins_only, is_admin, unix_time, unix_time_millis +from CTFd.models import db, Teams, Solves, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config +from itsdangerous import TimedSerializer, BadTimeSignature +from werkzeug.utils import secure_filename +from socket import inet_aton, inet_ntoa +from passlib.hash import bcrypt_sha256 +from flask import current_app as app + +import logging +import hashlib +import time +import re +import os +import json + +def init_admin(app): + @app.route('/admin', methods=['GET', 'POST']) + def admin(): + if request.method == 'POST': + username = request.form.get('name') + password = request.form.get('password') + + admin = Teams.query.filter_by(name=request.form['name'], admin=True).first() + if admin and bcrypt_sha256.verify(request.form['password'], admin.password): + session.regenerate() # NO SESSION FIXATION FOR YOU + session['username'] = admin.name + session['id'] = admin.id + session['admin'] = True + session['nonce'] = sha512(os.urandom(10)) + db.session.close() + return redirect('/admin/graphs') + + if is_admin(): + return redirect('/admin/graphs') + + return render_template('admin/login.html') + + @app.route('/admin/graphs') + @admins_only + def admin_graphs(): + return render_template('admin/graphs.html') + + @app.route('/admin/config', methods=['GET', 'POST']) + @admins_only + def admin_config(): + if request.method == "POST": + start = request.form['start'] + end = request.form['end'] + + if not start: + start = None + else: + start = int(start) + if not end: + end = None + else: + end = int(end) + + print repr(start), repr(end) + + db_start = Config.query.filter_by(key='start').first() + db_start.value = start + + db_end = Config.query.filter_by(key='end').first() + db_end.value = end + + db.session.add(db_start) + db.session.add(db_end) + + db.session.commit() + return redirect('/admin/config') + start = Config.query.filter_by(key="start").first().value + end = Config.query.filter_by(key="end").first().value + return render_template('admin/config.html', start=start, end=end) + + @app.route('/admin/pages', defaults={'route': None}, methods=['GET', 'POST']) + @app.route('/admin/pages/', methods=['GET', 'POST']) + @admins_only + def admin_pages(route): + if route and request.method == 'GET': + page = Pages.query.filter_by(route=route).first() + return render_template('admin/editor.html', page=page) + if route and request.method == 'POST': + page = Pages.query.filter_by(route=route).first() + errors = [] + html = request.form['html'] + route = request.form['route'] + if not route: + errors.append('Missing URL route') + if errors: + page = Pages(html, "") + return render_template('/admin/editor.html', page=page) + if page: + page.route = route + page.html = html + db.session.commit() + return redirect('/admin/pages') + page = Pages(route, html) + db.session.add(page) + db.session.commit() + return redirect('/admin/pages') + if not route and request.method == 'POST': + return render_template('admin/editor.html') + pages = Pages.query.all() + return render_template('admin/pages.html', routes=pages) + + + @app.route('/admin/chals', methods=['POST', 'GET']) + @admins_only + def admin_chals(): + # if authed(): + if request.method == 'POST': + chals = Challenges.query.add_columns('id', 'name', 'value', 'description', 'category').order_by(Challenges.value).all() + + json = {'game':[]} + for x in chals: + json['game'].append({'id':x[1], 'name':x[2], 'value':x[3], 'description':x[4], 'category':x[5]}) + + db.session.close() + return jsonify(json) + else: + return render_template('admin/chals.html') + + @app.route('/admin/keys/', methods=['POST', 'GET']) + @admins_only + def admin_keys(chalid): + if request.method == 'GET': + keys = Keys.query.filter_by(chal=chalid).all() + json = {'keys':[]} + for x in keys: + json['keys'].append({'id':x.id, 'key':x.flag, 'type':x.key_type}) + return jsonify(json) + elif request.method == 'POST': + keys = Keys.query.filter_by(chal=chalid).all() + for x in keys: + db.session.delete(x) + + newkeys = request.form.getlist('keys[]') + newvals = request.form.getlist('vals[]') + for flag, val in zip(newkeys, newvals): + key = Keys(chalid, flag, val) + db.session.add(key) + + db.session.commit() + db.session.close() + return '1' + + @app.route('/admin/tags/', methods=['GET', 'POST']) + @admins_only + def admin_tags(chalid): + if request.method == 'GET': + tags = Tags.query.filter_by(chal=chalid).all() + json = {'tags':[]} + for x in tags: + json['tags'].append({'id':x.id, 'chal':x.chal, 'tag':x.tag}) + return jsonify(json) + + elif request.method == 'POST': + newtags = request.form.getlist('tags[]') + for x in newtags: + tag = Tags(chalid, x) + db.session.add(tag) + db.session.commit() + db.session.close() + return '1' + + @app.route('/admin/tags//delete', methods=['POST']) + @admins_only + def admin_delete_tags(tagid): + if request.method == 'POST': + tag = Tags.query.filter_by(id=tagid).first_or_404() + db.session.delete(tag) + db.session.commit() + db.session.close() + return "1" + + + @app.route('/admin/files/', methods=['GET', 'POST']) + @admins_only + def admin_files(chalid): + if request.method == 'GET': + files = Files.query.filter_by(chal=chalid).all() + json = {'files':[]} + for x in files: + json['files'].append({'id':x.id, 'file':x.location}) + return jsonify(json) + if request.method == 'POST': + if request.form['method'] == "delete": + f = Files.query.filter_by(id=request.form['file']).first_or_404() + if os.path.isfile(f.location): + os.unlink(f.location) + db.session.delete(f) + db.session.commit() + db.session.close() + return "1" + elif request.form['method'] == "upload": + files = request.files.getlist('files[]') + + for f in files: + filename = secure_filename(f.filename) + + if len(filename) <= 0: + continue + + md5hash = hashlib.md5(os.urandom(64)).hexdigest() + + # BUG NEEDS TO GO TO S3 + if not os.path.exists(os.path.join(app.config['UPLOAD_FOLDER'], md5hash)): + os.makedirs(os.path.join(app.config['UPLOAD_FOLDER'], md5hash)) + + f.save(os.path.join(app.config['UPLOAD_FOLDER'], md5hash, filename)) + db_f = Files(chalid, os.path.join(app.config['UPLOAD_FOLDER'], md5hash, filename)) + db.session.add(db_f) + + db.session.commit() + db.session.close() + return redirect('/admin/chals') + + @app.route('/admin/teams') + @admins_only + def admin_teams(): + teams = Teams.query.all() + return render_template('admin/teams.html', teams=teams) + + @app.route('/admin/team/', methods=['GET', 'POST']) + @admins_only + def admin_team(teamid): + user = Teams.query.filter_by(id=teamid).first() + solves = Solves.query.filter_by(teamid=teamid).all() + addrs = Tracking.query.filter_by(team=teamid).group_by(Tracking.ip).all() + db.session.close() + + if request.method == 'GET': + return render_template('admin/team.html', solves=solves, team=user, addrs=addrs) + elif request.method == 'POST': + json = {'solves':[]} + for x in solves: + json['solves'].append({'id':x.id, 'chal':x.chalid, 'team':x.teamid}) + return jsonify(json) + + @app.route('/admin/team//ban', methods=['POST']) + @admins_only + def ban(teamid): + user = Teams.query.filter_by(id=teamid).first() + user.banned = 1; + db.session.commit() + return redirect('/scoreboard') + + @app.route('/admin/team//unban', methods=['POST']) + @admins_only + def unban(teamid): + user = Teams.query.filter_by(id=teamid).first() + user.banned = None; + db.session.commit() + return redirect('/scoreboard') + + + @app.route('/admin/graphs/') + @admins_only + def admin_graph(graph_type): + if graph_type == 'categories': + categories = db.session.query(Challenges.category, db.func.count(Challenges.category)).group_by(Challenges.category).all() + json = {'categories':[]} + for category, count in categories: + json['categories'].append({'category':category, 'count':count}) + return jsonify(json) + elif graph_type == "solves": + solves = Solves.query.add_columns(db.func.count(Solves.chalid)).group_by(Solves.chalid).all() + json = {} + for chal, count in solves: + json[chal.chal.name] = count + return jsonify(json) + + @app.route('/admin/scoreboard') + @admins_only + def admin_scoreboard(): + score = db.func.sum(Challenges.value).label('score') + quickest = db.func.max(Solves.date).label('quickest') + teams = db.session.query(Solves.teamid, Teams.name, score).join(Teams).join(Challenges).filter(Teams.banned == None).group_by(Solves.teamid).order_by(score.desc(), quickest) + db.session.close() + return render_template('admin/scoreboard.html', teams=teams) + + @app.route('/admin/scores') + @admins_only + def admin_scores(): + score = db.func.sum(Challenges.value).label('score') + quickest = db.func.max(Solves.date).label('quickest') + teams = db.session.query(Solves.teamid, Teams.name, score).join(Teams).join(Challenges).filter(Teams.banned == None).group_by(Solves.teamid).order_by(score.desc(), quickest) + db.session.close() + json = {'teams':[]} + for i, x in enumerate(teams): + json['teams'].append({'place':i+1, 'id':x.teamid, 'name':x.name,'score':int(x.score)}) + return jsonify(json) + + @app.route('/admin/solves/', methods=['GET']) + @admins_only + def admin_solves(teamid="all"): + if teamid == "all": + solves = Solves.query.all() + else: + solves = Solves.query.filter_by(teamid=teamid).all() + db.session.close() + json = {'solves':[]} + for x in solves: + json['solves'].append({'id':x.id, 'chal':x.chal.name, 'chalid':x.chalid,'team':x.teamid, 'value': x.chal.value, 'category':x.chal.category, 'time':unix_time(x.date)}) + return jsonify(json) + + @app.route('/admin/statistics', methods=['GET']) + @admins_only + def admin_stats(): + db.session.commit() + + teams_registered = db.session.query(db.func.count(Teams.id)).first()[0] + site_hits = db.session.query(db.func.count(Tracking.id)).first()[0] + wrong_count = db.session.query(db.func.count(WrongKeys.id)).first()[0] + solve_count = db.session.query(db.func.count(Solves.id)).first()[0] + challenge_count = db.session.query(db.func.count(Challenges.id)).first()[0] + most_solved_chal = Solves.query.add_columns(db.func.count(Solves.chalid).label('solves')).group_by(Solves.chalid).order_by('solves DESC').first() + least_solved_chal = Solves.query.add_columns(db.func.count(Solves.chalid).label('solves')).group_by(Solves.chalid).order_by('solves ASC').first() + + db.session.close() + + return render_template('admin/statistics.html', team_count=teams_registered, + hit_count=site_hits, + wrong_count=wrong_count, + solve_count=solve_count, + challenge_count=challenge_count, + most_solved=most_solved_chal, + least_solved = least_solved_chal + ) + + + + @app.route('/admin/fails/', methods=['GET']) + @admins_only + def admin_fails(teamid='all'): + if teamid == "all": + fails = WrongKeys.query.count() + solves = Solves.query.count() + db.session.close() + json = {'fails':str(fails), 'solves': str(solves)} + return jsonify(json) + else: + fails = WrongKeys.query.filter_by(team=teamid).count() + solves = Solves.query.filter_by(teamid=teamid).count() + db.session.close() + json = {'fails':str(fails), 'solves': str(solves)} + return jsonify(json) + + + + @app.route('/admin/chal/new', methods=['POST']) + def admin_create_chal(): + + files = request.files.getlist('files[]') + + # Create challenge + chal = Challenges(request.form['name'], request.form['desc'], request.form['value'], request.form['category']) + db.session.add(chal) + db.session.commit() + + # Add keys + key = Keys(chal.id, request.form['key'], request.form['key_type[0]']) + db.session.add(key) + db.session.commit() + + for f in files: + filename = secure_filename(f.filename) + + if len(filename) <= 0: + continue + + md5hash = hashlib.md5(filename).hexdigest() + + if not os.path.exists(os.path.join(app.config['UPLOAD_FOLDER'], md5hash)): + os.makedirs(os.path.join(app.config['UPLOAD_FOLDER'], md5hash)) + + f.save(os.path.join(app.config['UPLOAD_FOLDER'], md5hash, filename)) + db_f = Files(chal.id, os.path.join(app.config['UPLOAD_FOLDER'], md5hash, filename)) + db.session.add(db_f) + + db.session.commit() + db.session.close() + return redirect('/admin/chals') + + @app.route('/admin/chal/update', methods=['POST']) + def admin_update_chal(): + challenge=Challenges.query.filter_by(id=request.form['id']).first() + challenge.name = request.form['name'] + challenge.description = request.form['desc'] + challenge.value = request.form['value'] + challenge.category = request.form['category'] + db.session.add(challenge) + db.session.commit() + db.session.close() + return redirect('/admin/chals') diff --git a/CTFd/auth.py b/CTFd/auth.py new file mode 100644 index 0000000..238c5bf --- /dev/null +++ b/CTFd/auth.py @@ -0,0 +1,129 @@ +from CTFd import render_template, request, redirect, abort, jsonify, url_for, session +from CTFd.utils import sha512, is_safe_url, authed, mailserver +from CTFd.models import db, Teams + +from itsdangerous import TimedSerializer, BadTimeSignature +from passlib.hash import bcrypt_sha256 +from flask import current_app as app + +import logging +import time +import re +import os + +def init_auth(app): + @app.context_processor + def inject_user(): + if authed(): + return dict(session) + return dict() + + @app.route('/reset_password', methods=['POST', 'GET']) + @app.route('/reset_password/', methods=['POST', 'GET']) + def reset_password(data=None): + if data is not None and request.method == "GET": + return render_template('reset_password.html', mode='set') + if data is not None and request.method == "POST": + try: + s = TimedSerializer(app.config['SECRET_KEY']) + name = s.loads(data.decode('base64'), max_age=1800) + except BadTimeSignature: + return render_template('reset_password.html', errors=['Your link has expired']) + team = Teams.query.filter_by(name=name).first() + team.password = sha512(request.form['password'].strip()) + db.session.commit() + db.session.close() + return redirect('/login') + + if request.method == 'POST': + email = request.form['email'].strip() + team = Teams.query.filter_by(email=email).first() + if not team: + return render_template('reset_password.html', errors=['Check your email']) + s = TimedSerializer(app.config['SECRET_KEY']) + token = s.dumps(team.name) + text = """ +Did you initiate a password reset? + +{0}/reset_password/{1} + + """.format(app.config['HOST'], token.encode('base64')) + + sendmail(email, text) + + return render_template('reset_password.html', errors=['Check your email']) + return render_template('reset_password.html') + + @app.route('/register', methods=['POST', 'GET']) + def register(): + if request.method == 'POST': + errors = [] + name_len = len(request.form['name']) == 0 + names = Teams.query.add_columns('name', 'id').filter_by(name=request.form['name']).first() + emails = Teams.query.add_columns('email', 'id').filter_by(email=request.form['email']).first() + pass_len = len(request.form['password']) == 0 + valid_email = re.match("[^@]+@[^@]+\.[^@]+", request.form['email']) + + if not valid_email: + errors.append("That email doesn't look right") + if names: + errors.append('That team name is already taken') + if emails: + errors.append('That email has already been used') + if pass_len: + errors.append('Pick a longer password') + if name_len: + errors.append('Pick a longer team name') + + if not errors: + with app.app_context(): + team = Teams(request.form['name'], request.form['email'], request.form['password']) + db.session.add(team) + db.session.commit() + if mailserver(): + sendmail(request.form['email'], "You've successfully registered for the CTF") + + db.session.close() + if len(errors) > 0: + return render_template('register.html', errors=errors, name=request.form['name'], email=request.form['email'], password=request.form['password']) + + logger = logging.getLogger('regs') + logger.warn("[{0}] {1} registered with {2}".format(time.strftime("%m/%d/%Y %X"), request.form['name'], request.form['email'])) + return redirect('/login') + else: + return render_template('register.html') + + @app.route('/login', methods=['POST', 'GET']) + def login(): + if request.method == 'POST': + errors = [] + # team = Teams.query.filter_by(name=request.form['name'], password=sha512(request.form['password'])).first() + team = Teams.query.filter_by(name=request.form['name']).first() + if team and bcrypt_sha256.verify(request.form['password'], team.password): + # session.regenerate() # NO SESSION FIXATION FOR YOU + session['username'] = team.name + session['id'] = team.id + session['admin'] = team.admin + session['nonce'] = sha512(os.urandom(10)) + db.session.close() + + logger = logging.getLogger('logins') + logger.warn("[{0}] {1} logged in".format(time.strftime("%m/%d/%Y %X"), session['username'])) + + # if request.args.get('next') and is_safe_url(request.args.get('next')): + # return redirect(request.args.get('next')) + return redirect('/team/{0}'.format(team.id)) + else: + errors.append("That account doesn't seem to exist") + db.session.close() + return render_template('login.html', errors=errors) + else: + db.session.close() + return render_template('login.html') + + + @app.route('/logout') + def logout(): + if authed(): + session.clear() + return redirect('/') diff --git a/CTFd/challenges.py b/CTFd/challenges.py new file mode 100644 index 0000000..9e65ca3 --- /dev/null +++ b/CTFd/challenges.py @@ -0,0 +1,127 @@ +from flask import current_app as app, render_template, request, redirect, abort, jsonify, json as json_mod, url_for + +from CTFd import session, logging +from CTFd.utils import ctftime, authed, unix_time, get_kpm +from CTFd.models import db, Challenges, Files, Solves, WrongKeys, Keys + +import time + +def init_challenges(app): + @app.route('/challenges', methods=['GET']) + def challenges(): + if not ctftime(): + return redirect('/') + if authed(): + return render_template('chals.html') + else: + return redirect(url_for('login', next="challenges")) + + @app.route('/chals', methods=['GET']) + def chals(): + if not ctftime(): + return redirect('/') + if authed(): + chals = Challenges.query.add_columns('id', 'name', 'value', 'description', 'category').order_by(Challenges.value).all() + + json = {'game':[]} + for x in chals: + files = [ str(f.location) for f in Files.query.filter_by(chal=x.id).all() ] + json['game'].append({'id':x[1], 'name':x[2], 'value':x[3], 'description':x[4], 'category':x[5], 'files':files}) + + db.session.close() + return jsonify(json) + else: + db.session.close() + return redirect('/login') + + @app.route('/chals/solves') + def chals_per_solves(): + if authed(): + solves = Solves.query.add_columns(db.func.count(Solves.chalid)).group_by(Solves.chalid).all() + json = {} + for chal, count in solves: + json[chal.chal.name] = count + return jsonify(json) + return redirect(url_for('login', next="/chals/solves")) + + @app.route('/solves') + @app.route('/solves/') + def solves(teamid=None): + if teamid is None: + if authed(): + solves = Solves.query.filter_by(teamid=session['id']).all() + else: + return redirect('/login') + else: + solves = Solves.query.filter_by(teamid=teamid).all() + db.session.close() + json = {'solves':[]} + for x in solves: + json['solves'].append({'id':x.id, 'chal':x.chal.name, 'chalid':x.chalid,'team':x.teamid, 'value': x.chal.value, 'category':x.chal.category, 'time':unix_time(x.date)}) + return jsonify(json) + + @app.route('/fails/', methods=['GET']) + def fails(teamid): + fails = WrongKeys.query.filter_by(team=teamid).count() + solves = Solves.query.filter_by(teamid=teamid).count() + db.session.close() + json = {'fails':str(fails), 'solves': str(solves)} + return jsonify(json) + + @app.route('/chal//solves', methods=['GET']) + def who_solved(chalid): + solves = Solves.query.filter_by(chalid=chalid) + json = {'teams':[]} + for solve in solves: + json['teams'].append({'id':solve.team.id, 'name':solve.team.name, 'date':solve.date}) + return jsonify(json) + + @app.route('/chal/', methods=['POST']) + def chal(chalid): + if not ctftime(): + return redirect('/') + if authed(): + logger = logging.getLogger('keys') + data = (time.strftime("%m/%d/%Y %X"), session['username'], request.form['key'], get_kpm(session['id'])) + print "[{0}] {1} submitted {2} with kpm {3}".format(*data) + if get_kpm(session['id']) > 10: + wrong = WrongKeys(session['id'], chalid, request.form['key']) + db.session.add(wrong) + db.session.commit() + db.session.close() + logger.warn("[{0}] {1} submitted {2} with kpm {3} [TOO FAST]".format(*data)) + return "3" # Submitting too fast + solves = Solves.query.filter_by(teamid=session['id'], chalid=chalid).first() + if not solves: + keys = Keys.query.filter_by(chal=chalid).all() + key = request.form['key'].strip().lower() + for x in keys: + if x.key_type == 0: #static key + if x.flag.strip().lower() == key: + solve = Solves(chalid=chalid, teamid=session['id'], ip=request.remote_addr) + db.session.add(solve) + db.session.commit() + db.session.close() + logger.info("[{0}] {1} submitted {2} with kpm {3} [CORRECT]".format(*data)) + return "1" # key was correct + elif x.key_type == 1: #regex + res = re.match(str(x), key, re.IGNORECASE) + if res and res.group() == key: + solve = Solves(chalid=chalid, teamid=session['id'], ip=request.remote_addr) + db.session.add(solve) + db.session.commit() + db.session.close() + logger.info("[{0}] {1} submitted {2} with kpm {3} [CORRECT]".format(*data)) + return "1" # key was correct + + wrong = WrongKeys(session['id'], chalid, request.form['key']) + db.session.add(wrong) + db.session.commit() + db.session.close() + logger.info("[{0}] {1} submitted {2} with kpm {3} [WRONG]".format(*data)) + return '0' # key was wrong + else: + logger.info("{0} submitted {1} with kpm {2} [ALREADY SOLVED]".format(*data)) + return "2" # challenge was already solved + else: + return "-1" diff --git a/CTFd/config.py b/CTFd/config.py new file mode 100644 index 0000000..3267256 --- /dev/null +++ b/CTFd/config.py @@ -0,0 +1,19 @@ +import os +##### SERVER SETTINGS ##### +SECRET_KEY = os.urandom(64) +SQLALCHEMY_DATABASE_URI = 'sqlite:///ctfd.db' +SESSION_TYPE = "filesystem" +SESSION_FILE_DIR = "/tmp/flask_session" +SESSION_COOKIE_HTTPONLY = True +HOST = ".ctfd.io" +UPLOAD_FOLDER = 'static/uploads' + +##### EMAIL ##### +CTF_NAME = '' +MAIL_SERVER = '' +MAIL_PORT = 0 +MAIL_USE_TLS = False +MAIL_USE_SSL = False +MAIL_USERNAME = '' +MAIL_PASSWORD = '' +ADMINS = [] diff --git a/CTFd/errors.py b/CTFd/errors.py new file mode 100644 index 0000000..877fdff --- /dev/null +++ b/CTFd/errors.py @@ -0,0 +1,14 @@ +from flask import current_app as app, render_template + +def init_errors(app): + @app.errorhandler(404) + def page_not_found(error): + return render_template('errors/404.html'), 404 + + @app.errorhandler(403) + def forbidden(error): + return render_template('errors/403.html'), 403 + + @app.errorhandler(500) + def forbidden(error): + return render_template('errors/500.html'), 500 diff --git a/CTFd/models.py b/CTFd/models.py new file mode 100644 index 0000000..fb0484a --- /dev/null +++ b/CTFd/models.py @@ -0,0 +1,169 @@ +#from CTFd import db +from flask.ext.sqlalchemy import SQLAlchemy + +from socket import inet_aton, inet_ntoa +from struct import unpack, pack +from passlib.hash import bcrypt_sha256 + +import datetime +import hashlib + +def sha512(string): + return hashlib.sha512(string).hexdigest() + +def ip2long(ip): + return unpack('!I', inet_aton(ip))[0] + +def long2ip(ip_int): + return inet_ntoa(pack('!I', ip_int)) + +db = SQLAlchemy() + + +class Pages(db.Model): + id = db.Column(db.Integer, primary_key=True) + route = db.Column(db.String(80), unique=True) + html = db.Column(db.Text) + + def __init__(self, route, html): + self.route = route + self.html = html + + def __repr__(self): + return "".format(self.tag, self.chal) + +class Challenges(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(80)) + description = db.Column(db.Text) + value = db.Column(db.Integer) + category = db.Column(db.String(80)) + # add open category + + def __init__(self, name, description, value, category): + self.name = name + self.description = description + self.value = value + self.category = category + + def __repr__(self): + return '' % self.name + +class Tags(db.Model): + id = db.Column(db.Integer, primary_key=True) + chal = db.Column(db.Integer, db.ForeignKey('challenges.id')) + tag = db.Column(db.String(80)) + + def __init__(self, chal, tag): + self.chal = chal + self.tag = tag + + def __repr__(self): + return "".format(self.tag, self.chal) + +class Files(db.Model): + id = db.Column(db.Integer, primary_key=True) + chal = db.Column(db.Integer, db.ForeignKey('challenges.id')) + location = db.Column(db.Text) + + def __init__(self, chal, location): + self.chal = chal + self.location = location + + def __repr__(self): + return "".format(self.location, self.chal) + +class Keys(db.Model): + id = db.Column(db.Integer, primary_key=True) + chal = db.Column(db.Integer, db.ForeignKey('challenges.id')) + key_type = db.Column(db.Integer) + flag = db.Column(db.Text) + + + def __init__(self, chal, flag, key_type): + self.chal = chal + self.flag = flag + self.key_type = key_type + + def __repr__(self): + return self.flag + + +class Teams(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(128), unique=True) + email = db.Column(db.String(128), unique=True) + password = db.Column(db.String(128)) + website = db.Column(db.String(128)) + affiliation = db.Column(db.String(128)) + country = db.Column(db.String(32)) + bracket = db.Column(db.String(32)) + banned = db.Column(db.Boolean) + admin = db.Column(db.Boolean) + + def __init__(self, name, email, password): + self.name = name + self.email = email + self.password = bcrypt_sha256.encrypt(password) + + def __repr__(self): + return '' % self.name + +class Solves(db.Model): + __table_args__ = (db.UniqueConstraint('chalid', 'teamid'), {}) + id = db.Column(db.Integer, primary_key=True) + chalid = db.Column(db.Integer, db.ForeignKey('challenges.id')) + teamid = db.Column(db.Integer, db.ForeignKey('teams.id')) + ip = db.Column(db.Integer) + date = db.Column(db.DateTime, default=datetime.datetime.utcnow) + team = db.relationship('Teams', foreign_keys="Solves.teamid", lazy='joined') + chal = db.relationship('Challenges', foreign_keys="Solves.chalid", lazy='joined') + # value = db.Column(db.Integer) + + def __init__(self, chalid, teamid, ip): + self.ip = ip2long(ip) + self.chalid = chalid + self.teamid = teamid + # self.value = value + + def __repr__(self): + return '' % self.chal + + +class WrongKeys(db.Model): + id = db.Column(db.Integer, primary_key=True) + chal = db.Column(db.Integer, db.ForeignKey('challenges.id')) + team = db.Column(db.Integer, db.ForeignKey('teams.id')) + date = db.Column(db.DateTime, default=datetime.datetime.utcnow) + flag = db.Column(db.Text) + + def __init__(self, team, chal, flag): + self.team = team + self.chal = chal + self.flag = flag + + def __repr__(self): + return '' % self.flag + + +class Tracking(db.Model): + id = db.Column(db.Integer, primary_key=True) + ip = db.Column(db.BigInteger) + team = db.Column(db.Integer, db.ForeignKey('teams.id')) + date = db.Column(db.DateTime, default=datetime.datetime.utcnow) + + def __init__(self, ip, team): + self.ip = ip2long(ip) + self.team = team + + def __repr__(self): + return '' % self.team + +class Config(db.Model): + id = db.Column(db.Integer, primary_key=True) + key = db.Column(db.Text) + value = db.Column(db.Text) + + def __init__(self, key, value): + self.key = key + self.value = value diff --git a/CTFd/scoreboard.py b/CTFd/scoreboard.py new file mode 100644 index 0000000..92b5fc3 --- /dev/null +++ b/CTFd/scoreboard.py @@ -0,0 +1,50 @@ +from flask import current_app as app +from CTFd import session, render_template, jsonify +from CTFd.utils import unix_time +from CTFd.models import db, Teams, Solves, Challenges + +def init_scoreboard(app): + @app.route('/scoreboard') + def scoreboard(): + score = db.func.sum(Challenges.value).label('score') + quickest = db.func.max(Solves.date).label('quickest') + teams = db.session.query(Solves.teamid, Teams.name, score).join(Teams).join(Challenges).filter(Teams.banned == None).group_by(Solves.teamid).order_by(score.desc(), quickest) + #teams = db.engine.execute("SELECT solves.teamid, teams.id, teams.name, SUM(value) as score, MAX(solves.date) as quickest FROM solves JOIN teams ON solves.teamid=teams.id INNER JOIN challenges ON solves.chalid=challenges.id WHERE teams.banned IS NULL GROUP BY solves.teamid ORDER BY score DESC, quickest ASC;") + db.session.close() + return render_template('scoreboard.html', teams=teams) + + @app.route('/scores') + def scores(): + #teams = db.engine.execute("SELECT solves.teamid, teams.id, teams.name, SUM(value) as score, MAX(solves.date) as quickest FROM solves JOIN teams ON solves.teamid=teams.id INNER JOIN challenges ON solves.chalid=challenges.id WHERE teams.banned IS NULL GROUP BY solves.teamid ORDER BY score DESC, quickest ASC;") + score = db.func.sum(Challenges.value).label('score') + quickest = db.func.max(Solves.date).label('quickest') + teams = db.session.query(Solves.teamid, Teams.name, score).join(Teams).join(Challenges).filter(Teams.banned == None).group_by(Solves.teamid).order_by(score.desc(), quickest) + db.session.close() + json = {'teams':[]} + for i, x in enumerate(teams): + json['teams'].append({'place':i+1, 'id':x.teamid, 'name':x.name,'score':int(x.score)}) + return jsonify(json) + + @app.route('/top/') + def topteams(count): + try: + count = int(count) + except: + count = 10 + if count > 20 or count < 0: + count = 10 + + json = {'scores':{}} + + #teams = db.engine.execute("SELECT solves.teamid, teams.id, teams.name, SUM(value) as score, MAX(solves.date) as quickest FROM solves JOIN teams ON solves.teamid=teams.id INNER JOIN challenges ON solves.chalid=challenges.id WHERE teams.banned IS NULL GROUP BY solves.teamid ORDER BY score DESC, quickest ASC LIMIT {0};".format(count)) + score = db.func.sum(Challenges.value).label('score') + teams = db.session.query(Solves.teamid, Teams.name, score, db.func.max(Solves.date).label('quickest')).join(Teams).join(Challenges).filter(Teams.banned == None).group_by(Solves.teamid).order_by(score.desc(), Solves.date).limit(count) + + + for team in teams: + solves = Solves.query.filter_by(teamid=team.teamid).all() + json['scores'][team.name] = [] + for x in solves: + json['scores'][team.name].append({'id':x.teamid, 'chal':x.chalid, 'team':x.teamid, 'value': x.chal.value, 'time':unix_time(x.date)}) + + return jsonify(json) diff --git a/CTFd/utils.py b/CTFd/utils.py new file mode 100644 index 0000000..1844981 --- /dev/null +++ b/CTFd/utils.py @@ -0,0 +1,124 @@ +from CTFd import session +from CTFd.models import db, WrongKeys, Pages, Config + +from urlparse import urlparse, urljoin +from functools import wraps +from flask import current_app as app, g, request, redirect, url_for +from socket import inet_aton, inet_ntoa +from struct import unpack, pack + +import time +import datetime +import hashlib +import json + +def init_utils(app): + app.jinja_env.filters['unix_time'] = unix_time + app.jinja_env.filters['unix_time_millis'] = unix_time_millis + app.jinja_env.filters['long2ip'] = long2ip + app.jinja_env.globals.update(pages=pages) + +def pages(): + pages = Pages.query.filter(Pages.route!="index").all() + return pages + +def authed(): + try: + if session['id']: + return True + except KeyError: + pass + return False + +def is_setup(): + setup = Config.query.filter_by(key='setup').first() + if setup: + return setup.value + else: + return False + +def is_admin(): + if authed(): + return session['admin'] + else: + return False + +def admins_only(f): + @wraps(f) + def decorated_function(*args, **kwargs): + if session.get('admin', None) is None: + return redirect(url_for('login', next=request.url)) + return f(*args, **kwargs) + return decorated_function + + +def ctftime(): + """ Checks whether it's CTF time or not. """ + + start = Config.query.filter_by(key="start").first().value + end = Config.query.filter_by(key="end").first().value + + if start: + start = int(start) + if end: + end = int(end) + + if start and end: + if start < time.time() and time.time() < end: + # Within the two time bounds + return True + + if start < time.time() and end is None: + # CTF starts on a date but never ends + return True + + if start is None and time.time() < end: + # CTF started but ends at a date + return True + + if start is None and end is None: + # CTF has no time requirements + return True + + return False + +def unix_time(dt): + epoch = datetime.datetime.utcfromtimestamp(0) + delta = dt - epoch + return int(delta.total_seconds()) + +def unix_time_millis(dt): + return unix_time(dt) * 1000 + +def long2ip(ip_int): + return inet_ntoa(pack('!I', ip_int)) + +def ip2long(ip): + return unpack('!I', inet_aton(ip))[0] + +def get_kpm(teamid): # keys per minute + one_min_ago = datetime.datetime.utcnow() + datetime.timedelta(minutes=-1) + return len(db.session.query(WrongKeys).filter(WrongKeys.team == teamid, WrongKeys.date >= one_min_ago).all()) + + +def mailserver(): + if app.config['MAIL_SERVER'] and app.config['MAIL_PORT'] and app.config['ADMINS']: + return True + return False + +def sendmail(addr, text): + try: + msg = Message("Message from {0}".format(app.config['CTF_NAME']), sender = app.config['ADMINS'][0], recipients = [addr]) + msg.body = text + mail.send(msg) + return True + except: + return False + +def is_safe_url(target): + ref_url = urlparse(request.host_url) + test_url = urlparse(urljoin(request.host_url, target)) + return test_url.scheme in ('http', 'https') and ref_url.netloc == test_url.netloc + +def sha512(string): + return hashlib.sha512(string).hexdigest() diff --git a/CTFd/views.py b/CTFd/views.py new file mode 100644 index 0000000..c4693eb --- /dev/null +++ b/CTFd/views.py @@ -0,0 +1,167 @@ +from flask import current_app as app, render_template, render_template_string, request, redirect, abort, jsonify, json as json_mod, url_for +from CTFd import session +from CTFd.utils import authed, ip2long, long2ip, is_setup +from CTFd.models import db, Teams, Solves, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config + +from jinja2.exceptions import TemplateNotFound + +from collections import OrderedDict +import logging +import os +import re +import sys +import json +import os + +def init_views(app): + @app.before_request + def tracker(): + if authed(): + if not Tracking.query.filter_by(ip=ip2long(request.remote_addr)).first(): + visit = Tracking(request.remote_addr, session['id']) + db.session.add(visit) + db.session.commit() + db.session.close() + + @app.before_request + def csrf(): + if authed() and request.method == "POST": + if session['nonce'] != request.form.get('nonce'): + abort(403) + + @app.before_request + def redirect_setup(): + if request.path == "/static/css/style.css": + return + if not is_setup() and request.path != "/setup": + return redirect('/setup') + + @app.before_first_request + def needs_setup(): + if not is_setup(): + return redirect('/setup') + + @app.route('/setup', methods=['GET', 'POST']) + def setup(): + # with app.app_context(): + # admin = Teams.query.filter_by(admin=True).first() + + if not is_setup(): + if request.method == 'POST': + ## Admin user + name = request.form['name'] + email = request.form['email'] + password = request.form['password'] + admin = Teams(name, email, password) + admin.admin = True + + ## Index page + html = request.form['html'] + page = Pages('index', html) + + ## Start time + start = Config('start', None) + end = Config('end', None) + + setup = Config('setup', True) + + db.session.add(admin) + db.session.add(page) + db.session.add(start) + db.session.add(end) + db.session.add(setup) + db.session.commit() + app.setup = False + return redirect('/') + return render_template('setup.html') + return redirect('/') + + # Static HTML files + @app.route("/", defaults={'template': 'index'}) + @app.route("/