diff --git a/CTFd/__init__.py b/CTFd/__init__.py index ae962761..c52e23bc 100644 --- a/CTFd/__init__.py +++ b/CTFd/__init__.py @@ -7,6 +7,7 @@ from jinja2 import FileSystemLoader from sqlalchemy.engine.url import make_url from sqlalchemy.exc import OperationalError, ProgrammingError from sqlalchemy_utils import database_exists, create_database +from sqlalchemy_utils.functions import get_tables from six.moves import input from CTFd.utils import cache, migrate, migrate_upgrade, migrate_stamp @@ -74,13 +75,14 @@ def create_app(config='CTFd.config.Config'): # Register Flask-Migrate migrate.init_app(app, db) - # This creates tables instead of db.create_all() - # Allows migrations to happen properly - migrate_upgrade() - # Alembic sqlite support is lacking so we should just create_all anyway if url.drivername.startswith('sqlite'): db.create_all() + else: + if 'alembic_version' not in db.engine.table_names(): + # This creates tables instead of db.create_all() + # Allows migrations to happen properly + migrate_upgrade() app.db = db diff --git a/CTFd/admin/teams.py b/CTFd/admin/teams.py index a449f668..7ade7971 100644 --- a/CTFd/admin/teams.py +++ b/CTFd/admin/teams.py @@ -221,7 +221,7 @@ def admin_fails(teamid): @admin_teams.route('/admin/solves///solve', methods=['POST']) @admins_only def create_solve(teamid, chalid): - solve = Solves(chalid=chalid, teamid=teamid, ip='127.0.0.1', flag='MARKED_AS_SOLVED_BY_ADMIN') + solve = Solves(teamid=teamid, chalid=chalid, ip='127.0.0.1', flag='MARKED_AS_SOLVED_BY_ADMIN') db.session.add(solve) db.session.commit() db.session.close() diff --git a/CTFd/challenges.py b/CTFd/challenges.py index fee06bc5..31cb6590 100644 --- a/CTFd/challenges.py +++ b/CTFd/challenges.py @@ -262,7 +262,7 @@ def chal(chalid): # Anti-bruteforce / submitting keys too quickly if utils.get_kpm(session['id']) > 10: if utils.ctftime(): - wrong = WrongKeys(session['id'], chalid, request.form['key']) + wrong = WrongKeys(teamid=session['id'], chalid=chalid, ip=utils.get_ip(), flag=request.form['key'].strip()) db.session.add(wrong) db.session.commit() db.session.close() @@ -289,7 +289,7 @@ def chal(chalid): chal_class = get_chal_class(chal.type) if chal_class.solve(chal, provided_key): if utils.ctftime(): - solve = Solves(chalid=chalid, teamid=session['id'], ip=utils.get_ip(), flag=provided_key) + solve = Solves(teamid=session['id'], chalid=chalid, ip=utils.get_ip(), flag=provided_key) db.session.add(solve) db.session.commit() db.session.close() @@ -297,7 +297,7 @@ def chal(chalid): return jsonify({'status': 1, 'message': 'Correct'}) if utils.ctftime(): - wrong = WrongKeys(teamid=session['id'], chalid=chalid, flag=provided_key) + wrong = WrongKeys(teamid=session['id'], chalid=chalid, ip=utils.get_ip(), flag=provided_key) db.session.add(wrong) db.session.commit() db.session.close() diff --git a/CTFd/models.py b/CTFd/models.py index 29c0accf..70f24bad 100644 --- a/CTFd/models.py +++ b/CTFd/models.py @@ -225,39 +225,41 @@ class Solves(db.Model): 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) + ip = db.Column(db.String(46)) flag = db.Column(db.Text) 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, flag): - self.ip = ip2long(ip) + def __init__(self, teamid, chalid, ip, flag): + self.ip = ip self.chalid = chalid self.teamid = teamid self.flag = flag # self.value = value def __repr__(self): - return '' % self.chal + return ''.format(self.teamid, self.chalid, self.ip, self.flag) class WrongKeys(db.Model): 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.String(46)) date = db.Column(db.DateTime, default=datetime.datetime.utcnow) flag = db.Column(db.Text) chal = db.relationship('Challenges', foreign_keys="WrongKeys.chalid", lazy='joined') - def __init__(self, teamid, chalid, flag): + def __init__(self, teamid, chalid, ip, flag): + self.ip = ip self.teamid = teamid self.chalid = chalid self.flag = flag def __repr__(self): - return '' % self.flag + return ''.format(self.teamid, self.chalid, self.ip, self.flag) class Unlocks(db.Model): @@ -278,12 +280,12 @@ class Unlocks(db.Model): class Tracking(db.Model): id = db.Column(db.Integer, primary_key=True) - ip = db.Column(db.BigInteger) + ip = db.Column(db.String(46)) 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.ip = ip self.team = team def __repr__(self): diff --git a/CTFd/themes/admin/templates/team.html b/CTFd/themes/admin/templates/team.html index 21951459..ad1fcaa5 100644 --- a/CTFd/themes/admin/templates/team.html +++ b/CTFd/themes/admin/templates/team.html @@ -104,7 +104,7 @@ {% for addr in addrs %} - {{ addr[0]|long2ip }} + {{ addr[0] }} {% endfor %} diff --git a/CTFd/utils.py b/CTFd/utils.py index f29866f8..48dd825c 100644 --- a/CTFd/utils.py +++ b/CTFd/utils.py @@ -126,7 +126,7 @@ def init_utils(app): @app.before_request def tracker(): if authed(): - track = Tracking.query.filter_by(ip=ip2long(get_ip()), team=session['id']).first() + track = Tracking.query.filter_by(ip=get_ip(), team=session['id']).first() if not track: visit = Tracking(ip=get_ip(), team=session['id']) db.session.add(visit) diff --git a/migrations/versions/1ec4a28fe0ff_change_solves_ip_to_string_and_add_ip_to_wrongkeys.py b/migrations/versions/1ec4a28fe0ff_change_solves_ip_to_string_and_add_ip_to_wrongkeys.py new file mode 100644 index 00000000..5b59ca6b --- /dev/null +++ b/migrations/versions/1ec4a28fe0ff_change_solves_ip_to_string_and_add_ip_to_wrongkeys.py @@ -0,0 +1,94 @@ +"""Change Solves IP to String and add IP to WrongKeys + +Revision ID: 1ec4a28fe0ff +Revises: c7225db614c1 +Create Date: 2017-07-08 17:08:59.098805 + +""" +from CTFd.models import db, Solves, WrongKeys +from alembic import op +import sqlalchemy as sa +from sqlalchemy.sql import text, table, column +import netaddr + +def ip2long(ip): + '''Converts a user's IP address into an integer/long''' + return int(netaddr.IPAddress(ip)) + + +def long2ip(ip_int): + '''Converts a saved integer/long back into an IP address''' + return str(netaddr.IPAddress(ip_int)) + + +# revision identifiers, used by Alembic. +revision = '1ec4a28fe0ff' +down_revision = 'c7225db614c1' +branch_labels = None +depends_on = None + +solves_table = table('solves', + column('id', db.Integer), + column('ip', db.Integer), +) + +tracking_table = table('tracking', + column('id', db.Integer), + column('ip', db.String(46)), + column('team', db.Integer), + column('date', db.DateTime), +) + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + connection = op.get_bind() + + op.alter_column('solves', 'ip', existing_type=sa.Integer(), type_=sa.String(length=46)) + for solve in connection.execute(solves_table.select()): + connection.execute( + solves_table.update().where( + solves_table.c.id == solve.id + ).values( + ip=long2ip(solve.ip), + ) + ) + + op.alter_column('tracking', 'ip', existing_type=sa.Integer(), type_=sa.String(length=46)) + for track in connection.execute(tracking_table.select()): + connection.execute( + tracking_table.update().where( + tracking_table.c.id == track.id + ).values( + ip=long2ip(track.ip), + ) + ) + op.add_column('wrong_keys', sa.Column('ip', sa.String(length=46), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + connection = op.get_bind() + for solve in connection.execute(solves_table.select()): + connection.execute( + solves_table.update().where( + solves_table.c.id == solve.id + ).values( + ip=ip2long(solve.ip), + ) + ) + + for track in connection.execute(tracking_table.select()): + connection.execute( + tracking_table.update().where( + tracking_table.c.id == track.id + ).values( + ip=ip2long(track.ip), + ) + ) + + op.alter_column('solves', 'ip', existing_type=sa.String(length=46), type_=sa.Integer()) + op.alter_column('tracking', 'ip', existing_type=sa.String(length=46), type_=sa.Integer()) + op.drop_column('wrong_keys', 'ip') + # ### end Alembic commands ### diff --git a/populate.py b/populate.py index 08809f3c..b6dc29b0 100644 --- a/populate.py +++ b/populate.py @@ -260,7 +260,7 @@ if __name__ == '__main__': chalid = random.randint(1, CHAL_AMOUNT) if chalid not in used: used.append(chalid) - solve = Solves(chalid, x + 1, '127.0.0.1', gen_word()) + solve = Solves(x + 1, chalid, '127.0.0.1', gen_word()) new_base = random_date(base_time, base_time + datetime.timedelta(minutes=random.randint(30, 60))) solve.date = new_base @@ -293,7 +293,7 @@ if __name__ == '__main__': chalid = random.randint(1, CHAL_AMOUNT) if chalid not in used: used.append(chalid) - wrong = WrongKeys(x + 1, chalid, gen_word()) + wrong = WrongKeys(x + 1, chalid, '127.0.0.1', gen_word()) new_base = random_date(base_time, base_time + datetime.timedelta(minutes=random.randint(30, 60))) wrong.date = new_base diff --git a/requirements.txt b/requirements.txt index 28339945..b6b4ebd9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,16 +1,16 @@ -Flask==0.12 +Flask==0.12.2 Flask-SQLAlchemy==2.2 Flask-Session==0.3.1 Flask-Caching==1.2.0 -Flask-Migrate==2.0.3 -SQLAlchemy==1.1.6 -SQLAlchemy-Utils==0.32.12 +Flask-Migrate==2.0.4 +SQLAlchemy==1.1.11 +SQLAlchemy-Utils==0.32.14 passlib==1.7.1 bcrypt==3.1.3 six==1.10.0 itsdangerous==0.24 -requests==2.13.0 -PyMySQL==0.7.10 +requests==2.18.1 +PyMySQL==0.7.11 gunicorn==19.7.0 dataset==0.8.0 mistune==0.7.4 diff --git a/tests/helpers.py b/tests/helpers.py index 1d45c1aa..dcd0c95c 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -7,16 +7,6 @@ from sqlalchemy.engine.url import make_url def create_ctfd(ctf_name="CTFd", name="admin", email="admin@ctfd.io", password="password", setup=True): app = create_app('CTFd.config.TestingConfig') - url = make_url(app.config['SQLALCHEMY_DATABASE_URI']) - if url.drivername == 'postgres': - url.drivername = 'postgresql' - - if database_exists(url): - drop_database(url) - create_database(url) - with app.app_context(): - app.db.create_all() - if setup: with app.app_context(): with app.test_client() as client: @@ -34,6 +24,10 @@ def create_ctfd(ctf_name="CTFd", name="admin", email="admin@ctfd.io", password=" return app +def destroy_ctfd(app): + drop_database(app.config['SQLALCHEMY_DATABASE_URI']) + + def register_user(app, name="user", email="user@ctfd.io", password="password"): with app.app_context(): with app.test_client() as client: @@ -101,15 +95,15 @@ def gen_team(db, name='name', email='user@ctfd.io', password='password'): return team -def gen_solve(db, chalid, teamid, ip='127.0.0.1', flag='rightkey'): - solve = Solves(chalid, teamid, ip, flag) +def gen_solve(db, teamid, chalid, ip='127.0.0.1', flag='rightkey'): + solve = Solves(teamid, chalid, ip, flag) db.session.add(solve) db.session.commit() return solve -def gen_wrongkey(db, teamid, chalid, flag='wrongkey'): - wrongkey = WrongKeys(teamid, chalid, flag) +def gen_wrongkey(db, teamid, chalid, ip='127.0.0.1', flag='wrongkey'): + wrongkey = WrongKeys(teamid, chalid, ip, flag) db.session.add(wrongkey) db.session.commit() return wrongkey diff --git a/tests/test_admin_facing.py b/tests/test_admin_facing.py index 5e57b63a..598ede40 100644 --- a/tests/test_admin_facing.py +++ b/tests/test_admin_facing.py @@ -1,4 +1,4 @@ -from tests.helpers import create_ctfd, register_user, login_as_user +from tests.helpers import * from CTFd.models import Teams @@ -11,6 +11,7 @@ def test_admin_panel(): assert r.status_code == 302 r = client.get('/admin/graphs') assert r.status_code == 200 + destroy_ctfd(app) def test_admin_pages(): @@ -20,6 +21,7 @@ def test_admin_pages(): client = login_as_user(app, name="admin", password="password") r = client.get('/admin/pages') assert r.status_code == 200 + destroy_ctfd(app) def test_admin_teams(): @@ -29,6 +31,7 @@ def test_admin_teams(): client = login_as_user(app, name="admin", password="password") r = client.get('/admin/teams') assert r.status_code == 200 + destroy_ctfd(app) def test_admin_scoreboard(): @@ -38,6 +41,7 @@ def test_admin_scoreboard(): client = login_as_user(app, name="admin", password="password") r = client.get('/admin/scoreboard') assert r.status_code == 200 + destroy_ctfd(app) def test_admin_containers(): @@ -47,6 +51,7 @@ def test_admin_containers(): client = login_as_user(app, name="admin", password="password") r = client.get('/admin/containers') assert r.status_code == 200 + destroy_ctfd(app) def test_admin_chals(): @@ -56,6 +61,7 @@ def test_admin_chals(): client = login_as_user(app, name="admin", password="password") r = client.get('/admin/chals') assert r.status_code == 200 + destroy_ctfd(app) def test_admin_statistics(): @@ -65,6 +71,7 @@ def test_admin_statistics(): client = login_as_user(app, name="admin", password="password") r = client.get('/admin/statistics') assert r.status_code == 200 + destroy_ctfd(app) def test_admin_config(): @@ -74,3 +81,4 @@ def test_admin_config(): client = login_as_user(app, name="admin", password="password") r = client.get('/admin/config') assert r.status_code == 200 + destroy_ctfd(app) diff --git a/tests/test_user_facing.py b/tests/test_user_facing.py index 8452e663..a2014918 100644 --- a/tests/test_user_facing.py +++ b/tests/test_user_facing.py @@ -2,8 +2,9 @@ # -*- coding: utf-8 -*- from tests.helpers import * -from CTFd.models import Teams +from CTFd.models import Teams, Solves, WrongKeys import json +from CTFd import utils def test_index(): @@ -13,6 +14,7 @@ def test_index(): with app.test_client() as client: r = client.get('/') assert r.status_code == 200 + destroy_ctfd(app) def test_register_user(): @@ -22,6 +24,7 @@ def test_register_user(): register_user(app) team_count = app.db.session.query(app.db.func.count(Teams.id)).first()[0] assert team_count == 2 # There's the admin user and the created user + destroy_ctfd(app) def test_register_duplicate_teamname(): @@ -32,6 +35,7 @@ def test_register_duplicate_teamname(): register_user(app, name="user1", email="user2@ctfd.io", password="password") team_count = app.db.session.query(app.db.func.count(Teams.id)).first()[0] assert team_count == 2 # There's the admin user and the first created user + destroy_ctfd(app) def test_register_duplicate_email(): @@ -42,6 +46,7 @@ def test_register_duplicate_email(): register_user(app, name="user2", email="user1@ctfd.io", password="password") team_count = app.db.session.query(app.db.func.count(Teams.id)).first()[0] assert team_count == 2 # There's the admin user and the first created user + destroy_ctfd(app) def test_user_bad_login(): @@ -52,6 +57,7 @@ def test_user_bad_login(): client = login_as_user(app, name="user", password="wrong_password") r = client.get('/profile') assert r.location.startswith("http://localhost/login") # We got redirected to login + destroy_ctfd(app) def test_user_login(): @@ -63,6 +69,7 @@ def test_user_login(): r = client.get('/profile') assert r.location != "http://localhost/login" # We didn't get redirected to login assert r.status_code == 200 + destroy_ctfd(app) def test_user_isnt_admin(): @@ -74,6 +81,7 @@ def test_user_isnt_admin(): r = client.get('/admin/graphs') assert r.location == "http://localhost/login" assert r.status_code == 302 + destroy_ctfd(app) def test_user_get_teams(): @@ -84,6 +92,7 @@ def test_user_get_teams(): client = login_as_user(app) r = client.get('/teams') assert r.status_code == 200 + destroy_ctfd(app) def test_user_get_scoreboard(): @@ -94,6 +103,7 @@ def test_user_get_scoreboard(): client = login_as_user(app) r = client.get('/scoreboard') assert r.status_code == 200 + destroy_ctfd(app) def test_user_get_scores(): @@ -104,6 +114,7 @@ def test_user_get_scores(): client = login_as_user(app) r = client.get('/scores') assert r.status_code == 200 + destroy_ctfd(app) def test_user_get_topteams(): @@ -114,6 +125,7 @@ def test_user_get_topteams(): client = login_as_user(app) r = client.get('/top/10') assert r.status_code == 200 + destroy_ctfd(app) def test_user_get_challenges(): @@ -124,6 +136,7 @@ def test_user_get_challenges(): client = login_as_user(app) r = client.get('/challenges') assert r.status_code == 200 + destroy_ctfd(app) def test_user_get_chals(): @@ -134,6 +147,7 @@ def test_user_get_chals(): client = login_as_user(app) r = client.get('/chals') assert r.status_code == 200 + destroy_ctfd(app) def test_user_get_solves_per_chal(): @@ -144,6 +158,7 @@ def test_user_get_solves_per_chal(): client = login_as_user(app) r = client.get('/chals/solves') assert r.status_code == 200 + destroy_ctfd(app) def test_user_get_solves(): @@ -154,6 +169,7 @@ def test_user_get_solves(): client = login_as_user(app) r = client.get('/solves') assert r.status_code == 200 + destroy_ctfd(app) def test_user_get_team_page(): @@ -164,6 +180,7 @@ def test_user_get_team_page(): client = login_as_user(app) r = client.get('/team/2') assert r.status_code == 200 + destroy_ctfd(app) def test_user_get_profile(): @@ -174,6 +191,7 @@ def test_user_get_profile(): client = login_as_user(app) r = client.get('/profile') assert r.status_code == 200 + destroy_ctfd(app) def test_user_get_logout(): @@ -186,6 +204,7 @@ def test_user_get_logout(): r = client.get('/challenges') assert r.location == "http://localhost/login?next=challenges" assert r.status_code == 302 + destroy_ctfd(app) def test_user_get_reset_password(): @@ -196,6 +215,7 @@ def test_user_get_reset_password(): client = app.test_client() r = client.get('/reset_password') assert r.status_code == 200 + destroy_ctfd(app) def test_viewing_challenges(): @@ -208,6 +228,7 @@ def test_viewing_challenges(): 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(): @@ -227,6 +248,7 @@ def test_submitting_correct_flag(): 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(): @@ -246,6 +268,7 @@ def test_submitting_incorrect_flag(): 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(): @@ -265,6 +288,50 @@ def test_submitting_unicode_flag(): 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(): + 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_pages_routing_and_rendering(): @@ -279,6 +346,7 @@ def test_pages_routing_and_rendering(): r = client.get('/test') output = r.get_data(as_text=True) assert "

