From 6f60ddd2f58897fdf6546c9f7dd474d19043d955 Mon Sep 17 00:00:00 2001 From: Kevin Chung Date: Mon, 4 Sep 2017 05:03:06 -0400 Subject: [PATCH] Admins can bypass ctftime (#374) * Admins can see/solve challenges regardless of ctftime * Adding tests for ctftime based functionality --- CTFd/challenges.py | 16 +- CTFd/themes/original/templates/chals.html | 24 +-- .../themes/original/templates/errors/403.html | 11 +- .../themes/original/templates/errors/404.html | 10 +- .../themes/original/templates/errors/500.html | 8 +- .../themes/original/templates/errors/502.html | 8 +- tests/admin/__init__.py | 0 tests/{ => admin}/test_admin_facing.py | 31 ++++ tests/test_utils.py | 126 ++++++++++++++- tests/user/__init__.py | 0 tests/user/test_challenges.py | 149 ++++++++++++++++++ tests/{ => user}/test_user_facing.py | 139 ---------------- 12 files changed, 348 insertions(+), 174 deletions(-) create mode 100644 tests/admin/__init__.py rename tests/{ => admin}/test_admin_facing.py (63%) create mode 100644 tests/user/__init__.py create mode 100644 tests/user/test_challenges.py rename tests/{ => user}/test_user_facing.py (78%) diff --git a/CTFd/challenges.py b/CTFd/challenges.py index 8a120b5..a084c39 100644 --- a/CTFd/challenges.py +++ b/CTFd/challenges.py @@ -3,7 +3,7 @@ import logging import re import time -from flask import render_template, request, redirect, jsonify, url_for, session, Blueprint +from flask import render_template, request, redirect, jsonify, url_for, session, Blueprint, abort from sqlalchemy.sql import or_ from CTFd.models import db, Challenges, Files, Solves, WrongKeys, Keys, Tags, Teams, Awards, Hints, Unlocks @@ -104,7 +104,7 @@ def chals(): if utils.view_after_ctf(): pass else: - return redirect(url_for('views.static_html')) + abort(403) if utils.user_can_view_challenges() and (utils.ctf_started() or utils.is_admin()): chals = Challenges.query.filter(or_(Challenges.hidden != True, Challenges.hidden == None)).order_by(Challenges.value).all() json = {'game': []} @@ -136,7 +136,7 @@ def chals(): return jsonify(json) else: db.session.close() - return redirect(url_for('auth.login', next='chals')) + abort(403) @challenges.route('/chals/solves') @@ -250,10 +250,10 @@ def who_solved(chalid): @challenges.route('/chal/', methods=['POST']) def chal(chalid): if utils.ctf_ended() and not utils.view_after_ctf(): - return redirect(url_for('challenges.challenges_view')) + abort(403) if not utils.user_can_view_challenges(): return redirect(url_for('auth.login', next=request.path)) - if utils.authed() and utils.is_verified() and (utils.ctf_started() or utils.view_after_ctf()): + if (utils.authed() and utils.is_verified() and (utils.ctf_started() or utils.view_after_ctf())) or utils.is_admin(): fails = WrongKeys.query.filter_by(teamid=session['id'], chalid=chalid).count() logger = logging.getLogger('keys') data = (time.strftime("%m/%d/%Y %X"), session['username'].encode('utf-8'), request.form['key'].encode('utf-8'), utils.get_kpm(session['id'])) @@ -289,7 +289,7 @@ def chal(chalid): chal_class = get_chal_class(chal.type) status, message = chal_class.solve(chal, provided_key) if status: # The challenge plugin says the input is right - if utils.ctftime(): + if utils.ctftime() or utils.is_admin(): solve = Solves(teamid=session['id'], chalid=chalid, ip=utils.get_ip(), flag=provided_key) db.session.add(solve) db.session.commit() @@ -297,7 +297,7 @@ def chal(chalid): logger.info("[{0}] {1} submitted {2} with kpm {3} [CORRECT]".format(*data)) return jsonify({'status': 1, 'message': message}) else: # The challenge plugin says the input is wrong - if utils.ctftime(): + if utils.ctftime() or utils.is_admin(): wrong = WrongKeys(teamid=session['id'], chalid=chalid, ip=utils.get_ip(), flag=provided_key) db.session.add(wrong) db.session.commit() @@ -324,4 +324,4 @@ def chal(chalid): return jsonify({ 'status': -1, 'message': "You must be logged in to solve a challenge" - }) + }), 403 diff --git a/CTFd/themes/original/templates/chals.html b/CTFd/themes/original/templates/chals.html index 4b22dc9..85a1896 100644 --- a/CTFd/themes/original/templates/chals.html +++ b/CTFd/themes/original/templates/chals.html @@ -70,21 +70,21 @@ {% block content %} -{% if errors %} -
-
-{% for error in errors %} -

{{ error }}

-{% endfor %} -
-
-{% else %} -

Challenges

+ +{% if errors %} +
+{% for error in errors %} +

{{ error }}

+{% endfor %} +
+{% endif %} + +{% if admin or not errors %}
@@ -113,6 +113,8 @@ {% block scripts %} - {% if not errors %}{% endif %} + {% if admin or not errors %} + + {% endif %} {% endblock %} diff --git a/CTFd/themes/original/templates/errors/403.html b/CTFd/themes/original/templates/errors/403.html index 9af6eec..66f4195 100644 --- a/CTFd/themes/original/templates/errors/403.html +++ b/CTFd/themes/original/templates/errors/403.html @@ -2,13 +2,14 @@ {% block content %} -
-

403

-

An authorization error has occured

-

Please try again

+
+
+

403

+

An authorization error has occured

+

Please try again

+
- {% endblock %} {% block scripts %} diff --git a/CTFd/themes/original/templates/errors/404.html b/CTFd/themes/original/templates/errors/404.html index 5a8c1bc..abb7bda 100644 --- a/CTFd/themes/original/templates/errors/404.html +++ b/CTFd/themes/original/templates/errors/404.html @@ -2,10 +2,12 @@ {% block content %} -
-

404

-

Whoops, looks like we can't find that.

-

Sorry about that

+
+
+

404

+

Whoops, looks like we can't find that.

+

Sorry about that

+
diff --git a/CTFd/themes/original/templates/errors/500.html b/CTFd/themes/original/templates/errors/500.html index 13a2436..a6e69ac 100644 --- a/CTFd/themes/original/templates/errors/500.html +++ b/CTFd/themes/original/templates/errors/500.html @@ -2,9 +2,11 @@ {% block content %} -
-

500

-

An Internal Server Error has occured

+
+
+

500

+

An Internal Server Error has occured

+
diff --git a/CTFd/themes/original/templates/errors/502.html b/CTFd/themes/original/templates/errors/502.html index 87261d0..2552a7d 100644 --- a/CTFd/themes/original/templates/errors/502.html +++ b/CTFd/themes/original/templates/errors/502.html @@ -2,9 +2,11 @@ {% block content %} -
-

502

-

That action isn't allowed

+
+
+

502

+

That action isn't allowed

+
diff --git a/tests/admin/__init__.py b/tests/admin/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_admin_facing.py b/tests/admin/test_admin_facing.py similarity index 63% rename from tests/test_admin_facing.py rename to tests/admin/test_admin_facing.py index 2878eab..d306dec 100644 --- a/tests/test_admin_facing.py +++ b/tests/admin/test_admin_facing.py @@ -1,5 +1,8 @@ from tests.helpers import * from CTFd.models import Teams +from CTFd.utils import get_config, set_config, override_template, sendmail, verify_email, ctf_started, ctf_ended +from freezegun import freeze_time +from mock import patch def test_admin_panel(): @@ -72,3 +75,31 @@ def test_admin_config(): r = client.get('/admin/config') assert r.status_code == 200 destroy_ctfd(app) + + +def test_admins_can_access_challenges_before_ctftime(): + '''Admins can see and solve challenges despite it being before ctftime''' + app = create_ctfd() + with app.app_context(): + set_config('start', '1507089600') # Wednesday, October 4, 2017 12:00:00 AM GMT-04:00 DST + set_config('end', '1507262400') # Friday, October 6, 2017 12:00:00 AM GMT-04:00 DST + register_user(app) + chal = gen_challenge(app.db) + chal_id = chal.id + flag = gen_flag(app.db, chal=chal.id, flag=u'flag') + + with freeze_time("2017-10-2"): + client = login_as_user(app, name='admin', password='password') + r = client.get('/chals') + assert r.status_code == 200 + + with client.session_transaction() as sess: + data = { + "key": 'flag', + "nonce": sess.get('nonce') + } + r = client.post('/chal/{}'.format(chal_id), data=data) + assert r.status_code == 200 + solve_count = app.db.session.query(app.db.func.count(Solves.id)).first()[0] + assert solve_count == 1 + destroy_ctfd(app) diff --git a/tests/test_utils.py b/tests/test_utils.py index 6d97704..6d5a86c 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -3,7 +3,7 @@ from tests.helpers import * from CTFd.models import ip2long, long2ip -from CTFd.utils import get_config, set_config, override_template, sendmail, verify_email +from CTFd.utils import get_config, set_config, override_template, sendmail, verify_email, ctf_started, ctf_ended from CTFd.utils import base64encode, base64decode from freezegun import freeze_time from mock import patch @@ -169,3 +169,127 @@ def test_verify_email(mock_smtp): # For now just assert that sendmail was called. mock_smtp.return_value.sendmail.assert_called_with(from_addr, [to_addr], email_msg.as_string()) destroy_ctfd(app) + + +def test_ctftime_prevents_accessing_challenges_before_ctf(): + """Test that the ctftime function prevents users from accessing challenges after the ctf""" + app = create_ctfd() + with app.app_context(): + set_config('start', '1507089600') # Wednesday, October 4, 2017 12:00:00 AM GMT-04:00 DST + set_config('end', '1507262400') # Friday, October 6, 2017 12:00:00 AM GMT-04:00 DST + register_user(app) + chal = gen_challenge(app.db) + chal_id = chal.id + flag = gen_flag(app.db, chal=chal.id, flag=u'flag') + + with freeze_time("2017-10-3"): + client = login_as_user(app) + r = client.get('/chals') + assert r.status_code == 403 + + with client.session_transaction() as sess: + data = { + "key": 'flag', + "nonce": sess.get('nonce') + } + r = client.post('/chal/{}'.format(chal_id), data=data) + assert r.status_code == 403 + solve_count = app.db.session.query(app.db.func.count(Solves.id)).first()[0] + assert solve_count == 0 + destroy_ctfd(app) + + +def test_ctftime_allows_accessing_challenges_during_ctf(): + """Test that the ctftime function allows accessing challenges during the ctf""" + app = create_ctfd() + with app.app_context(): + set_config('start', '1507089600') # Wednesday, October 4, 2017 12:00:00 AM GMT-04:00 DST + set_config('end', '1507262400') # Friday, October 6, 2017 12:00:00 AM GMT-04:00 DST + register_user(app) + chal = gen_challenge(app.db) + chal_id = chal.id + flag = gen_flag(app.db, chal=chal.id, flag=u'flag') + + with freeze_time("2017-10-5"): + client = login_as_user(app) + r = client.get('/chals') + assert r.status_code == 200 + + with client.session_transaction() as sess: + data = { + "key": 'flag', + "nonce": sess.get('nonce') + } + r = client.post('/chal/{}'.format(chal_id), data=data) + assert r.status_code == 200 + solve_count = app.db.session.query(app.db.func.count(Solves.id)).first()[0] + assert solve_count == 1 + destroy_ctfd(app) + + +def test_ctftime_prevents_accessing_challenges_after_ctf(): + """Test that the ctftime function prevents accessing challenges after the ctf""" + app = create_ctfd() + with app.app_context(): + set_config('start', '1507089600') # Wednesday, October 4, 2017 12:00:00 AM GMT-04:00 DST + set_config('end', '1507262400') # Friday, October 6, 2017 12:00:00 AM GMT-04:00 DST + register_user(app) + chal = gen_challenge(app.db) + chal_id = chal.id + flag = gen_flag(app.db, chal=chal.id, flag=u'flag') + + with freeze_time("2017-10-7"): + client = login_as_user(app) + r = client.get('/chals') + assert r.status_code == 403 + + with client.session_transaction() as sess: + data = { + "key": 'flag', + "nonce": sess.get('nonce') + } + r = client.post('/chal/{}'.format(chal_id), data=data) + assert r.status_code == 403 + solve_count = app.db.session.query(app.db.func.count(Solves.id)).first()[0] + assert solve_count == 0 + destroy_ctfd(app) + + +def test_ctf_started(): + '''Tests that the ctf_started function returns the correct value''' + app = create_ctfd() + with app.app_context(): + assert ctf_started() == True + + set_config('start', '1507089600') # Wednesday, October 4, 2017 12:00:00 AM GMT-04:00 DST + set_config('end', '1507262400') # Friday, October 6, 2017 12:00:00 AM GMT-04:00 DST + + with freeze_time("2017-10-3"): + assert ctf_started() == False + + with freeze_time("2017-10-5"): + assert ctf_started() == True + + with freeze_time("2017-10-7"): + assert ctf_started() == True + destroy_ctfd(app) + + +def test_ctf_ended(): + '''Tests that the ctf_ended function returns the correct value''' + app = create_ctfd() + with app.app_context(): + assert ctf_ended() == False + + set_config('start', '1507089600') # Wednesday, October 4, 2017 12:00:00 AM GMT-04:00 DST + set_config('end', '1507262400') # Friday, October 6, 2017 12:00:00 AM GMT-04:00 DST + + with freeze_time("2017-10-3"): + assert ctf_ended() == False + + with freeze_time("2017-10-5"): + assert ctf_ended() == False + + with freeze_time("2017-10-7"): + assert ctf_ended() == True + destroy_ctfd(app) diff --git a/tests/user/__init__.py b/tests/user/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/user/test_challenges.py b/tests/user/test_challenges.py new file mode 100644 index 0000000..eb93752 --- /dev/null +++ b/tests/user/test_challenges.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from CTFd.models import Teams, Solves, WrongKeys +from CTFd.utils import get_config, set_config +from CTFd import utils +from tests.helpers import * +from freezegun import freeze_time +from mock import patch +import json + + +def test_user_get_challenges(): + """Can a registered user load /challenges""" + app = create_ctfd() + with app.app_context(): + register_user(app) + client = login_as_user(app) + r = client.get('/challenges') + assert r.status_code == 200 + destroy_ctfd(app) + + +def test_user_get_chals(): + """Can a registered user load /chals""" + app = create_ctfd() + with app.app_context(): + register_user(app) + client = login_as_user(app) + r = client.get('/chals') + assert r.status_code == 200 + destroy_ctfd(app) + + +def test_viewing_challenges(): + """Test that users can see added challenges""" + app = create_ctfd() + with app.app_context(): + register_user(app) + client = login_as_user(app) + gen_challenge(app.db) + r = client.get('/chals') + chals = json.loads(r.get_data(as_text=True)) + assert len(chals['game']) == 1 + destroy_ctfd(app) + + +def test_submitting_correct_flag(): + """Test that correct flags are correct""" + app = create_ctfd() + with app.app_context(): + register_user(app) + client = login_as_user(app) + chal = gen_challenge(app.db) + flag = gen_flag(app.db, chal=chal.id, flag='flag') + with client.session_transaction() as sess: + data = { + "key": 'flag', + "nonce": sess.get('nonce') + } + r = client.post('/chal/{}'.format(chal.id), data=data) + assert r.status_code == 200 + resp = json.loads(r.data.decode('utf8')) + assert resp.get('status') == 1 and resp.get('message') == "Correct" + destroy_ctfd(app) + + +def test_submitting_incorrect_flag(): + """Test that incorrect flags are incorrect""" + app = create_ctfd() + with app.app_context(): + register_user(app) + client = login_as_user(app) + chal = gen_challenge(app.db) + flag = gen_flag(app.db, chal=chal.id, flag='flag') + with client.session_transaction() as sess: + data = { + "key": 'notflag', + "nonce": sess.get('nonce') + } + r = client.post('/chal/{}'.format(chal.id), data=data) + assert r.status_code == 200 + resp = json.loads(r.data.decode('utf8')) + assert resp.get('status') == 0 and resp.get('message') == "Incorrect" + destroy_ctfd(app) + + +def test_submitting_unicode_flag(): + """Test that users can submit a unicode flag""" + app = create_ctfd() + with app.app_context(): + register_user(app) + client = login_as_user(app) + chal = gen_challenge(app.db) + flag = gen_flag(app.db, chal=chal.id, flag=u'你好') + with client.session_transaction() as sess: + data = { + "key": '你好', + "nonce": sess.get('nonce') + } + r = client.post('/chal/{}'.format(chal.id), data=data) + assert r.status_code == 200 + resp = json.loads(r.data.decode('utf8')) + assert resp.get('status') == 1 and resp.get('message') == "Correct" + destroy_ctfd(app) + + +def test_submitting_flags_with_large_ips(): + '''Test that users with high octect IP addresses can submit flags''' + app = create_ctfd() + with app.app_context(): + register_user(app) + client = login_as_user(app) + + # SQLite doesn't support BigInteger well so we can't test it properly + ip_addresses = ['172.18.0.1', '255.255.255.255', '2001:0db8:85a3:0000:0000:8a2e:0370:7334'] + for ip_address in ip_addresses: + # Monkeypatch get_ip + utils.get_ip = lambda: ip_address + + # Generate challenge and flag + chal = gen_challenge(app.db) + chal_id = chal.id + flag = gen_flag(app.db, chal=chal.id, flag=u'correct_key') + + # Submit wrong_key + with client.session_transaction() as sess: + data = { + "key": 'wrong_key', + "nonce": sess.get('nonce') + } + r = client.post('/chal/{}'.format(chal_id), data=data) + assert r.status_code == 200 + resp = json.loads(r.data.decode('utf8')) + assert resp.get('status') == 0 and resp.get('message') == "Incorrect" + assert WrongKeys.query.filter_by(ip=ip_address).first() + + # Submit correct key + with client.session_transaction() as sess: + data = { + "key": 'correct_key', + "nonce": sess.get('nonce') + } + r = client.post('/chal/{}'.format(chal_id), data=data) + assert r.status_code == 200 + resp = json.loads(r.data.decode('utf8')) + assert resp.get('status') == 1 and resp.get('message') == "Correct" + assert Solves.query.filter_by(ip=ip_address).first() + destroy_ctfd(app) diff --git a/tests/test_user_facing.py b/tests/user/test_user_facing.py similarity index 78% rename from tests/test_user_facing.py rename to tests/user/test_user_facing.py index 2310746..2568d46 100644 --- a/tests/test_user_facing.py +++ b/tests/user/test_user_facing.py @@ -141,28 +141,6 @@ def test_user_get_topteams(): destroy_ctfd(app) -def test_user_get_challenges(): - """Can a registered user load /challenges""" - app = create_ctfd() - with app.app_context(): - register_user(app) - client = login_as_user(app) - r = client.get('/challenges') - assert r.status_code == 200 - destroy_ctfd(app) - - -def test_user_get_chals(): - """Can a registered user load /chals""" - app = create_ctfd() - with app.app_context(): - register_user(app) - client = login_as_user(app) - r = client.get('/chals') - assert r.status_code == 200 - destroy_ctfd(app) - - def test_user_get_solves_per_chal(): """Can a registered user load /chals/solves""" app = create_ctfd() @@ -231,123 +209,6 @@ def test_user_get_reset_password(): destroy_ctfd(app) -def test_viewing_challenges(): - """Test that users can see added challenges""" - app = create_ctfd() - with app.app_context(): - register_user(app) - client = login_as_user(app) - gen_challenge(app.db) - r = client.get('/chals') - chals = json.loads(r.get_data(as_text=True)) - assert len(chals['game']) == 1 - destroy_ctfd(app) - - -def test_submitting_correct_flag(): - """Test that correct flags are correct""" - app = create_ctfd() - with app.app_context(): - register_user(app) - client = login_as_user(app) - chal = gen_challenge(app.db) - flag = gen_flag(app.db, chal=chal.id, flag='flag') - with client.session_transaction() as sess: - data = { - "key": 'flag', - "nonce": sess.get('nonce') - } - r = client.post('/chal/{}'.format(chal.id), data=data) - assert r.status_code == 200 - resp = json.loads(r.data.decode('utf8')) - assert resp.get('status') == 1 and resp.get('message') == "Correct" - destroy_ctfd(app) - - -def test_submitting_incorrect_flag(): - """Test that incorrect flags are incorrect""" - app = create_ctfd() - with app.app_context(): - register_user(app) - client = login_as_user(app) - chal = gen_challenge(app.db) - flag = gen_flag(app.db, chal=chal.id, flag='flag') - with client.session_transaction() as sess: - data = { - "key": 'notflag', - "nonce": sess.get('nonce') - } - r = client.post('/chal/{}'.format(chal.id), data=data) - assert r.status_code == 200 - resp = json.loads(r.data.decode('utf8')) - assert resp.get('status') == 0 and resp.get('message') == "Incorrect" - destroy_ctfd(app) - - -def test_submitting_unicode_flag(): - """Test that users can submit a unicode flag""" - app = create_ctfd() - with app.app_context(): - register_user(app) - client = login_as_user(app) - chal = gen_challenge(app.db) - flag = gen_flag(app.db, chal=chal.id, flag=u'你好') - with client.session_transaction() as sess: - data = { - "key": '你好', - "nonce": sess.get('nonce') - } - r = client.post('/chal/{}'.format(chal.id), data=data) - assert r.status_code == 200 - resp = json.loads(r.data.decode('utf8')) - assert resp.get('status') == 1 and resp.get('message') == "Correct" - destroy_ctfd(app) - - -def test_submitting_flags_with_large_ips(): - '''Test that users with high octect IP addresses can submit flags''' - app = create_ctfd() - with app.app_context(): - register_user(app) - client = login_as_user(app) - - # SQLite doesn't support BigInteger well so we can't test it properly - ip_addresses = ['172.18.0.1', '255.255.255.255', '2001:0db8:85a3:0000:0000:8a2e:0370:7334'] - for ip_address in ip_addresses: - # Monkeypatch get_ip - utils.get_ip = lambda: ip_address - - # Generate challenge and flag - chal = gen_challenge(app.db) - chal_id = chal.id - flag = gen_flag(app.db, chal=chal.id, flag=u'correct_key') - - # Submit wrong_key - with client.session_transaction() as sess: - data = { - "key": 'wrong_key', - "nonce": sess.get('nonce') - } - r = client.post('/chal/{}'.format(chal_id), data=data) - assert r.status_code == 200 - resp = json.loads(r.data.decode('utf8')) - assert resp.get('status') == 0 and resp.get('message') == "Incorrect" - assert WrongKeys.query.filter_by(ip=ip_address).first() - - # Submit correct key - with client.session_transaction() as sess: - data = { - "key": 'correct_key', - "nonce": sess.get('nonce') - } - r = client.post('/chal/{}'.format(chal_id), data=data) - assert r.status_code == 200 - resp = json.loads(r.data.decode('utf8')) - assert resp.get('status') == 1 and resp.get('message') == "Correct" - assert Solves.query.filter_by(ip=ip_address).first() - destroy_ctfd(app) - - def test_scoring_logic(): """Test that scoring logic is correct""" app = create_ctfd()