Admins can bypass ctftime (#374)

* Admins can see/solve challenges regardless of ctftime
* Adding tests for ctftime based functionality
selenium-screenshot-testing
Kevin Chung 2017-09-04 05:03:06 -04:00 committed by GitHub
parent bcaff30a7d
commit 6f60ddd2f5
12 changed files with 348 additions and 174 deletions

View File

@ -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/<int:chalid>', 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

View File

@ -70,21 +70,21 @@
{% block content %}
{% if errors %}
<div class="container main-container">
<div id='errors' class="row">
{% for error in errors %}
<h1>{{ error }}</h1>
{% endfor %}
</div>
</div>
{% else %}
<div class="jumbotron home">
<div class="container">
<h1>Challenges</h1>
</div>
</div>
{% if errors %}
<div id='errors' class="row">
{% for error in errors %}
<h1>{{ error }}</h1>
{% endfor %}
</div>
{% endif %}
{% if admin or not errors %}
<div class="container main-container">
<div id='challenges-board' class="row">
</div>
@ -113,6 +113,8 @@
{% block scripts %}
<script src="{{ request.script_root }}/themes/{{ ctf_theme() }}/static/js/utils.js"></script>
<script src="{{ request.script_root }}/themes/{{ ctf_theme() }}/static/js/multi-modal.js"></script>
{% if not errors %}<script src="{{ request.script_root }}/themes/{{ ctf_theme() }}/static/js/chalboard.js"></script>{% endif %}
{% if admin or not errors %}
<script src="{{ request.script_root }}/themes/{{ ctf_theme() }}/static/js/chalboard.js"></script>
{% endif %}
<script src="{{ request.script_root }}/themes/{{ ctf_theme() }}/static/js/style.js"></script>
{% endblock %}

View File

@ -2,13 +2,14 @@
{% block content %}
<div class="row">
<h1 class="text-center">403</h1>
<h2 class="text-center">An authorization error has occured</h2>
<h2 class="text-center">Please try again</h2>
<div class="container main-container">
<div class="row">
<h1 class="text-center">403</h1>
<h2 class="text-center">An authorization error has occured</h2>
<h2 class="text-center">Please try again</h2>
</div>
</div>
{% endblock %}
{% block scripts %}

View File

@ -2,10 +2,12 @@
{% block content %}
<div class="row">
<h1 class="text-center">404</h1>
<h2 class="text-center">Whoops, looks like we can't find that.</h2>
<h2 class="text-center">Sorry about that</h2>
<div class="container main-container">
<div class="row">
<h1 class="text-center">404</h1>
<h2 class="text-center">Whoops, looks like we can't find that.</h2>
<h2 class="text-center">Sorry about that</h2>
</div>
</div>

View File

@ -2,9 +2,11 @@
{% block content %}
<div class="row">
<h1 class="text-center">500</h1>
<h2 class="text-center">An Internal Server Error has occured</h2>
<div class="container main-container">
<div class="row">
<h1 class="text-center">500</h1>
<h2 class="text-center">An Internal Server Error has occured</h2>
</div>
</div>

View File

@ -2,9 +2,11 @@
{% block content %}
<div class="row">
<h1 class="text-center">502</h1>
<h2 class="text-center">That action isn't allowed</h2>
<div class="container main-container">
<div class="row">
<h1 class="text-center">502</h1>
<h2 class="text-center">That action isn't allowed</h2>
</div>
</div>

0
tests/admin/__init__.py Normal file
View File

View File

@ -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)

View File

@ -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)

0
tests/user/__init__.py Normal file
View File

View File

@ -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)

View File

@ -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()