The quick brown fox jumped over the lazy dog

" in output + destroy_ctfd(app) def test_themes_handler(): @@ -298,6 +366,7 @@ def test_themes_handler(): assert r.status_code == 404 r = client.get('/themes/original/static/../../../utils.py') assert r.status_code == 404 + destroy_ctfd(app) def test_ctfd_setup_redirect(): @@ -312,3 +381,4 @@ def test_ctfd_setup_redirect(): # Files in /themes load properly r = client.get('/themes/original/static/css/style.css') assert r.status_code == 200 + destroy_ctfd(app) diff --git a/tests/test_utils.py b/tests/test_utils.py index 18bd61c4..f09b1ce8 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -16,6 +16,7 @@ def test_get_config_and_set_config(): config = set_config('TEST_CONFIG_ENTRY', 'test_config_entry') assert config.value == 'test_config_entry' assert get_config('TEST_CONFIG_ENTRY') == 'test_config_entry' + destroy_ctfd(app) def test_ip2long_ipv4(): @@ -50,6 +51,7 @@ def test_override_template(): assert r.status_code == 200 output = r.get_data(as_text=True) assert 'LOGIN OVERRIDE' in output + destroy_ctfd(app) def test_admin_override_template(): @@ -63,6 +65,7 @@ def test_admin_override_template(): assert r.status_code == 200 output = r.get_data(as_text=True) assert 'ADMIN TEAM OVERRIDE' in output + destroy_ctfd(app) @patch('smtplib.SMTP') @@ -89,3 +92,4 @@ def test_sendmail_with_smtp(mock_smtp): email_msg['To'] = to_addr mock_smtp.return_value.sendmail.assert_called_once_with(from_addr, [to_addr], email_msg.as_string()) + destroy_ctfd(app)