From 4bc9a9cc2cecba0fa3866e578a31d1f971f32368 Mon Sep 17 00:00:00 2001 From: Sebastian Gehaxelt Date: Tue, 23 Feb 2016 20:30:30 +0100 Subject: [PATCH 01/10] Added .gitignore and moved config.json to config.json.sample --- .gitignore | 93 ++++++++++++++++++++++++++++++- config.json => config.json.sample | 4 +- 2 files changed, 95 insertions(+), 2 deletions(-) rename config.json => config.json.sample (70%) diff --git a/.gitignore b/.gitignore index 17eba50..08b4c9b 100755 --- a/.gitignore +++ b/.gitignore @@ -1 +1,92 @@ -ctf.db \ No newline at end of file +ctf.db +config.json + +# Created by https://www.gitignore.io/api/python,virtualenv + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask instance folder +instance/ + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# IPython Notebook +.ipynb_checkpoints + +# pyenv +.python-version + + + +### VirtualEnv ### +# Virtualenv +# http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ +.Python +[Bb]in +[Ii]nclude +[Ll]ib +[Ll]ib64 +[Ll]ocal +[Ss]cripts +pyvenv.cfg +.venv +pip-selfcheck.json + diff --git a/config.json b/config.json.sample similarity index 70% rename from config.json rename to config.json.sample index 18bafec..6ad6b66 100755 --- a/config.json +++ b/config.json.sample @@ -9,5 +9,7 @@ "language_file": "lang.json", "language": "english", - "debug": false + "username_regex": "^[a-zA-Z0-9-_. &']+$", + + "debug": true } \ No newline at end of file From d9552e985c3d8baf2108d40f503c1a74534ab984 Mon Sep 17 00:00:00 2001 From: Sebastian Gehaxelt Date: Tue, 23 Feb 2016 20:31:12 +0100 Subject: [PATCH 02/10] Added virtualenv requirements --- requirements.txt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..28695e7 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,14 @@ +alembic==0.8.4 +dataset==0.6.2 +Flask==0.10.1 +itsdangerous==0.24 +Jinja2==2.8 +Mako==1.0.3 +MarkupSafe==0.23 +normality==0.2.4 +python-editor==0.5 +PyYAML==3.11 +six==1.10.0 +SQLAlchemy==1.0.12 +Werkzeug==0.11.4 +wheel==0.29.0 From 0c599cf7c3d78ed3534e1554932dd74e3f4310e0 Mon Sep 17 00:00:00 2001 From: Sebastian Gehaxelt Date: Tue, 23 Feb 2016 20:31:35 +0100 Subject: [PATCH 03/10] Added username validation --- lang.json | 3 ++- server.py | 16 +++++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/lang.json b/lang.json index 53952cd..e4c3f5e 100755 --- a/lang.json +++ b/lang.json @@ -44,7 +44,8 @@ "invalid_credentials": "Invalid username or password", "already_registered": "This user is already registered", "empty_user": "Empty username is not allowed", - "task_not_found": "TBD" + "task_not_found": "TBD", + "invalid_user": "Invalid username" } } } \ No newline at end of file diff --git a/server.py b/server.py index 6c19389..edae26f 100755 --- a/server.py +++ b/server.py @@ -6,6 +6,7 @@ import json import random import time +import re from base64 import b64decode from functools import wraps @@ -24,6 +25,13 @@ db = None lang = None config = None +username_regex = None + +def is_valid_username(u): + """Ensures that the username matches username_regex""" + if(username_regex.match(u)): + return True + return False def login_required(f): """Ensures that an user is logged in""" @@ -65,7 +73,7 @@ def get_flags(): def error(msg): """Displays an error message""" - if msg in lang: + if msg in lang['error']: message = lang['error'][msg] else: message = lang['error']['unknown'] @@ -121,6 +129,9 @@ def register_submit(): if not username: return redirect('/error/empty_user') + if not is_valid_username(username): + return redirect('/error/invalid_user') + user_found = db['users'].find_one(username=username) if user_found: return redirect('/error/already_registered') @@ -293,6 +304,9 @@ def index(): # Connect to database db = dataset.connect(config['db']) + # Compile regex for usernames + username_regex = re.compile(config['username_regex']) + # Setup the flags table at first execution if 'flags' not in db.tables: db.query('''create table flags ( From 32ea75312f3a77ce517111ff02bc921563e5a916 Mon Sep 17 00:00:00 2001 From: Sebastian Gehaxelt Date: Tue, 23 Feb 2016 21:21:09 +0100 Subject: [PATCH 04/10] Implemented CSRF protection --- server.py | 31 +++++++++++++++++++++++++++---- static/js/submit.js | 30 ++++++++++++++++++++---------- templates/frame.html | 2 ++ templates/register.html | 1 + templates/task.html | 2 ++ 5 files changed, 52 insertions(+), 14 deletions(-) diff --git a/server.py b/server.py index edae26f..4362ce6 100755 --- a/server.py +++ b/server.py @@ -7,6 +7,8 @@ import random import time import re +import hashlib +import os from base64 import b64decode from functools import wraps @@ -19,6 +21,7 @@ from flask import request from flask import session from flask import url_for +from flask import abort app = Flask(__name__, static_folder='static', static_url_path='') @@ -209,12 +212,14 @@ def task(category, score): user=user, category=category, task=task, score=score) return make_response(render) -@app.route('/submit///') +@app.route('/task/submit', methods = ['POST']) @login_required -def submit(category, score, flag): +def submit(): """Handles the submission of flags""" - print "ok" + category = request.form['category'] + score = request.form['score'] + flag = request.form['flag'] login, user = get_user() @@ -222,7 +227,8 @@ def submit(category, score, flag): flags = get_flags() task_done = task['id'] in flags - result = {'success': False} + result = {'success': False, 'csrf': generate_csrf_token() } + if not task_done and task['flag'] == b64decode(flag): timestamp = int(time.time() * 1000) @@ -274,6 +280,21 @@ def logout(): del session['user_id'] return redirect('/') +@app.before_request +def csrf_protect(): + if request.method == "POST": + token = session.pop('_csrf_token', None) + if not token or token != request.form.get('_csrf_token'): + abort(400) + +def some_random_string(): + return hashlib.sha256(os.urandom(16)).hexdigest() + +def generate_csrf_token(): + if '_csrf_token' not in session: + session['_csrf_token'] = some_random_string() + return session['_csrf_token'] + @app.route('/') def index(): """Displays the main page""" @@ -316,6 +337,8 @@ def index(): timestamp BIGINT, PRIMARY KEY (task_id, user_id))''') + app.jinja_env.globals['csrf_token'] = generate_csrf_token + # Start web server app.run(host=config['host'], port=config['port'], debug=config['debug'], threaded=True) diff --git a/static/js/submit.js b/static/js/submit.js index 93c6047..19aaee8 100755 --- a/static/js/submit.js +++ b/static/js/submit.js @@ -1,24 +1,34 @@ $("#flag-submission").click(function() { + submit(); +}); +$(document).keypress(function( event ) { + if(event.which == 13) { + event.preventDefault(); + submit(); + } +}); +function submit() { var cat = $(".task-box").data("category"); var score = $(".task-box").data("score"); var flag = $("#flag-input").val(); - - console.log("/submit/" + cat + "/" + score + "/" + btoa(flag)); + var csrf = $("#_csrf_token").val(); $.ajax({ - url: "/submit/" + cat + "/" + score + "/" + btoa(flag) + url: "/task/submit", + method: "POST", + data: {"category": cat, "score":score, "flag": btoa(flag), "_csrf_token": csrf} }).done(function(data) { - - console.log(data); - - if (data["success"]) { - $("#flag-input").val($(".lang").data("success")); + $("#_csrf_token").val(data['csrf']); + $("#flag-output").fadeIn(900); + if (data["success"]) { + $("#flag-output").html($(".lang").data("success")); $("#flag-submission").removeClass("btn-primary"); $("#flag-submission").addClass("btn-success"); $("#flag-submission").attr('disabled','disabled'); } else { - $("#flag-input").val($(".lang").data("failure")); + $("#flag-output").html($(".lang").data("failure")); } + $("#flag-output").fadeOut(1000); }); -}); \ No newline at end of file +} diff --git a/templates/frame.html b/templates/frame.html index 8b0cc59..7db58fc 100755 --- a/templates/frame.html +++ b/templates/frame.html @@ -25,6 +25,7 @@ {% if not login %} diff --git a/templates/register.html b/templates/register.html index ce87eda..06934e0 100755 --- a/templates/register.html +++ b/templates/register.html @@ -2,6 +2,7 @@
+

Register


diff --git a/templates/task.html b/templates/task.html index a8de2bb..96e9990 100755 --- a/templates/task.html +++ b/templates/task.html @@ -19,6 +19,8 @@
({{ category }}{{ score }}, {{ task.file }}

+

+ From f3dde227d8c1a422350c34aa33cfd17075760a22 Mon Sep 17 00:00:00 2001 From: Sebastian Gehaxelt Date: Tue, 23 Feb 2016 21:54:02 +0100 Subject: [PATCH 05/10] Added JSON scoreboard --- server.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/server.py b/server.py index 4362ce6..59e8528 100755 --- a/server.py +++ b/server.py @@ -260,6 +260,19 @@ def scoreboard(): login=login, user=user, scores=scores) return make_response(render) +@app.route("/scoreboard.json") +def scoreboard_json(): + """Displays data for ctftime.org in json format""" + scores = db.query('''select u.username as team, ifnull(sum(f.score), 0) as score, ifnull(max(timestamp), 0) as lastAccept from users + u left join flags f on u.id = f.user_id where u.hidden = 0 group by u.username + order by score desc, lastAccept asc''') + scores = list(scores) + for i, s in enumerate(scores): + s['pos'] = i + 1 + + data = map(dict,scores) + return jsonify({'standings':data}) + @app.route('/about') @login_required def about(): From ad34963c627eebb31bbfa958d0a19b3e7269521b Mon Sep 17 00:00:00 2001 From: Sebastian Gehaxelt Date: Tue, 23 Feb 2016 23:10:53 +0100 Subject: [PATCH 06/10] Implemented before_end and after_start annotations --- config.json.sample | 3 +++ lang.json | 4 +++- server.py | 44 +++++++++++++++++++++++++++++++++++++++++--- templates/frame.html | 1 + 4 files changed, 48 insertions(+), 4 deletions(-) diff --git a/config.json.sample b/config.json.sample index 6ad6b66..baff9f1 100755 --- a/config.json.sample +++ b/config.json.sample @@ -11,5 +11,8 @@ "username_regex": "^[a-zA-Z0-9-_. &']+$", + "start":0, + "end":0, + "debug": true } \ No newline at end of file diff --git a/lang.json b/lang.json index e4c3f5e..035dc4c 100755 --- a/lang.json +++ b/lang.json @@ -45,7 +45,9 @@ "already_registered": "This user is already registered", "empty_user": "Empty username is not allowed", "task_not_found": "TBD", - "invalid_user": "Invalid username" + "invalid_user": "Invalid username", + "ctf_not_started": "Wait until the CTF starts", + "ctf_over": "CTF is over" } } } \ No newline at end of file diff --git a/server.py b/server.py index 59e8528..74ebc3f 100755 --- a/server.py +++ b/server.py @@ -29,6 +29,8 @@ lang = None config = None username_regex = None +start = None +end = None def is_valid_username(u): """Ensures that the username matches username_regex""" @@ -46,6 +48,35 @@ def decorated_function(*args, **kwargs): return f(*args, **kwargs) return decorated_function +def before_end(f): + """Ensures that actions can only be done before the CTF is over""" + + @wraps(f) + def decorated_function(*args, **kwargs): + cur_time = int(time.time()) + if cur_time >= end: + return redirect(url_for('error', msg='ctf_over')) + return f(*args, **kwargs) + return decorated_function + +def after_start(f): + """Ensures that actions can only be done after the CTF has started""" + + @wraps(f) + def decorated_function(*args, **kwargs): + cur_time = int(time.time()) + if cur_time < start: + return redirect(url_for('error', msg='ctf_not_started')) + return f(*args, **kwargs) + return decorated_function + +@app.context_processor +def inject_ctftime(): + st = time.strftime("%Y-%m-%d, %H:%M:%S (%Z)",time.localtime(start)) + ed = time.strftime("%Y-%m-%d, %H:%M:%S (%Z)",time.localtime(end)) + return dict(ctf_start=st, ctf_stop=ed) + + def get_user(): """Looks up the current user in the database""" @@ -112,6 +143,7 @@ def login(): return redirect('/error/invalid_credentials') @app.route('/register') +@before_end def register(): """Displays the register form""" @@ -121,6 +153,7 @@ def register(): return make_response(render) @app.route('/register/submit', methods = ['POST']) +@before_end def register_submit(): """Attempts to register a new user""" @@ -150,6 +183,7 @@ def register_submit(): @app.route('/tasks') @login_required +@after_start def tasks(): """Displays all the tasks in a grid""" @@ -191,6 +225,7 @@ def tasks(): @app.route('/tasks//') @login_required +@after_start def task(category, score): """Displays a task with a given category and score""" @@ -214,9 +249,11 @@ def task(category, score): @app.route('/task/submit', methods = ['POST']) @login_required +@before_end +@after_start def submit(): """Handles the submission of flags""" - + print "fuck" category = request.form['category'] score = request.form['score'] flag = request.form['flag'] @@ -243,7 +280,6 @@ def submit(): return jsonify(result) @app.route('/scoreboard') -@login_required def scoreboard(): """Displays the scoreboard""" @@ -274,7 +310,6 @@ def scoreboard_json(): return jsonify({'standings':data}) @app.route('/about') -@login_required def about(): """Displays the about menu""" @@ -352,6 +387,9 @@ def index(): app.jinja_env.globals['csrf_token'] = generate_csrf_token + start = config['start'] + end = config['end'] + # Start web server app.run(host=config['host'], port=config['port'], debug=config['debug'], threaded=True) diff --git a/templates/frame.html b/templates/frame.html index 7db58fc..7967b73 100755 --- a/templates/frame.html +++ b/templates/frame.html @@ -54,6 +54,7 @@ {% include page %}