def admin_plugin_config(plugin): if request.method == 'GET': if plugin in utils.get_configurable_plugins(): config = open(os.path.join(app.root_path, 'plugins', plugin, 'config.html')).read() return render_template_string(config) abort(404) elif request.method == 'POST': for k, v in request.form.items(): if k == "nonce": continue utils.set_config(k, v) return '1'
def admin_css(): if request.method == 'POST': css = request.form['css'] css = set_config('css', css) print css return "1" return "0"
def test_get_config_and_set_config(): """Does get_config and set_config work properly""" app = create_ctfd() with app.app_context(): assert get_config('setup') == True config = set_config('TEST_CONFIG_ENTRY', 'test_config_entry') assert config.value == 'test_config_entry' assert get_config('TEST_CONFIG_ENTRY') == 'test_config_entry'
def admin_css(): if request.method == 'POST': css = request.form['css'] css = utils.set_config('css', css) with app.app_context(): cache.clear() return '1' return '0'
def test_sendmail_with_smtp(mock_smtp): """Does sendmail work properly with simple SMTP mail servers""" from email.mime.text import MIMEText app = create_ctfd() with app.app_context(): set_config('mail_server', 'localhost') set_config('mail_port', 25) set_config('mail_username', 'username') set_config('mail_password', 'password') from_addr = get_config('mailfrom_addr') or app.config.get('MAILFROM_ADDR') to_addr = '*****@*****.**' msg = 'this is a test' sendmail(to_addr, msg) ctf_name = get_config('ctf_name') email_msg = MIMEText(msg) email_msg['Subject'] = "Message from {0}".format(ctf_name) email_msg['From'] = from_addr email_msg['To'] = to_addr mock_smtp.return_value.sendmail.assert_called_once_with(from_addr, [to_addr], email_msg.as_string())
def test_user_can_confirm_email(mock_smtp): """Test that a user is capable of confirming their email address""" app = create_ctfd() with app.app_context(), freeze_time("2012-01-14 03:21:34"): # Set CTFd to only allow confirmed users and send emails set_config('verify_emails', True) set_config('mail_server', 'localhost') set_config('mail_port', 25) set_config('mail_useauth', True) set_config('mail_username', 'username') set_config('mail_password', 'password') register_user(app, name="user1", email="*****@*****.**") # Teams are not verified by default user = Users.query.filter_by(email='*****@*****.**').first() assert user.verified is False client = login_as_user(app, name="user1", password="******") r = client.get('http://localhost/confirm') assert "Need to resend the confirmation email?" in r.get_data( as_text=True) # smtp.sendmail was called mock_smtp.return_value.sendmail.assert_called() with client.session_transaction() as sess: data = {"nonce": sess.get('nonce')} r = client.post('http://localhost/confirm', data=data) assert "confirmation email has been resent" in r.get_data( as_text=True) r = client.get('/challenges') assert r.location == "http://localhost/confirm" # We got redirected to /confirm r = client.get('http://localhost/confirm/' + serialize('*****@*****.**')) assert r.location == 'http://localhost/challenges' # The team is now verified user = Users.query.filter_by(email='*****@*****.**').first() assert user.verified is True r = client.get('http://localhost/confirm') assert r.location == "http://localhost/settings" destroy_ctfd(app)
def admin_config(): if request.method == "POST": start = None end = None freeze = None if request.form.get('start'): start = int(request.form['start']) if request.form.get('end'): end = int(request.form['end']) if request.form.get('freeze'): freeze = int(request.form['freeze']) try: view_challenges_unregistered = bool( request.form.get('view_challenges_unregistered', None)) view_scoreboard_if_authed = bool( request.form.get('view_scoreboard_if_authed', None)) hide_scores = bool(request.form.get('hide_scores', None)) prevent_registration = bool( request.form.get('prevent_registration', None)) prevent_name_change = bool( request.form.get('prevent_name_change', None)) view_after_ctf = bool(request.form.get('view_after_ctf', None)) verify_emails = bool(request.form.get('verify_emails', None)) mail_tls = bool(request.form.get('mail_tls', None)) mail_ssl = bool(request.form.get('mail_ssl', None)) mail_useauth = bool(request.form.get('mail_useauth', None)) except (ValueError, TypeError): view_challenges_unregistered = None view_scoreboard_if_authed = None hide_scores = None prevent_registration = None prevent_name_change = None view_after_ctf = None verify_emails = None mail_tls = None mail_ssl = None mail_useauth = None finally: view_challenges_unregistered = utils.set_config( 'view_challenges_unregistered', view_challenges_unregistered) view_scoreboard_if_authed = utils.set_config( 'view_scoreboard_if_authed', view_scoreboard_if_authed) hide_scores = utils.set_config('hide_scores', hide_scores) prevent_registration = utils.set_config('prevent_registration', prevent_registration) prevent_name_change = utils.set_config('prevent_name_change', prevent_name_change) view_after_ctf = utils.set_config('view_after_ctf', view_after_ctf) verify_emails = utils.set_config('verify_emails', verify_emails) mail_tls = utils.set_config('mail_tls', mail_tls) mail_ssl = utils.set_config('mail_ssl', mail_ssl) mail_useauth = utils.set_config('mail_useauth', mail_useauth) mail_server = utils.set_config("mail_server", request.form.get('mail_server', None)) mail_port = utils.set_config("mail_port", request.form.get('mail_port', None)) mail_username = utils.set_config( "mail_username", request.form.get('mail_username', None)) mail_password = utils.set_config( "mail_password", request.form.get('mail_password', None)) ctf_name = utils.set_config("ctf_name", request.form.get('ctf_name', None)) ctf_theme = utils.set_config("ctf_theme", request.form.get('ctf_theme', None)) mailfrom_addr = utils.set_config( "mailfrom_addr", request.form.get('mailfrom_addr', None)) mg_base_url = utils.set_config("mg_base_url", request.form.get('mg_base_url', None)) mg_api_key = utils.set_config("mg_api_key", request.form.get('mg_api_key', None)) db_freeze = utils.set_config("freeze", freeze) 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() db.session.close() with app.app_context(): cache.clear() return redirect(url_for('admin.admin_config')) with app.app_context(): cache.clear() ctf_name = utils.get_config('ctf_name') ctf_theme = utils.get_config('ctf_theme') hide_scores = utils.get_config('hide_scores') mail_server = utils.get_config('mail_server') mail_port = utils.get_config('mail_port') mail_username = utils.get_config('mail_username') mail_password = utils.get_config('mail_password') mailfrom_addr = utils.get_config('mailfrom_addr') mg_api_key = utils.get_config('mg_api_key') mg_base_url = utils.get_config('mg_base_url') view_after_ctf = utils.get_config('view_after_ctf') start = utils.get_config('start') end = utils.get_config('end') freeze = utils.get_config('freeze') mail_tls = utils.get_config('mail_tls') mail_ssl = utils.get_config('mail_ssl') mail_useauth = utils.get_config('mail_useauth') view_challenges_unregistered = utils.get_config( 'view_challenges_unregistered') view_scoreboard_if_authed = utils.get_config('view_scoreboard_if_authed') prevent_registration = utils.get_config('prevent_registration') prevent_name_change = utils.get_config('prevent_name_change') verify_emails = utils.get_config('verify_emails') db.session.commit() db.session.close() themes = utils.get_themes() themes.remove(ctf_theme) return render_template( 'admin/config.html', ctf_name=ctf_name, ctf_theme_config=ctf_theme, start=start, end=end, freeze=freeze, hide_scores=hide_scores, mail_server=mail_server, mail_port=mail_port, mail_useauth=mail_useauth, mail_username=mail_username, mail_password=mail_password, mail_tls=mail_tls, mail_ssl=mail_ssl, view_challenges_unregistered=view_challenges_unregistered, view_scoreboard_if_authed=view_scoreboard_if_authed, prevent_registration=prevent_registration, mailfrom_addr=mailfrom_addr, mg_base_url=mg_base_url, mg_api_key=mg_api_key, prevent_name_change=prevent_name_change, verify_emails=verify_emails, view_after_ctf=view_after_ctf, themes=themes)
def setup(): # with app.app_context(): # admin = Teams.query.filter_by(admin=True).first() if not is_setup(): if not session.get('nonce'): session['nonce'] = sha512(os.urandom(10)) if request.method == 'POST': ctf_name = request.form['ctf_name'] ctf_name = set_config('ctf_name', ctf_name) ## CSS css = set_config('start', '') ## Admin user name = request.form['name'] email = request.form['email'] password = request.form['password'] admin = Teams(name, email, password) admin.admin = True admin.banned = True #max attempts per challenge max_tries = set_config("max_tries",0) ## Start time start = set_config('start', None) end = set_config('end', None) ## Challenges cannot be viewed by unregistered users view_challenges_unregistered = set_config('view_challenges_unregistered', None) ## Allow/Disallow registration prevent_registration = set_config('prevent_registration', None) ## Verify emails verify_emails = set_config('verify_emails', None) mail_server = set_config('mail_server', None) mail_port = set_config('mail_port', None) mail_tls = set_config('mail_tls', None) mail_ssl = set_config('mail_ssl', None) mail_username = set_config('mail_username', None) mail_password = set_config('mail_password', None) setup = set_config('setup', True) db.session.add(page) db.session.add(admin) db.session.commit() app.setup = False return redirect('/') return render_template('setup.html', nonce=session.get('nonce')) return redirect('/')
def create_app(config='CTFd.config.Config'): app = CTFdFlask(__name__) with app.app_context(): app.config.from_object(config) theme_loader = ThemeLoader(os.path.join(app.root_path, 'themes'), followlinks=True) app.jinja_loader = theme_loader from CTFd.models import db, Teams, Solves, Challenges, WrongKeys, Keys, Tags, Files, Tracking url = make_url(app.config['SQLALCHEMY_DATABASE_URI']) if url.drivername == 'postgres': url.drivername = 'postgresql' if url.drivername.startswith('mysql'): url.query['charset'] = 'utf8mb4' # Creates database if the database database does not exist if not database_exists(url): if url.drivername.startswith('mysql'): create_database(url, encoding='utf8mb4') else: create_database(url) # This allows any changes to the SQLALCHEMY_DATABASE_URI to get pushed back in # This is mostly so we can force MySQL's charset app.config['SQLALCHEMY_DATABASE_URI'] = str(url) # Register database db.init_app(app) # Register Flask-Migrate migrate.init_app(app, db) # Alembic sqlite support is lacking so we should just create_all anyway if url.drivername.startswith('sqlite'): db.create_all() else: if len(db.engine.table_names()) == 0: # This creates tables instead of db.create_all() # Allows migrations to happen properly migrate_upgrade() elif 'alembic_version' not in db.engine.table_names(): # There is no alembic_version because CTFd is from before it had migrations # Stamp it to the base migration if confirm_upgrade(): migrate_stamp(revision='cb3cfcc47e2f') run_upgrade() else: exit() app.db = db app.VERSION = __version__ cache.init_app(app) app.cache = cache update_check(force=True) version = utils.get_config('ctf_version') # Upgrading from an older version of CTFd if version and (StrictVersion(version) < StrictVersion(__version__)): if confirm_upgrade(): run_upgrade() else: exit() if not version: utils.set_config('ctf_version', __version__) if not utils.get_config('ctf_theme'): utils.set_config('ctf_theme', 'core') from CTFd.views import views from CTFd.challenges import challenges from CTFd.scoreboard import scoreboard from CTFd.auth import auth from CTFd.admin import admin, admin_statistics, admin_challenges, admin_pages, admin_scoreboard, admin_keys, admin_teams from CTFd.utils import init_utils, init_errors, init_logs init_utils(app) init_errors(app) init_logs(app) app.register_blueprint(views) app.register_blueprint(challenges) app.register_blueprint(scoreboard) app.register_blueprint(auth) app.register_blueprint(admin) app.register_blueprint(admin_statistics) app.register_blueprint(admin_challenges) app.register_blueprint(admin_teams) app.register_blueprint(admin_scoreboard) app.register_blueprint(admin_keys) app.register_blueprint(admin_pages) from CTFd.plugins import init_plugins init_plugins(app) return app
def test_api_user_send_email(): """Can an admin post /api/v1/users/<user_id>/email""" app = create_ctfd() with app.app_context(): register_user(app) with login_as_user(app) as client: r = client.post("/api/v1/users/2/email", json={"text": "email should get rejected"}) assert r.status_code == 403 with login_as_user(app, "admin") as admin: r = admin.post("/api/v1/users/2/email", json={"text": "email should be accepted"}) assert r.get_json() == { "success": False, "errors": { "": ["Email settings not configured"] }, } assert r.status_code == 400 set_config("verify_emails", True) set_config("mail_server", "localhost") set_config("mail_port", 25) set_config("mail_useauth", True) set_config("mail_username", "username") set_config("mail_password", "password") with login_as_user(app, "admin") as admin: r = admin.post("/api/v1/users/2/email", json={"text": ""}) assert r.get_json() == { "success": False, "errors": { "text": ["Email text cannot be empty"] }, } assert r.status_code == 400 with login_as_user(app, "admin") as admin: r = admin.post("/api/v1/users/2/email", json={"text": "email should be accepted"}) assert r.status_code == 200 destroy_ctfd(app)
def import_ctf(backup, erase=True): if not zipfile.is_zipfile(backup): raise zipfile.BadZipfile if erase: drop_database() create_database() # We explicitly do not want to upgrade or stamp here. # The import will have this information. side_db = dataset.connect(get_app_config("SQLALCHEMY_DATABASE_URI")) sqlite = get_app_config("SQLALCHEMY_DATABASE_URI").startswith("sqlite") postgres = get_app_config("SQLALCHEMY_DATABASE_URI").startswith("postgres") backup = zipfile.ZipFile(backup) members = backup.namelist() max_content_length = get_app_config("MAX_CONTENT_LENGTH") for f in members: if f.startswith("/") or ".." in f: # Abort on malicious zip files raise zipfile.BadZipfile info = backup.getinfo(f) if max_content_length: if info.file_size > max_content_length: raise zipfile.LargeZipFile first = [ "db/teams.json", "db/users.json", "db/challenges.json", "db/dynamic_challenge.json", "db/flags.json", "db/hints.json", "db/unlocks.json", "db/awards.json", "db/tags.json", "db/submissions.json", "db/solves.json", "db/files.json", "db/notifications.json", "db/pages.json", "db/tracking.json", "db/config.json", ] for item in first: if item in members: members.remove(item) members = first + members alembic_version = json.loads( backup.open( "db/alembic_version.json").read())["results"][0]["version_num"] upgrade(revision=alembic_version) # Create tables created by plugins try: app.db.create_all() except OperationalError as e: if not postgres: raise e else: print("Allowing error during app.db.create_all() due to Postgres") members.remove("db/alembic_version.json") for member in members: if member.startswith("db/"): table_name = member[3:-5] try: # Try to open a file but skip if it doesn't exist. data = backup.open(member).read() except KeyError: continue if data: table = side_db[table_name] saved = json.loads(data) for entry in saved["results"]: # This is a hack to get SQLite to properly accept datetime values from dataset # See Issue #246 if sqlite: for k, v in entry.items(): if isinstance(v, six.string_types): match = re.match( r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d", v) if match: entry[k] = datetime.datetime.strptime( v, "%Y-%m-%dT%H:%M:%S.%f") continue match = re.match( r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}", v) if match: entry[k] = datetime.datetime.strptime( v, "%Y-%m-%dT%H:%M:%S") continue # From v2.0.0 to v2.1.0 requirements could have been a string or JSON because of a SQLAlchemy issue # This is a hack to ensure we can still accept older exports. See #867 if member in ( "db/challenges.json", "db/hints.json", "db/awards.json", ): requirements = entry.get("requirements") if requirements and isinstance(requirements, six.string_types): entry["requirements"] = json.loads(requirements) try: table.insert(entry) except ProgrammingError: # MariaDB does not like JSON objects and prefers strings because it internally # represents JSON with LONGTEXT. # See Issue #973 requirements = entry.get("requirements") if requirements and isinstance(requirements, dict): entry["requirements"] = json.dumps(requirements) table.insert(entry) db.session.commit() if postgres: # This command is to set the next primary key ID for the re-inserted tables in Postgres. However, # this command is very difficult to translate into SQLAlchemy code. Because Postgres is not # officially supported, no major work will go into this functionality. # https://stackoverflow.com/a/37972960 if '"' not in table_name and "'" not in table_name: query = "SELECT setval(pg_get_serial_sequence('{table_name}', 'id'), coalesce(max(id)+1,1), false) FROM \"{table_name}\"".format( # nosec table_name=table_name) side_db.engine.execute(query) else: raise Exception( "Table name {table_name} contains quotes".format( table_name=table_name)) # Extracting files files = [f for f in backup.namelist() if f.startswith("uploads/")] uploader = get_uploader() for f in files: filename = f.split(os.sep, 1) if len(filename ) < 2: # just an empty uploads directory (e.g. uploads/) continue filename = filename[ 1] # Get the second entry in the list (the actual filename) source = backup.open(f) uploader.store(fileobj=source, filename=filename) # Alembic sqlite support is lacking so we should just create_all anyway try: upgrade(revision="head") except (CommandError, RuntimeError, SystemExit): app.db.create_all() stamp() # Invalidate all cached data cache.clear() # Set default theme in case the current instance or the import does not provide it set_config("ctf_theme", "core")
def create_app(config="CTFd.config.Config"): app = CTFdFlask(__name__) with app.app_context(): app.config.from_object(config) app.theme_loader = ThemeLoader(os.path.join(app.root_path, "themes"), followlinks=True) # Weird nested solution for accessing plugin templates app.plugin_loader = jinja2.PrefixLoader({ "plugins": jinja2.FileSystemLoader(searchpath=os.path.join( app.root_path, "plugins"), followlinks=True) }) # Load from themes first but fallback to loading from the plugin folder app.jinja_loader = jinja2.ChoiceLoader( [app.theme_loader, app.plugin_loader]) from CTFd.models import ( # noqa: F401 db, Teams, Solves, Challenges, Fails, Flags, Tags, Files, Tracking, ) url = create_database() # This allows any changes to the SQLALCHEMY_DATABASE_URI to get pushed back in # This is mostly so we can force MySQL's charset app.config["SQLALCHEMY_DATABASE_URI"] = str(url) # Register database db.init_app(app) # Register Flask-Migrate migrations.init_app(app, db) # Alembic sqlite support is lacking so we should just create_all anyway if url.drivername.startswith("sqlite"): # Enable foreign keys for SQLite. This must be before the # db.create_all call because tests use the in-memory SQLite # database (each connection, including db creation, is a new db). # https://docs.sqlalchemy.org/en/13/dialects/sqlite.html#foreign-key-support from sqlalchemy.engine import Engine from sqlalchemy import event @event.listens_for(Engine, "connect") def set_sqlite_pragma(dbapi_connection, connection_record): cursor = dbapi_connection.cursor() cursor.execute("PRAGMA foreign_keys=ON") cursor.close() db.create_all() stamp_latest_revision() else: # This creates tables instead of db.create_all() # Allows migrations to happen properly upgrade() from CTFd.models import ma ma.init_app(app) app.db = db app.VERSION = __version__ app.CHANNEL = __channel__ from CTFd.cache import cache cache.init_app(app) app.cache = cache reverse_proxy = app.config.get("REVERSE_PROXY") if reverse_proxy: if type(reverse_proxy) is str and "," in reverse_proxy: proxyfix_args = [int(i) for i in reverse_proxy.split(",")] app.wsgi_app = ProxyFix(app.wsgi_app, *proxyfix_args) else: app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_port=1, x_prefix=1) version = utils.get_config("ctf_version") # Upgrading from an older version of CTFd if version and (StrictVersion(version) < StrictVersion(__version__)): if confirm_upgrade(): run_upgrade() else: exit() if not version: utils.set_config("ctf_version", __version__) if not utils.get_config("ctf_theme"): utils.set_config("ctf_theme", "core") update_check(force=True) init_request_processors(app) init_template_filters(app) init_template_globals(app) # Importing here allows tests to use sensible names (e.g. api instead of api_bp) from CTFd.views import views from CTFd.teams import teams from CTFd.users import users from CTFd.challenges import challenges from CTFd.training import training from CTFd.scoreboard import scoreboard from CTFd.auth import auth from CTFd.admin import admin from CTFd.api import api from CTFd.events import events from CTFd.errors import page_not_found, forbidden, general_error, gateway_error app.register_blueprint(views) app.register_blueprint(teams) app.register_blueprint(users) app.register_blueprint(challenges) app.register_blueprint(training) app.register_blueprint(scoreboard) app.register_blueprint(auth) app.register_blueprint(api) app.register_blueprint(events) app.register_blueprint(admin) app.register_error_handler(404, page_not_found) app.register_error_handler(403, forbidden) app.register_error_handler(500, general_error) app.register_error_handler(502, gateway_error) init_logs(app) init_events(app) init_plugins(app) return app
def admin_config(): if request.method == "POST": start = None end = None freeze = None if request.form.get('start'): start = int(request.form['start']) if request.form.get('end'): end = int(request.form['end']) if request.form.get('freeze'): freeze = int(request.form['freeze']) try: view_challenges_unregistered = bool(request.form.get('view_challenges_unregistered', None)) view_scoreboard_if_authed = bool(request.form.get('view_scoreboard_if_authed', None)) hide_scores = bool(request.form.get('hide_scores', None)) prevent_registration = bool(request.form.get('prevent_registration', None)) prevent_name_change = bool(request.form.get('prevent_name_change', None)) view_after_ctf = bool(request.form.get('view_after_ctf', None)) verify_emails = bool(request.form.get('verify_emails', None)) mail_tls = bool(request.form.get('mail_tls', None)) mail_ssl = bool(request.form.get('mail_ssl', None)) except (ValueError, TypeError): view_challenges_unregistered = None view_scoreboard_if_authed = None hide_scores = None prevent_registration = None prevent_name_change = None view_after_ctf = None verify_emails = None mail_tls = None mail_ssl = None finally: view_challenges_unregistered = utils.set_config('view_challenges_unregistered', view_challenges_unregistered) view_scoreboard_if_authed = utils.set_config('view_scoreboard_if_authed', view_scoreboard_if_authed) hide_scores = utils.set_config('hide_scores', hide_scores) prevent_registration = utils.set_config('prevent_registration', prevent_registration) prevent_name_change = utils.set_config('prevent_name_change', prevent_name_change) view_after_ctf = utils.set_config('view_after_ctf', view_after_ctf) verify_emails = utils.set_config('verify_emails', verify_emails) mail_tls = utils.set_config('mail_tls', mail_tls) mail_ssl = utils.set_config('mail_ssl', mail_ssl) mail_server = utils.set_config("mail_server", request.form.get('mail_server', None)) mail_port = utils.set_config("mail_port", request.form.get('mail_port', None)) mail_username = utils.set_config("mail_username", request.form.get('mail_username', None)) mail_password = utils.set_config("mail_password", request.form.get('mail_password', None)) ctf_name = utils.set_config("ctf_name", request.form.get('ctf_name', None)) ctf_theme = utils.set_config("ctf_theme", request.form.get('ctf_theme', None)) mailfrom_addr = utils.set_config("mailfrom_addr", request.form.get('mailfrom_addr', None)) mg_base_url = utils.set_config("mg_base_url", request.form.get('mg_base_url', None)) mg_api_key = utils.set_config("mg_api_key", request.form.get('mg_api_key', None)) db_freeze = utils.set_config("freeze", freeze) 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() db.session.close() with app.app_context(): cache.clear() return redirect(url_for('admin.admin_config')) with app.app_context(): cache.clear() ctf_name = utils.get_config('ctf_name') ctf_theme = utils.get_config('ctf_theme') hide_scores = utils.get_config('hide_scores') mail_server = utils.get_config('mail_server') mail_port = utils.get_config('mail_port') mail_username = utils.get_config('mail_username') mail_password = utils.get_config('mail_password') mailfrom_addr = utils.get_config('mailfrom_addr') mg_api_key = utils.get_config('mg_api_key') mg_base_url = utils.get_config('mg_base_url') view_after_ctf = utils.get_config('view_after_ctf') start = utils.get_config('start') end = utils.get_config('end') freeze = utils.get_config('freeze') mail_tls = utils.get_config('mail_tls') mail_ssl = utils.get_config('mail_ssl') view_challenges_unregistered = utils.get_config('view_challenges_unregistered') view_scoreboard_if_authed = utils.get_config('view_scoreboard_if_authed') prevent_registration = utils.get_config('prevent_registration') prevent_name_change = utils.get_config('prevent_name_change') verify_emails = utils.get_config('verify_emails') db.session.commit() db.session.close() themes = utils.get_themes() themes.remove(ctf_theme) return render_template('admin/config.html', ctf_name=ctf_name, ctf_theme_config=ctf_theme, start=start, end=end, freeze=freeze, hide_scores=hide_scores, mail_server=mail_server, mail_port=mail_port, mail_username=mail_username, mail_password=mail_password, mail_tls=mail_tls, mail_ssl=mail_ssl, view_challenges_unregistered=view_challenges_unregistered, view_scoreboard_if_authed=view_scoreboard_if_authed, prevent_registration=prevent_registration, mailfrom_addr=mailfrom_addr, mg_base_url=mg_base_url, mg_api_key=mg_api_key, prevent_name_change=prevent_name_change, verify_emails=verify_emails, view_after_ctf=view_after_ctf, themes=themes)
def create_app(config='CTFd.config.Config'): app = Flask(__name__) babel = Babel(app) app.config['BABEL_DEFAULT_LOCALE'] = 'hi' with app.app_context(): app.config.from_object(config) app.jinja_loader = ThemeLoader(os.path.join(app.root_path, app.template_folder), followlinks=True) from CTFd.models import db, Teams, Solves, Challenges, WrongKeys, Keys, Tags, Files, Tracking url = make_url(app.config['SQLALCHEMY_DATABASE_URI']) if url.drivername == 'postgres': url.drivername = 'postgresql' db.init_app(app) try: if not (url.drivername.startswith('sqlite') or database_exists(url)): create_database(url) db.create_all() except OperationalError: db.create_all() except ProgrammingError: ## Database already exists pass else: db.create_all() app.db = db migrate.init_app(app, db) app.config["CACHE_TYPE"] = "null" cache.init_app(app) app.cache = cache version = utils.get_config('ctf_version') if not version: ## Upgrading from an unversioned CTFd utils.set_config('ctf_version', __version__) if version and (StrictVersion(version) < StrictVersion(__version__) ): ## Upgrading from an older version of CTFd print("/*\\ CTFd has updated and must update the database! /*\\") print("/*\\ Please backup your database before proceeding! /*\\") print( "/*\\ CTFd maintainers are not responsible for any data loss! /*\\" ) if input('Run database migrations (Y/N)').lower().strip() == 'y': migrate_stamp() migrate_upgrade() utils.set_config('ctf_version', __version__) else: print('/*\\ Ignored database migrations... /*\\') exit() if not utils.get_config('ctf_theme'): utils.set_config('ctf_theme', 'original') from CTFd.views import views from CTFd.challenges import challenges from CTFd.scoreboard import scoreboard from CTFd.auth import auth from CTFd.admin import admin, admin_statistics, admin_challenges, admin_pages, admin_scoreboard, admin_containers, admin_keys, admin_teams from CTFd.utils import init_utils, init_errors, init_logs init_utils(app) init_errors(app) init_logs(app) app.register_blueprint(views) app.register_blueprint(challenges) app.register_blueprint(scoreboard) app.register_blueprint(auth) app.register_blueprint(admin) app.register_blueprint(admin_statistics) app.register_blueprint(admin_challenges) app.register_blueprint(admin_teams) app.register_blueprint(admin_scoreboard) app.register_blueprint(admin_keys) app.register_blueprint(admin_containers) app.register_blueprint(admin_pages) from CTFd.plugins import init_plugins init_plugins(app) return app
def admin_config(): if request.method == "POST": try: start = int(request.form['start']) end = int(request.form['end']) except (ValueError, TypeError): start = None end = None try: view_challenges_unregistered = bool(request.form.get('view_challenges_unregistered', None)) prevent_registration = bool(request.form.get('prevent_registration', None)) except (ValueError, TypeError): view_challenges_unregistered = None prevent_registration = None ctf_name = set_config("ctf_name", request.form.get('ctf_name', None)) mg_api_key = set_config("mg_api_key", request.form.get('mg_api_key', None)) do_api_key = set_config("do_api_key", request.form.get('do_api_key', None)) 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_view_challenges_unregistered = Config.query.filter_by(key='view_challenges_unregistered').first() db_view_challenges_unregistered.value = view_challenges_unregistered db_prevent_registration = Config.query.filter_by(key='prevent_registration').first() db_prevent_registration.value = prevent_registration db.session.add(db_start) db.session.add(db_end) db.session.add(db_view_challenges_unregistered) db.session.add(db_prevent_registration) db.session.commit() return redirect('/admin/config') ctf_name = get_config('ctf_name') if not ctf_name: set_config('do_api_key', None) mg_api_key = get_config('do_api_key') if not mg_api_key: set_config('do_api_key', None) do_api_key = get_config('do_api_key') if not do_api_key: set_config('do_api_key', None) start = get_config('start') if not start: set_config('start', None) end = get_config('end') if not end: set_config('end', None) view_challenges_unregistered = get_config('view_challenges_unregistered') == '1' if not view_challenges_unregistered: set_config('view_challenges_unregistered', None) prevent_registration = get_config('prevent_registration') == '1' if not prevent_registration: set_config('prevent_registration', None) db.session.commit() db.session.close() return render_template('admin/config.html', ctf_name=ctf_name, start=start, end=end, view_challenges_unregistered=view_challenges_unregistered, prevent_registration=prevent_registration, do_api_key=do_api_key, mg_api_key=mg_api_key)
def test_verify_email(mock_smtp): """Does verify_email send emails""" app = create_ctfd() with app.app_context(): set_config("mail_server", "localhost") set_config("mail_port", 25) set_config("mail_useauth", True) set_config("mail_username", "username") set_config("mail_password", "password") set_config("verify_emails", True) from_addr = get_config("mailfrom_addr") or app.config.get( "MAILFROM_ADDR") to_addr = "*****@*****.**" verify_email_address(to_addr) # This is currently not actually validated msg = ( "Please click the following link to confirm" " your email address for CTFd:" " http://localhost/confirm/InVzZXJAdXNlci5jb20i.TxD0vg.28dY_Gzqb1TH9nrcE_H7W8YFM-U" ) ctf_name = get_config("ctf_name") email_msg = MIMEText(msg) email_msg["Subject"] = "Message from {0}".format(ctf_name) email_msg["From"] = from_addr email_msg["To"] = to_addr # Need to freeze time to predict the value of the itsdangerous token. # 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 load(app): ######################## # Plugin Configuration # ######################## authentication_url_prefix = "/auth" oauth_client_id = utils.get_app_config('OAUTHLOGIN_CLIENT_ID') oauth_client_secret = utils.get_app_config('OAUTHLOGIN_CLIENT_SECRET') oauth_provider = utils.get_app_config('OAUTHLOGIN_PROVIDER') create_missing_user = utils.get_app_config( 'OAUTHLOGIN_CREATE_MISSING_USER') ################## # User Functions # ################## def retrieve_user_from_database(username): user = Users.query.filter_by(email=username).first() if user is not None: log( 'logins', "[{date}] {ip} - " + user.name + " - OAuth2 bridged user found") return user def create_user(username, displayName): with app.app_context(): log( 'logins', "[{date}] {ip} - " + user.name + " - No OAuth2 bridged user found, creating user") user = Users(email=username, name=displayName.strip()) db.session.add(user) db.session.commit() db.session.flush() return user def create_or_get_user(username, displayName): user = retrieve_user_from_database(username) if user is not None: return user if create_missing_user: return create_user(username, displayName) else: log( 'logins', "[{date}] {ip} - " + user.name + " - No OAuth2 bridged user found and not configured to create missing users" ) return None ########################## # Provider Configuration # ########################## provider_blueprints = { 'azure': lambda: flask_dance.contrib.azure.make_azure_blueprint( login_url='/azure', client_id=oauth_client_id, client_secret=oauth_client_secret, redirect_url=authentication_url_prefix + "/azure/confirm"), 'github': lambda: flask_dance.contrib.github.make_github_blueprint( login_url='/github', client_id=oauth_client_id, client_secret=oauth_client_secret, redirect_url=authentication_url_prefix + "/github/confirm") } def get_azure_user(): user_info = flask_dance.contrib.azure.azure.get("/v1.0/me").json() return create_or_get_user(username=user_info["userPrincipalName"], displayName=user_info["displayName"]) def get_github_user(): user_info = flask_dance.contrib.github.github.get("/user").json() return create_or_get_user(username=user_info["email"], displayName=user_info["name"]) provider_users = { 'azure': lambda: get_azure_user(), 'github': lambda: get_github_user() } provider_blueprint = provider_blueprints[oauth_provider]( ) # Resolved lambda ####################### # Blueprint Functions # ####################### @provider_blueprint.route('/<string:auth_provider>/confirm', methods=['GET']) def confirm_auth_provider(auth_provider): if not provider_users.has_key(auth_provider): return redirect('/') provider_user = provider_users[oauth_provider]() # Resolved lambda session.regenerate() if provider_user is not None: login_user(provider_user) return redirect('/') app.register_blueprint(provider_blueprint, url_prefix=authentication_url_prefix) ############################### # Application Reconfiguration # ############################### # ('', 204) is "No Content" code set_config('registration_visibility', False) app.view_functions['auth.login'] = lambda: redirect( authentication_url_prefix + "/" + oauth_provider) app.view_functions['auth.register'] = lambda: ('', 204) app.view_functions['auth.reset_password'] = lambda: ('', 204) app.view_functions['auth.confirm'] = lambda: ('', 204)
def test_successful_registration_email(mock_smtp): """Does successful_registration_notification send emails""" app = create_ctfd() with app.app_context(): set_config("mail_server", "localhost") set_config("mail_port", 25) set_config("mail_useauth", True) set_config("mail_username", "username") set_config("mail_password", "password") set_config("verify_emails", True) ctf_name = get_config("ctf_name") from_addr = get_config("mailfrom_addr") or app.config.get( "MAILFROM_ADDR") from_addr = "{} <{}>".format(ctf_name, from_addr) to_addr = "*****@*****.**" successful_registration_notification(to_addr) msg = "You've successfully registered for CTFd!" email_msg = MIMEText(msg) email_msg["Subject"] = "Successfully registered for {ctf_name}".format( ctf_name=ctf_name) email_msg["From"] = from_addr email_msg["To"] = to_addr # Need to freeze time to predict the value of the itsdangerous token. # 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 import_ctf(backup, erase=True): if not zipfile.is_zipfile(backup): raise zipfile.BadZipfile backup = zipfile.ZipFile(backup) members = backup.namelist() max_content_length = get_app_config("MAX_CONTENT_LENGTH") for f in members: if f.startswith("/") or ".." in f: # Abort on malicious zip files raise zipfile.BadZipfile info = backup.getinfo(f) if max_content_length: if info.file_size > max_content_length: raise zipfile.LargeZipFile # Get list of directories in zipfile member_dirs = [os.path.split(m)[0] for m in members if "/" in m] if "db" not in member_dirs: raise Exception( 'CTFd couldn\'t find the "db" folder in this backup. ' "The backup may be malformed or corrupted and the import process cannot continue." ) try: alembic_version = json.loads( backup.open("db/alembic_version.json").read()) alembic_version = alembic_version["results"][0]["version_num"] except Exception: raise Exception( "Could not determine appropriate database version. This backup cannot be automatically imported." ) # Check if the alembic version is from CTFd 1.x if alembic_version in ( "1ec4a28fe0ff", "2539d8b5082e", "7e9efd084c5a", "87733981ca0e", "a4e30c94c360", "c12d2a1b0926", "c7225db614c1", "cb3cfcc47e2f", "cbf5620f8e15", "d5a224bf5862", "d6514ec92738", "dab615389702", "e62fd69bd417", ): raise Exception( "The version of CTFd that this backup is from is too old to be automatically imported." ) if erase: # Clear out existing connections to release any locks db.session.close() db.engine.dispose() # Drop database and recreate it to get to a clean state drop_database() create_database() # We explicitly do not want to upgrade or stamp here. # The import will have this information. side_db = dataset.connect(get_app_config("SQLALCHEMY_DATABASE_URI")) sqlite = get_app_config("SQLALCHEMY_DATABASE_URI").startswith("sqlite") postgres = get_app_config("SQLALCHEMY_DATABASE_URI").startswith("postgres") try: if postgres: side_db.query("SET session_replication_role=replica;") else: side_db.query("SET FOREIGN_KEY_CHECKS=0;") except Exception: print("Failed to disable foreign key checks. Continuing.") first = [ "db/teams.json", "db/users.json", "db/challenges.json", "db/dynamic_challenge.json", "db/flags.json", "db/hints.json", "db/unlocks.json", "db/awards.json", "db/tags.json", "db/submissions.json", "db/solves.json", "db/files.json", "db/notifications.json", "db/pages.json", "db/tracking.json", "db/config.json", ] # We want to insert certain database tables first so we are specifying # the order with a list. The leftover tables are tables that are from a # plugin (more likely) or a table where we do not care about insertion order for item in first: if item in members: members.remove(item) # Upgrade the database to the point in time that the import was taken from migration_upgrade(revision=alembic_version) members.remove("db/alembic_version.json") # Combine the database insertion code into a function so that we can pause # insertion between official database tables and plugin tables def insertion(table_filenames): for member in table_filenames: if member.startswith("db/"): table_name = member[3:-5] try: # Try to open a file but skip if it doesn't exist. data = backup.open(member).read() except KeyError: continue if data: table = side_db[table_name] saved = json.loads(data) for entry in saved["results"]: # This is a hack to get SQLite to properly accept datetime values from dataset # See Issue #246 if sqlite: direct_table = get_class_by_tablename(table.name) for k, v in entry.items(): if isinstance(v, string_types): # We only want to apply this hack to columns that are expecting a datetime object try: is_dt_column = (type( getattr( direct_table, k).type) == sqltypes.DateTime) except AttributeError: is_dt_column = False # If the table is expecting a datetime, we should check if the string is one and convert it if is_dt_column: match = re.match( r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d", v, ) if match: entry[ k] = datetime.datetime.strptime( v, "%Y-%m-%dT%H:%M:%S.%f") continue match = re.match( r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}", v) if match: entry[ k] = datetime.datetime.strptime( v, "%Y-%m-%dT%H:%M:%S") continue # From v2.0.0 to v2.1.0 requirements could have been a string or JSON because of a SQLAlchemy issue # This is a hack to ensure we can still accept older exports. See #867 if member in ( "db/challenges.json", "db/hints.json", "db/awards.json", ): requirements = entry.get("requirements") if requirements and isinstance( requirements, string_types): entry["requirements"] = json.loads( requirements) try: table.insert(entry) except ProgrammingError: # MariaDB does not like JSON objects and prefers strings because it internally # represents JSON with LONGTEXT. # See Issue #973 requirements = entry.get("requirements") if requirements and isinstance(requirements, dict): entry["requirements"] = json.dumps( requirements) table.insert(entry) db.session.commit() if postgres: # This command is to set the next primary key ID for the re-inserted tables in Postgres. However, # this command is very difficult to translate into SQLAlchemy code. Because Postgres is not # officially supported, no major work will go into this functionality. # https://stackoverflow.com/a/37972960 if '"' not in table_name and "'" not in table_name: query = "SELECT setval(pg_get_serial_sequence('{table_name}', 'id'), coalesce(max(id)+1,1), false) FROM \"{table_name}\"".format( # nosec table_name=table_name) side_db.engine.execute(query) else: raise Exception( "Table name {table_name} contains quotes". format(table_name=table_name)) # Insert data from official tables insertion(first) # Create tables created by plugins try: # Run plugin migrations plugins = get_plugin_names() try: for plugin in plugins: revision = plugin_current(plugin_name=plugin) plugin_upgrade(plugin_name=plugin, revision=revision) finally: # Create tables that don't have migrations app.db.create_all() except OperationalError as e: if not postgres: raise e else: print("Allowing error during app.db.create_all() due to Postgres") # Insert data for plugin tables insertion(members) # Bring plugin tables up to head revision plugins = get_plugin_names() for plugin in plugins: plugin_upgrade(plugin_name=plugin) # Extracting files files = [f for f in backup.namelist() if f.startswith("uploads/")] uploader = get_uploader() for f in files: filename = f.split(os.sep, 1) if ( len(filename) < 2 or os.path.basename(filename[1]) == "" ): # just an empty uploads directory (e.g. uploads/) or any directory continue filename = filename[ 1] # Get the second entry in the list (the actual filename) source = backup.open(f) uploader.store(fileobj=source, filename=filename) # Alembic sqlite support is lacking so we should just create_all anyway try: migration_upgrade(revision="head") except (OperationalError, CommandError, RuntimeError, SystemExit, Exception): app.db.create_all() stamp_latest_revision() try: if postgres: side_db.query("SET session_replication_role=DEFAULT;") else: side_db.query("SET FOREIGN_KEY_CHECKS=1;") except Exception: print("Failed to enable foreign key checks. Continuing.") # Invalidate all cached data cache.clear() # Set default theme in case the current instance or the import does not provide it set_config("ctf_theme", "core") set_config("ctf_version", CTFD_VERSION)
def create_app(config='CTFd.config.Config'): app = CTFdFlask(__name__) with app.app_context(): app.config.from_object(config) theme_loader = ThemeLoader(os.path.join(app.root_path, 'themes'), followlinks=True) app.jinja_loader = theme_loader from CTFd.models import db, Teams, Solves, Challenges, Fails, Flags, Tags, Files, Tracking url = create_database() # This allows any changes to the SQLALCHEMY_DATABASE_URI to get pushed back in # This is mostly so we can force MySQL's charset app.config['SQLALCHEMY_DATABASE_URI'] = str(url) # Register database db.init_app(app) # Register Flask-Migrate migrations.init_app(app, db) # Alembic sqlite support is lacking so we should just create_all anyway if url.drivername.startswith('sqlite'): db.create_all() stamp() else: # This creates tables instead of db.create_all() # Allows migrations to happen properly upgrade() from CTFd.models import ma ma.init_app(app) app.db = db app.VERSION = __version__ from CTFd.cache import cache cache.init_app(app) app.cache = cache # If you have multiple workers you must have a shared cache socketio.init_app(app, async_mode=app.config.get('SOCKETIO_ASYNC_MODE'), message_queue=app.config.get('CACHE_REDIS_URL')) if app.config.get('REVERSE_PROXY'): app.wsgi_app = ProxyFix(app.wsgi_app) version = utils.get_config('ctf_version') # Upgrading from an older version of CTFd if version and (StrictVersion(version) < StrictVersion(__version__)): if confirm_upgrade(): run_upgrade() else: exit() if not version: utils.set_config('ctf_version', __version__) if not utils.get_config('ctf_theme'): utils.set_config('ctf_theme', 'core') update_check(force=True) init_request_processors(app) init_template_filters(app) init_template_globals(app) # Importing here allows tests to use sensible names (e.g. api instead of api_bp) from CTFd.views import views from CTFd.teams import teams from CTFd.users import users from CTFd.challenges import challenges from CTFd.scoreboard import scoreboard from CTFd.auth import auth from CTFd.admin import admin from CTFd.api import api from CTFd.events import events from CTFd.errors import page_not_found, forbidden, general_error, gateway_error app.register_blueprint(views) app.register_blueprint(teams) app.register_blueprint(users) app.register_blueprint(challenges) app.register_blueprint(scoreboard) app.register_blueprint(auth) app.register_blueprint(api) app.register_blueprint(events) app.register_blueprint(admin) app.register_error_handler(404, page_not_found) app.register_error_handler(403, forbidden) app.register_error_handler(500, general_error) app.register_error_handler(502, gateway_error) init_logs(app) init_plugins(app) return app
def test_challenges_under_view_after_ctf(): app = create_ctfd() with app.app_context(), freeze_time("2017-10-7"): 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) client = login_as_user(app) gen_challenge(app.db) gen_flag(app.db, challenge_id=1, content="flag") r = client.get("/challenges") assert r.status_code == 403 r = client.get("/api/v1/challenges") assert r.status_code == 403 assert r.get_json().get("data") is None r = client.get("/api/v1/challenges/1") assert r.status_code == 403 assert r.get_json().get("data") is None data = {"submission": "flag", "challenge_id": 1} r = client.post("/api/v1/challenges/attempt", json=data) assert r.status_code == 403 assert r.get_json().get("data") is None assert Solves.query.count() == 0 data = {"submission": "notflag", "challenge_id": 1} r = client.post("/api/v1/challenges/attempt", json=data) assert r.status_code == 403 assert r.get_json().get("data") is None assert Fails.query.count() == 0 set_config("view_after_ctf", True) r = client.get("/challenges") assert r.status_code == 200 r = client.get("/api/v1/challenges") assert r.status_code == 200 assert r.get_json()["data"][0]["id"] == 1 r = client.get("/api/v1/challenges/1") assert r.status_code == 200 assert r.get_json()["data"]["id"] == 1 data = {"submission": "flag", "challenge_id": 1} r = client.post("/api/v1/challenges/attempt", json=data) assert r.status_code == 200 assert r.get_json()["data"]["status"] == "correct" assert Solves.query.count() == 0 data = {"submission": "notflag", "challenge_id": 1} r = client.post("/api/v1/challenges/attempt", json=data) assert r.status_code == 200 assert r.get_json()["data"]["status"] == "incorrect" assert Fails.query.count() == 0 destroy_ctfd(app)
def setup(): # with app.app_context(): # admin = Teams.query.filter_by(admin=True).first() if not is_setup(): if not session.get('nonce'): session['nonce'] = sha512(os.urandom(10)) if request.method == 'POST': ctf_name = request.form['ctf_name'] ctf_name = set_config('ctf_name', ctf_name) ## CSS css = set_config('start', '') ## Admin user name = request.form['name'] email = request.form['email'] password = request.form['password'] admin = Teams(name, email, password) admin.admin = True admin.banned = True ## Index page page = Pages('index', """<div class="container main-container"> <img class="logo" src="{0}/static/original/img/logo.png" /> <h3 class="text-center"> Welcome to a cool CTF framework written by <a href="https://github.com/ColdHeat">Kevin Chung</a> of <a href="https://github.com/isislab">@isislab</a> </h3> <h4 class="text-center"> <a href="{0}/admin">Click here</a> to login and setup your CTF </h4> </div>""".format(request.script_root)) #max attempts per challenge max_tries = set_config("max_tries",0) ## Start time start = set_config('start', None) end = set_config('end', None) ## Challenges cannot be viewed by unregistered users view_challenges_unregistered = set_config('view_challenges_unregistered', None) ## Allow/Disallow registration prevent_registration = set_config('prevent_registration', None) ## Verify emails verify_emails = set_config('verify_emails', None) mail_server = set_config('mail_server', None) mail_port = set_config('mail_port', None) mail_tls = set_config('mail_tls', None) mail_ssl = set_config('mail_ssl', None) mail_username = set_config('mail_username', None) mail_password = set_config('mail_password', None) setup = set_config('setup', True) db.session.add(page) db.session.add(admin) db.session.commit() db.session.close() app.setup = False with app.app_context(): cache.clear() return redirect(url_for('views.static_html')) return render_template('setup.html', nonce=session.get('nonce')) return redirect(url_for('views.static_html'))
def test_user_can_reset_password(mock_smtp): """Test that a user is capable of resetting their password""" from email.mime.text import MIMEText app = create_ctfd() with app.app_context(), freeze_time("2012-01-14 03:21:34"): # Set CTFd to send emails set_config('mail_server', 'localhost') set_config('mail_port', 25) set_config('mail_useauth', True) set_config('mail_username', 'username') set_config('mail_password', 'password') # Create a user register_user(app, name="user1", email="*****@*****.**") with app.test_client() as client: client.get('/reset_password') # Build reset password data with client.session_transaction() as sess: data = {'nonce': sess.get('nonce'), 'email': '*****@*****.**'} # Issue the password reset request client.post('/reset_password', data=data) from_addr = get_config('mailfrom_addr') or app.config.get( 'MAILFROM_ADDR') to_addr = '*****@*****.**' # Build the email msg = ( """Did you initiate a password reset? Click the following link to reset """ """your password:\n\nhttp://localhost/reset_password/InVzZXIxIg.TxD0vg.-gvVg-KVy0RWkiclAE6JViv1I0M\n\n""" ) email_msg = MIMEText(msg) email_msg['Subject'] = "Message from CTFd" email_msg['From'] = from_addr email_msg['To'] = to_addr # Make sure that the reset password email is sent mock_smtp.return_value.sendmail.assert_called_with( from_addr, [to_addr], email_msg.as_string()) # Get user's original password user = Users.query.filter_by(email="*****@*****.**").first() # Build the POST data with client.session_transaction() as sess: data = {'nonce': sess.get('nonce'), 'password': '******'} # Do the password reset client.get( '/reset_password/InVzZXIxIg.TxD0vg.-gvVg-KVy0RWkiclAE6JViv1I0M' ) client.post( '/reset_password/InVzZXIxIg.TxD0vg.-gvVg-KVy0RWkiclAE6JViv1I0M', data=data) # Make sure that the user's password changed user = Users.query.filter_by(email="*****@*****.**").first() assert verify_password('passwordtwo', user.password) destroy_ctfd(app)
def test_api_challenges_solves_score_visibility(): """Can a user load /api/v1/challenges/<challenge_id>/solves if score_visibility is public/private/admin""" app = create_ctfd() with app.app_context(), freeze_time("2017-10-5"): 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 set_config("challenge_visibility", "public") set_config("score_visibility", "public") gen_challenge(app.db) with app.test_client() as client: r = client.get("/api/v1/challenges/1/solves") assert r.status_code == 200 set_config("challenge_visibility", "private") set_config("score_visibility", "private") register_user(app) private_client = login_as_user(app) r = private_client.get("/api/v1/challenges/1/solves") assert r.status_code == 200 set_config("score_visibility", "admin") admin = login_as_user(app, "admin", "password") r = admin.get("/api/v1/challenges/1/solves") assert r.status_code == 200 destroy_ctfd(app)
def test_user_can_confirm_email(mock_smtp): """Test that a user is capable of confirming their email address""" app = create_ctfd() with app.app_context(), freeze_time("2012-01-14 03:21:34"): # Set CTFd to only allow confirmed users and send emails set_config("verify_emails", True) set_config("mail_server", "localhost") set_config("mail_port", 25) set_config("mail_useauth", True) set_config("mail_username", "username") set_config("mail_password", "password") register_user(app, name="user1", email="*****@*****.**") # Teams are not verified by default user = Users.query.filter_by(email="*****@*****.**").first() assert user.verified is False client = login_as_user(app, name="user1", password="******") r = client.get("http://localhost/confirm") assert "We've sent a confirmation email" in r.get_data(as_text=True) # smtp send message function was called mock_smtp.return_value.send_message.assert_called() with client.session_transaction() as sess: data = {"nonce": sess.get("nonce")} r = client.post("http://localhost/confirm", data=data) assert "Confirmation email sent to" in r.get_data(as_text=True) r = client.get("/challenges") assert (r.location == "http://localhost/confirm" ) # We got redirected to /confirm r = client.get("http://localhost/confirm/" + serialize("*****@*****.**")) assert r.location == "http://localhost/challenges" # The team is now verified user = Users.query.filter_by(email="*****@*****.**").first() assert user.verified is True r = client.get("http://localhost/confirm") assert r.location == "http://localhost/settings" destroy_ctfd(app)
def setup(): # with app.app_context(): # admin = Teams.query.filter_by(admin=True).first() if not utils.is_setup(): if not session.get('nonce'): session['nonce'] = utils.sha512(os.urandom(10)) if request.method == 'POST': ctf_name = request.form['ctf_name'] ctf_name = utils.set_config('ctf_name', ctf_name) # CSS css = utils.set_config('start', '') # Admin user name = request.form['name'] email = request.form['email'] password = request.form['password'] admin = Teams(name, email, password) admin.admin = True admin.banned = True # Index page page = Pages( 'index', """<div class="container main-container index" id="index"> <img class="logo" src="static/original/img/logo.png" /> <h3 class="text-center"> <p>A cool CTF platform from (tom)<a href="https://ctfd.io">ctfd.io</a></p> <p>Follow us on social media:</p> <a href="https://twitter.com/ctfdio"><i class="fa fa-twitter fa-2x" aria-hidden="true"></i></a> <a href="https://facebook.com/ctfdio"><i class="fa fa-facebook-official fa-2x" aria-hidden="true"></i></a> <a href="https://github.com/ctfd"><i class="fa fa-github fa-2x" aria-hidden="true"></i></a> </h3> <br> <h4 class="text-center"> <a href="admin">Click here</a> to login and setup your CTF </h4> </div>""".format(request.script_root)) # max attempts per challenge max_tries = utils.set_config('max_tries', 0) # Start time start = utils.set_config('start', None) end = utils.set_config('end', None) freeze = utils.set_config('freeze', None) # Challenges cannot be viewed by unregistered users view_challenges_unregistered = utils.set_config( 'view_challenges_unregistered', None) # Allow/Disallow registration prevent_registration = utils.set_config('prevent_registration', None) # Verify emails verify_emails = utils.set_config('verify_emails', None) mail_server = utils.set_config('mail_server', None) mail_port = utils.set_config('mail_port', None) mail_tls = utils.set_config('mail_tls', None) mail_ssl = utils.set_config('mail_ssl', None) mail_username = utils.set_config('mail_username', None) mail_password = utils.set_config('mail_password', None) setup = utils.set_config('setup', True) db.session.add(page) db.session.add(admin) db.session.commit() session['username'] = admin.name session['id'] = admin.id session['admin'] = admin.admin session['nonce'] = utils.sha512(os.urandom(10)) db.session.close() app.setup = False with app.app_context(): cache.clear() return redirect(url_for('views.static_html')) return render_template('setup.html', nonce=session.get('nonce')) return redirect(url_for('views.static_html'))
def test_user_can_reset_password(mock_smtp): """Test that a user is capable of resetting their password""" from email.message import EmailMessage app = create_ctfd() with app.app_context(), freeze_time("2012-01-14 03:21:34"): # Set CTFd to send emails set_config("mail_server", "localhost") set_config("mail_port", 25) set_config("mail_useauth", True) set_config("mail_username", "username") set_config("mail_password", "password") # Create a user register_user(app, name="user1", email="*****@*****.**") with app.test_client() as client: client.get("/reset_password") # Build reset password data with client.session_transaction() as sess: data = {"nonce": sess.get("nonce"), "email": "*****@*****.**"} # Issue the password reset request client.post("/reset_password", data=data) ctf_name = get_config("ctf_name") from_addr = get_config("mailfrom_addr") or app.config.get( "MAILFROM_ADDR") from_addr = "{} <{}>".format(ctf_name, from_addr) to_addr = "*****@*****.**" # Build the email msg = ( "Did you initiate a password reset on CTFd? If you didn't initiate this request you can ignore this email. " "\n\nClick the following link to reset your password:\n" "http://localhost/reset_password/InVzZXJAdXNlci5jb20i.TxD0vg.28dY_Gzqb1TH9nrcE_H7W8YFM-U\n\n" "If the link is not clickable, try copying and pasting it into your browser." ) ctf_name = get_config("ctf_name") email_msg = EmailMessage() email_msg.set_content(msg) email_msg[ "Subject"] = "Password Reset Request from {ctf_name}".format( ctf_name=ctf_name) email_msg["From"] = from_addr email_msg["To"] = to_addr # Make sure that the reset password email is sent mock_smtp.return_value.send_message.assert_called() assert str(mock_smtp.return_value.send_message.call_args[0] [0]) == str(email_msg) # Get user's original password user = Users.query.filter_by(email="*****@*****.**").first() # Build the POST data with client.session_transaction() as sess: data = {"nonce": sess.get("nonce"), "password": "******"} # Do the password reset client.get( "/reset_password/InVzZXJAdXNlci5jb20i.TxD0vg.28dY_Gzqb1TH9nrcE_H7W8YFM-U" ) client.post( "/reset_password/InVzZXJAdXNlci5jb20i.TxD0vg.28dY_Gzqb1TH9nrcE_H7W8YFM-U", data=data, ) # Make sure that the user's password changed user = Users.query.filter_by(email="*****@*****.**").first() assert verify_password("passwordtwo", user.password) destroy_ctfd(app)
def test_user_can_reset_password(mock_smtp): '''Test that a user is capable of resetting their password''' from email.mime.text import MIMEText app = create_ctfd() with app.app_context(): # Set CTFd to send emails set_config('mail_server', 'localhost') set_config('mail_port', 25) set_config('mail_username', 'username') set_config('mail_password', 'password') # Create a user register_user(app, name="user1", email="*****@*****.**") with app.test_client() as client: r = client.get('/reset_password') # Build reset password data with client.session_transaction() as sess: data = {'nonce': sess.get('nonce'), 'email': '*****@*****.**'} # Issue the password reset request r = client.post('/reset_password', data=data) from_addr = get_config('mailfrom_addr') or app.config.get( 'MAILFROM_ADDR') to_addr = '*****@*****.**' # Build the email msg = """Did you initiate a password reset? Click the following link to reset your password: http://localhost/reset_password/InVzZXIxIi5BZktHUGcuTVhkTmZtOWU2U2xwSXZ1MlFwTjdwa3F5V3hR """ email_msg = MIMEText(msg) email_msg['Subject'] = "Message from CTFd" email_msg['From'] = from_addr email_msg['To'] = to_addr # Make sure that the reset password email is sent mock_smtp.return_value.sendmail.assert_called_with( from_addr, [to_addr], email_msg.as_string()) # Get user's original password team = Teams.query.filter_by(email="*****@*****.**").first() team_password_saved = team.password # Build the POST data with client.session_transaction() as sess: data = {'nonce': sess.get('nonce'), 'password': '******'} # Do the password reset r = client.get('/reset_password') r = client.post( '/reset_password/InVzZXIxIi5BZktHUGcuTVhkTmZtOWU2U2xwSXZ1MlFwTjdwa3F5V3hR', data=data) # Make sure that the user's password changed team = Teams.query.filter_by(email="*****@*****.**").first() assert team.password != team_password_saved destroy_ctfd(app)
def setup(): errors = get_errors() if not config.is_setup(): if not session.get("nonce"): session["nonce"] = generate_nonce() if request.method == "POST": # General ctf_name = request.form.get("ctf_name") ctf_description = request.form.get("ctf_description") user_mode = request.form.get("user_mode", USERS_MODE) set_config("ctf_name", ctf_name) set_config("ctf_description", ctf_description) set_config("user_mode", user_mode) # Style theme = request.form.get("ctf_theme", "core") set_config("ctf_theme", theme) theme_color = request.form.get("theme_color") theme_header = get_config("theme_header") if theme_color and bool(theme_header) is False: # Uses {{ and }} to insert curly braces while using the format method css = ( '<style id="theme-color">\n' ":root {{--theme-color: {theme_color};}}\n" ".navbar{{background-color: var(--theme-color) !important;}}\n" ".jumbotron{{background-color: var(--theme-color) !important;}}\n" "</style>\n" ).format(theme_color=theme_color) set_config("theme_header", css) # DateTime start = request.form.get("start") end = request.form.get("end") set_config("start", start) set_config("end", end) set_config("freeze", None) # Administration name = request.form["name"] email = request.form["email"] password = request.form["password"] name_len = len(name) == 0 names = Users.query.add_columns("name", "id").filter_by(name=name).first() emails = ( Users.query.add_columns("email", "id").filter_by(email=email).first() ) pass_short = len(password) == 0 pass_long = len(password) > 128 valid_email = validators.validate_email(request.form["email"]) team_name_email_check = validators.validate_email(name) if not valid_email: errors.append("Please enter a valid email address") if names: errors.append("That user name is already taken") if team_name_email_check is True: errors.append("Your user name cannot be an email address") if emails: errors.append("That email has already been used") if pass_short: errors.append("Pick a longer password") if pass_long: errors.append("Pick a shorter password") if name_len: errors.append("Pick a longer user name") if len(errors) > 0: return render_template( "setup.html", errors=errors, name=name, email=email, password=password, state=serialize(generate_nonce()), ) admin = Admins( name=name, email=email, password=password, type="admin", hidden=True ) # Index page index = """<div class="row"> <div class="col-md-9 offset-md-2"> <img class="w-100 mx-auto d-block" style="max-width: 500px;padding: 50px;padding-top: 14vh;" src="themes/core/static/img/xsutd-istd-logo-web-2021.png.pagespeed.ic.w1dqDYZAE-.webp"> <h3 class="text-center"> <p>A CTF platform for System Security course</p> </h3> <br> <h4 class="text-center"> <a href="admin">Click here</a> to login and setup your CTF </h4> </div> </div>""" # """<div class="row"> # <div class="col-md-6 offset-md-3"> # <img class="w-100 mx-auto d-block" style="max-width: 500px;padding: 50px;padding-top: 14vh;" src="themes/core/static/img/logo.png" /> # <h3 class="text-center"> # <p>A cool CTF platform from <a href="https://ctfd.io">ctfd.io</a></p> # <p>Follow us on social media:</p> # <a href="https://twitter.com/ctfdio"><i class="fab fa-twitter fa-2x" aria-hidden="true"></i></a> # <a href="https://facebook.com/ctfdio"><i class="fab fa-facebook fa-2x" aria-hidden="true"></i></a> # <a href="https://github.com/ctfd"><i class="fab fa-github fa-2x" aria-hidden="true"></i></a> # </h3> # <br> # <h4 class="text-center"> # <a href="admin">Click here</a> to login and setup your CTF # </h4> # </div> # </div>""" page = Pages(title=None, route="index", content=index, draft=False) # Visibility set_config( ConfigTypes.CHALLENGE_VISIBILITY, ChallengeVisibilityTypes.PRIVATE ) set_config( ConfigTypes.REGISTRATION_VISIBILITY, RegistrationVisibilityTypes.PUBLIC ) set_config(ConfigTypes.SCORE_VISIBILITY, ScoreVisibilityTypes.PUBLIC) set_config(ConfigTypes.ACCOUNT_VISIBILITY, AccountVisibilityTypes.PUBLIC) # Verify emails set_config("verify_emails", None) set_config("mail_server", None) set_config("mail_port", None) set_config("mail_tls", None) set_config("mail_ssl", None) set_config("mail_username", None) set_config("mail_password", None) set_config("mail_useauth", None) # Set up default emails set_config("verification_email_subject", DEFAULT_VERIFICATION_EMAIL_SUBJECT) set_config("verification_email_body", DEFAULT_VERIFICATION_EMAIL_BODY) set_config( "successful_registration_email_subject", DEFAULT_SUCCESSFUL_REGISTRATION_EMAIL_SUBJECT, ) set_config( "successful_registration_email_body", DEFAULT_SUCCESSFUL_REGISTRATION_EMAIL_BODY, ) set_config( "user_creation_email_subject", DEFAULT_USER_CREATION_EMAIL_SUBJECT ) set_config("user_creation_email_body", DEFAULT_USER_CREATION_EMAIL_BODY) set_config("password_reset_subject", DEFAULT_PASSWORD_RESET_SUBJECT) set_config("password_reset_body", DEFAULT_PASSWORD_RESET_BODY) set_config( "password_change_alert_subject", "Password Change Confirmation for {ctf_name}", ) set_config( "password_change_alert_body", ( "Your password for {ctf_name} has been changed.\n\n" "If you didn't request a password change you can reset your password here: {url}" ), ) set_config("setup", True) try: db.session.add(admin) db.session.commit() except IntegrityError: db.session.rollback() try: db.session.add(page) db.session.commit() except IntegrityError: db.session.rollback() login_user(admin) db.session.close() with app.app_context(): cache.clear() return redirect(url_for("views.static_html")) return render_template("setup.html", state=serialize(generate_nonce())) return redirect(url_for("views.static_html"))
def setup(): if not config.is_setup(): if not session.get('nonce'): session['nonce'] = generate_nonce() if request.method == 'POST': ctf_name = request.form['ctf_name'] set_config('ctf_name', ctf_name) # CSS set_config('start', '') # Admin user name = request.form['name'] email = request.form['email'] password = request.form['password'] admin = Admins(name=name, email=email, password=password, type='admin', hidden=True) user_mode = request.form['user_mode'] set_config('user_mode', user_mode) # Index page index = """<div class="row"> <div class="col-md-6 offset-md-3"> <img class="w-100 mx-auto d-block" style="max-width: 500px;padding: 50px;padding-top: 14vh;" src="themes/core/static/img/logo.png" /> <h3 class="text-center"> <p>A cool CTF platform from <a href="https://ctfd.io">ctfd.io</a></p> <p>Follow us on social media:</p> <a href="https://twitter.com/ctfdio"><i class="fab fa-twitter fa-2x" aria-hidden="true"></i></a> <a href="https://facebook.com/ctfdio"><i class="fab fa-facebook fa-2x" aria-hidden="true"></i></a> <a href="https://github.com/ctfd"><i class="fab fa-github fa-2x" aria-hidden="true"></i></a> </h3> <br> <h4 class="text-center"> <a href="admin">Click here</a> to login and setup your CTF </h4> </div> </div>""".format(request.script_root) page = Pages(title=None, route='index', content=index, draft=False) # Visibility set_config('challenge_visibility', 'private') set_config('registration_visibility', 'public') set_config('score_visibility', 'public') set_config('account_visibility', 'public') # Start time set_config('start', None) set_config('end', None) set_config('freeze', None) # Verify emails set_config('verify_emails', None) set_config('mail_server', None) set_config('mail_port', None) set_config('mail_tls', None) set_config('mail_ssl', None) set_config('mail_username', None) set_config('mail_password', None) set_config('mail_useauth', None) setup = set_config('setup', True) try: db.session.add(admin) db.session.commit() except IntegrityError: db.session.rollback() try: db.session.add(page) db.session.commit() except IntegrityError: db.session.rollback() login_user(admin) db.session.close() app.setup = False with app.app_context(): cache.clear() return redirect(url_for('views.static_html')) return render_template('setup.html', nonce=session.get('nonce')) return redirect(url_for('views.static_html'))
def admin_css(): if request.method == 'POST': css = request.form['css'] css = set_config('css', css) return "1" return "0"
def create_app(config="CTFd.config.Config"): app = CTFdFlask(__name__) with app.app_context(): app.config.from_object(config) theme_loader = ThemeLoader(os.path.join(app.root_path, "themes"), followlinks=True) app.jinja_loader = theme_loader from CTFd.models import ( # noqa: F401 db, Teams, Solves, Challenges, Fails, Flags, Tags, Files, Tracking, ) url = create_database() # This allows any changes to the SQLALCHEMY_DATABASE_URI to get pushed back in # This is mostly so we can force MySQL's charset app.config["SQLALCHEMY_DATABASE_URI"] = str(url) # Register database db.init_app(app) # Register Flask-Migrate migrations.init_app(app, db) # Alembic sqlite support is lacking so we should just create_all anyway if url.drivername.startswith("sqlite"): db.create_all() stamp_latest_revision() else: # This creates tables instead of db.create_all() # Allows migrations to happen properly upgrade() from CTFd.models import ma ma.init_app(app) app.db = db app.VERSION = __version__ from CTFd.cache import cache cache.init_app(app) app.cache = cache reverse_proxy = app.config.get("REVERSE_PROXY") if reverse_proxy: if type(reverse_proxy) is str and "," in reverse_proxy: proxyfix_args = [int(i) for i in reverse_proxy.split(",")] app.wsgi_app = ProxyFix(app.wsgi_app, None, *proxyfix_args) else: app.wsgi_app = ProxyFix( app.wsgi_app, num_proxies=None, x_for=1, x_proto=1, x_host=1, x_port=1, x_prefix=1, ) version = utils.get_config("ctf_version") # Upgrading from an older version of CTFd if version and (StrictVersion(version) < StrictVersion(__version__)): if confirm_upgrade(): run_upgrade() else: exit() if not version: utils.set_config("ctf_version", __version__) if not utils.get_config("ctf_theme"): utils.set_config("ctf_theme", "core") update_check(force=True) init_request_processors(app) init_template_filters(app) init_template_globals(app) # Importing here allows tests to use sensible names (e.g. api instead of api_bp) from CTFd.views import views from CTFd.teams import teams from CTFd.users import users from CTFd.challenges import challenges from CTFd.scoreboard import scoreboard from CTFd.auth import auth from CTFd.admin import admin from CTFd.api import api from CTFd.events import events from CTFd.errors import page_not_found, forbidden, general_error, gateway_error app.register_blueprint(views) app.register_blueprint(teams) app.register_blueprint(users) app.register_blueprint(challenges) app.register_blueprint(scoreboard) app.register_blueprint(auth) app.register_blueprint(api) app.register_blueprint(events) app.register_blueprint(admin) app.register_error_handler(404, page_not_found) app.register_error_handler(403, forbidden) app.register_error_handler(500, general_error) app.register_error_handler(502, gateway_error) init_logs(app) init_events(app) init_plugins(app) return app
def setup(): # with app.app_context(): # admin = Teams.query.filter_by(admin=True).first() if not is_setup(): if not session.get('nonce'): session['nonce'] = sha512(os.urandom(10)) if request.method == 'POST': ctf_name = request.form['ctf_name'] ctf_name = set_config('ctf_name', ctf_name) ## CSS css = set_config('start', '') ## Admin user name = request.form['name'] email = request.form['email'] password = request.form['password'] admin = Teams(name, email, password) admin.admin = True admin.banned = True ## Index page page = Pages('index', """<div class="container main-container"> <img class="logo" src="/static/img/logo.png" /> <h3 class="text-center"> Welcome to a cool CTF framework written by <a href="https://github.com/ColdHeat">Kevin Chung</a> of <a href="https://github.com/isislab">@isislab</a> </h3> <h4 class="text-center"> <a href="/admin">Click here</a> to login and setup your CTF </h4> </div>""") #max attempts per challenge max_tries = set_config("max_tries",0) ## Start time start = set_config('start', None) end = set_config('end', None) ## Challenges cannot be viewed by unregistered users view_challenges_unregistered = set_config('view_challenges_unregistered', None) ## Allow/Disallow registration prevent_registration = set_config('prevent_registration', None) ## Verify emails verify_emails = set_config('verify_emails', None) mail_server = set_config('mail_server', None) mail_port = set_config('mail_port', None) mail_tls = set_config('mail_tls', None) mail_ssl = set_config('mail_ssl', None) mail_username = set_config('mail_username', None) mail_password = set_config('mail_password', None) setup = set_config('setup', True) db.session.add(page) db.session.add(admin) db.session.commit() app.setup = False return redirect('/') return render_template('setup.html', nonce=session.get('nonce')) return redirect('/')
def run_upgrade(): upgrade() utils.set_config('ctf_version', __version__)
def admin_config(): if request.method == "POST": try: start = int(request.form['start']) end = int(request.form['end']) except (ValueError, TypeError): start = None end = None try: view_challenges_unregistered = bool(request.form.get('view_challenges_unregistered', None)) prevent_registration = bool(request.form.get('prevent_registration', None)) prevent_name_change = bool(request.form.get('prevent_name_change', None)) view_after_ctf = bool(request.form.get('view_after_ctf', None)) except (ValueError, TypeError): view_challenges_unregistered = None prevent_registration = None prevent_name_change = None view_after_ctf = None finally: view_challenges_unregistered = set_config('view_challenges_unregistered', view_challenges_unregistered) prevent_registration = set_config('prevent_registration', prevent_registration) prevent_name_change = set_config('prevent_name_change', prevent_name_change) view_after_ctf = set_config('view_after_ctf', view_after_ctf) ctf_name = set_config("ctf_name", request.form.get('ctf_name', None)) mg_api_key = set_config("mg_api_key", request.form.get('mg_api_key', None)) max_tries = set_config("max_tries", request.form.get('max_tries', None)) 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') ctf_name = get_config('ctf_name') if not ctf_name: set_config('ctf_name', None) mg_api_key = get_config('mg_api_key') if not mg_api_key: set_config('mg_api_key', None) max_tries = get_config('max_tries') if not max_tries: set_config('max_tries', 0) max_tries = 0 view_after_ctf = get_config('view_after_ctf') == '1' if not view_after_ctf: set_config('view_after_ctf', 0) view_after_ctf = 0 start = get_config('start') if not start: set_config('start', None) end = get_config('end') if not end: set_config('end', None) view_challenges_unregistered = get_config('view_challenges_unregistered') == '1' if not view_challenges_unregistered: set_config('view_challenges_unregistered', None) prevent_registration = get_config('prevent_registration') == '1' if not prevent_registration: set_config('prevent_registration', None) prevent_name_change = get_config('prevent_name_change') == '1' if not prevent_name_change: set_config('prevent_name_change', None) db.session.commit() db.session.close() return render_template('admin/config.html', ctf_name=ctf_name, start=start, end=end, max_tries=max_tries, view_challenges_unregistered=view_challenges_unregistered, prevent_registration=prevent_registration, mg_api_key=mg_api_key, prevent_name_change=prevent_name_change, view_after_ctf=view_after_ctf)
def admin_config(): if request.method == "POST": start = None end = None if request.form.get('start'): start = int(request.form['start']) if request.form.get('end'): end = int(request.form['end']) if end < unix_time(datetime.datetime.now()): end = None try: view_challenges_unregistered = bool( request.form.get('view_challenges_unregistered', None)) view_scoreboard_if_authed = bool( request.form.get('view_scoreboard_if_authed', None)) prevent_registration = bool( request.form.get('prevent_registration', None)) prevent_name_change = bool( request.form.get('prevent_name_change', None)) view_after_ctf = bool(request.form.get('view_after_ctf', None)) verify_emails = bool(request.form.get('verify_emails', None)) mail_tls = bool(request.form.get('mail_tls', None)) mail_ssl = bool(request.form.get('mail_ssl', None)) except (ValueError, TypeError): view_challenges_unregistered = None view_scoreboard_if_authed = None prevent_registration = None prevent_name_change = None view_after_ctf = None verify_emails = None mail_tls = None mail_ssl = None finally: view_challenges_unregistered = set_config( 'view_challenges_unregistered', view_challenges_unregistered) view_scoreboard_if_authed = set_config('view_scoreboard_if_authed', view_scoreboard_if_authed) prevent_registration = set_config('prevent_registration', prevent_registration) prevent_name_change = set_config('prevent_name_change', prevent_name_change) view_after_ctf = set_config('view_after_ctf', view_after_ctf) verify_emails = set_config('verify_emails', verify_emails) mail_tls = set_config('mail_tls', mail_tls) mail_ssl = set_config('mail_ssl', mail_ssl) mail_server = set_config("mail_server", request.form.get('mail_server', None)) mail_port = set_config("mail_port", request.form.get('mail_port', None)) mail_username = set_config("mail_username", request.form.get('mail_username', None)) mail_password = set_config("mail_password", request.form.get('mail_password', None)) ctf_name = set_config("ctf_name", request.form.get('ctf_name', None)) ctf_theme = set_config("ctf_theme", request.form.get('ctf_theme', None)) mg_base_url = set_config("mg_base_url", request.form.get('mg_base_url', None)) mg_api_key = set_config("mg_api_key", request.form.get('mg_api_key', None)) max_tries = set_config("max_tries", request.form.get('max_tries', None)) 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() db.session.close() return redirect(url_for('admin.admin_config', _external=True)) ctf_name = get_config('ctf_name') ctf_theme = get_config('ctf_theme') max_tries = get_config('max_tries') mail_server = get_config('mail_server') mail_port = get_config('mail_port') mail_username = get_config('mail_username') mail_password = get_config('mail_password') mg_api_key = get_config('mg_api_key') mg_base_url = get_config('mg_base_url') if not max_tries: set_config('max_tries', 0) max_tries = 0 view_after_ctf = get_config('view_after_ctf') start = get_config('start') end = get_config('end') mail_tls = get_config('mail_tls') mail_ssl = get_config('mail_ssl') view_challenges_unregistered = get_config('view_challenges_unregistered') view_scoreboard_if_authed = get_config('view_scoreboard_if_authed') prevent_registration = get_config('prevent_registration') prevent_name_change = get_config('prevent_name_change') verify_emails = get_config('verify_emails') db.session.commit() db.session.close() months = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ] curr_year = datetime.date.today().year start_days = 0 end_days = 0 if start: start = datetime.datetime.fromtimestamp(float(start)) start_days = calendar.monthrange(start.year, start.month)[1] if end: end = datetime.datetime.fromtimestamp(float(end)) end_days = calendar.monthrange(end.year, end.month)[1] themes = get_themes() themes.remove(ctf_theme) return render_template( 'admin/config.html', ctf_name=ctf_name, ctf_theme_config=ctf_theme, start=start, end=end, max_tries=max_tries, mail_server=mail_server, mail_port=mail_port, mail_username=mail_username, mail_password=mail_password, mail_tls=mail_tls, mail_ssl=mail_ssl, view_challenges_unregistered=view_challenges_unregistered, view_scoreboard_if_authed=view_scoreboard_if_authed, prevent_registration=prevent_registration, mg_base_url=mg_base_url, mg_api_key=mg_api_key, prevent_name_change=prevent_name_change, verify_emails=verify_emails, view_after_ctf=view_after_ctf, months=months, curr_year=curr_year, start_days=start_days, end_days=end_days, themes=themes)
def create_app(config='CTFd.config.Config'): app = Flask(__name__) with app.app_context(): app.config.from_object(config) app.jinja_loader = ThemeLoader(os.path.join(app.root_path, 'themes'), followlinks=True) from CTFd.models import db, Teams, Solves, Challenges, WrongKeys, Keys, Tags, Files, Tracking url = make_url(app.config['SQLALCHEMY_DATABASE_URI']) if url.drivername == 'postgres': url.drivername = 'postgresql' if url.drivername.startswith('mysql'): url.query['charset'] = 'utf8mb4' # Creates database if the database database does not exist if not database_exists(url): if url.drivername.startswith('mysql'): create_database(url, encoding='utf8mb4') else: create_database(url) # This allows any changes to the SQLALCHEMY_DATABASE_URI to get pushed back in # This is mostly so we can force MySQL's charset app.config['SQLALCHEMY_DATABASE_URI'] = str(url) # Register database db.init_app(app) # 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() app.db = db cache.init_app(app) app.cache = cache version = utils.get_config('ctf_version') if not version: # Upgrading from an unversioned CTFd utils.set_config('ctf_version', __version__) if version and (StrictVersion(version) < StrictVersion(__version__)): # Upgrading from an older version of CTFd print("/*\\ CTFd has updated and must update the database! /*\\") print("/*\\ Please backup your database before proceeding! /*\\") print("/*\\ CTFd maintainers are not responsible for any data loss! /*\\") if input('Run database migrations (Y/N)').lower().strip() == 'y': migrate_stamp() migrate_upgrade() utils.set_config('ctf_version', __version__) else: print('/*\\ Ignored database migrations... /*\\') exit() if not utils.get_config('ctf_theme'): utils.set_config('ctf_theme', 'original') from CTFd.views import views from CTFd.challenges import challenges from CTFd.scoreboard import scoreboard from CTFd.auth import auth from CTFd.admin import admin, admin_statistics, admin_challenges, admin_pages, admin_scoreboard, admin_containers, admin_keys, admin_teams from CTFd.utils import init_utils, init_errors, init_logs init_utils(app) init_errors(app) init_logs(app) app.register_blueprint(views) app.register_blueprint(challenges) app.register_blueprint(scoreboard) app.register_blueprint(auth) app.register_blueprint(admin) app.register_blueprint(admin_statistics) app.register_blueprint(admin_challenges) app.register_blueprint(admin_teams) app.register_blueprint(admin_scoreboard) app.register_blueprint(admin_keys) app.register_blueprint(admin_containers) app.register_blueprint(admin_pages) from CTFd.plugins import init_plugins init_plugins(app) return app
def test_user_can_access_files(): app = create_ctfd() with app.app_context(): from CTFd.utils.uploads import rmdir chal = gen_challenge(app.db) chal_id = chal.id path = app.config.get("UPLOAD_FOLDER") location = os.path.join(path, "test_file_path", "test.txt") directory = os.path.dirname(location) model_path = os.path.join("test_file_path", "test.txt") try: os.makedirs(directory) with open(location, "wb") as obj: obj.write("testing file load".encode()) gen_file(app.db, location=model_path, challenge_id=chal_id) url = url_for("views.files", path=model_path) # Unauthed user should be able to see challenges if challenges are public set_config("challenge_visibility", "public") with app.test_client() as client: r = client.get(url) assert r.status_code == 200 assert r.get_data(as_text=True) == "testing file load" # Unauthed user should not be able to see challenges if challenges are private set_config("challenge_visibility", "private") with app.test_client() as client: r = client.get(url) assert r.status_code == 403 assert r.get_data(as_text=True) != "testing file load" # Authed user should be able to see files if challenges are private register_user(app) client = login_as_user(app) r = client.get(url) assert r.status_code == 200 assert r.get_data(as_text=True) == "testing file load" with freeze_time("2017-10-5"): # Friday, October 6, 2017 12:00:00 AM GMT-04:00 DST set_config("start", "1507262400") for v in ("public", "private"): set_config("challenge_visibility", v) # Unauthed users shouldn't be able to see files if the CTF hasn't started client = app.test_client() r = client.get(url) assert r.status_code == 403 assert r.get_data(as_text=True) != "testing file load" # Authed users shouldn't be able to see files if the CTF hasn't started client = login_as_user(app) r = client.get(url) assert r.status_code == 403 assert r.get_data(as_text=True) != "testing file load" # Admins should be able to see files if the CTF hasn't started admin = login_as_user(app, "admin") r = admin.get(url) assert r.status_code == 200 assert r.get_data(as_text=True) == "testing file load" with freeze_time("2017-10-7"): # Friday, October 6, 2017 12:00:00 AM GMT-04:00 DST set_config("end", "1507262400") for v in ("public", "private"): set_config("challenge_visibility", v) # Unauthed users shouldn't be able to see files if the CTF has ended client = app.test_client() r = client.get(url) assert r.status_code == 403 assert r.get_data(as_text=True) != "testing file load" # Authed users shouldn't be able to see files if the CTF has ended client = login_as_user(app) r = client.get(url) assert r.status_code == 403 assert r.get_data(as_text=True) != "testing file load" # Admins should be able to see files if the CTF has ended admin = login_as_user(app, "admin") r = admin.get(url) assert r.status_code == 200 assert r.get_data(as_text=True) == "testing file load" finally: rmdir(directory) destroy_ctfd(app)
def test_user_can_access_files_with_auth_token(): app = create_ctfd() with app.app_context(): from CTFd.utils.uploads import rmdir chal = gen_challenge(app.db) chal_id = chal.id path = app.config.get("UPLOAD_FOLDER") md5hash = hexencode(os.urandom(16)) location = os.path.join(path, md5hash, "test.txt") directory = os.path.dirname(location) model_path = os.path.join(md5hash, "test.txt") try: os.makedirs(directory) with open(location, "wb") as obj: obj.write("testing file load".encode()) gen_file(app.db, location=model_path, challenge_id=chal_id) url = url_for("views.files", path=model_path) register_user(app) with login_as_user(app) as client: req = client.get("/api/v1/challenges/1") data = req.get_json() file_url = data["data"]["files"][0] with app.test_client() as client: r = client.get(url) assert r.status_code == 403 assert r.get_data(as_text=True) != "testing file load" r = client.get( url_for( "views.files", path=model_path, token="random_token_that_shouldnt_work", )) assert r.status_code == 403 assert r.get_data(as_text=True) != "testing file load" r = client.get(file_url) assert r.status_code == 200 assert r.get_data(as_text=True) == "testing file load" # Unauthed users shouldn't be able to see files if the CTF is admins only set_config("challenge_visibility", "admins") r = client.get(file_url) assert r.status_code == 403 assert r.get_data(as_text=True) != "testing file load" set_config("challenge_visibility", "private") with freeze_time("2017-10-5"): # Friday, October 6, 2017 12:00:00 AM GMT-04:00 DST set_config("start", "1507262400") # Unauthed users shouldn't be able to see files if the CTF hasn't started r = client.get(file_url) assert r.status_code == 403 assert r.get_data(as_text=True) != "testing file load" with freeze_time("2017-10-5"): # Friday, October 6, 2017 12:00:00 AM GMT-04:00 DST set_config("start", "1507262400") for v in ("public", "private"): set_config("challenge_visibility", v) # Unauthed users shouldn't be able to see files if the CTF hasn't started client = app.test_client() r = client.get(file_url) assert r.status_code == 403 assert r.get_data(as_text=True) != "testing file load" # Authed users shouldn't be able to see files if the CTF hasn't started client = login_as_user(app) r = client.get(file_url) assert r.status_code == 403 assert r.get_data(as_text=True) != "testing file load" # Admins should be able to see files if the CTF hasn't started admin = login_as_user(app, "admin") r = admin.get(file_url) assert r.status_code == 200 assert r.get_data(as_text=True) == "testing file load" with freeze_time("2017-10-7"): # Friday, October 6, 2017 12:00:00 AM GMT-04:00 DST set_config("end", "1507262400") for v in ("public", "private"): set_config("challenge_visibility", v) # Unauthed users shouldn't be able to see files if the CTF has ended client = app.test_client() r = client.get(file_url) assert r.status_code == 403 assert r.get_data(as_text=True) != "testing file load" # Authed users shouldn't be able to see files if the CTF has ended client = login_as_user(app) r = client.get(file_url) assert r.status_code == 403 assert r.get_data(as_text=True) != "testing file load" # Admins should be able to see files if the CTF has ended admin = login_as_user(app, "admin") r = admin.get(file_url) assert r.status_code == 200 assert r.get_data(as_text=True) == "testing file load" finally: rmdir(directory) destroy_ctfd(app)
def test_challenges_under_view_after_ctf(): app = create_ctfd() with app.app_context(), freeze_time("2017-10-7"): 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) client = login_as_user(app) gen_challenge(app.db) gen_flag(app.db, challenge_id=1, content='flag') r = client.get('/challenges') assert r.status_code == 403 r = client.get('/api/v1/challenges') assert r.status_code == 403 assert r.get_json().get('data') is None r = client.get('/api/v1/challenges/1') assert r.status_code == 403 assert r.get_json().get('data') is None data = {"submission": 'flag', "challenge_id": 1} r = client.post('/api/v1/challenges/attempt', json=data) assert r.status_code == 403 assert r.get_json().get('data') is None assert Solves.query.count() == 0 data = {"submission": 'notflag', "challenge_id": 1} r = client.post('/api/v1/challenges/attempt', json=data) assert r.status_code == 403 assert r.get_json().get('data') is None assert Fails.query.count() == 0 set_config('view_after_ctf', True) r = client.get('/challenges') assert r.status_code == 200 r = client.get('/api/v1/challenges') assert r.status_code == 200 assert r.get_json()['data'][0]['id'] == 1 r = client.get('/api/v1/challenges/1') assert r.status_code == 200 assert r.get_json()['data']['id'] == 1 data = {"submission": 'flag', "challenge_id": 1} r = client.post('/api/v1/challenges/attempt', json=data) assert r.status_code == 200 assert r.get_json()['data']['status'] == "correct" assert Solves.query.count() == 0 data = {"submission": 'notflag', "challenge_id": 1} r = client.post('/api/v1/challenges/attempt', json=data) assert r.status_code == 200 assert r.get_json()['data']['status'] == "incorrect" assert Fails.query.count() == 0 destroy_ctfd(app)
def admin_config(): if request.method == "POST": start = None end = None if request.form.get('start'): start = int(request.form['start']) if request.form.get('end'): end = int(request.form['end']) if end < unix_time(datetime.datetime.now()): end = None try: view_challenges_unregistered = bool(request.form.get('view_challenges_unregistered', None)) view_scoreboard_if_authed = bool(request.form.get('view_scoreboard_if_authed', None)) prevent_registration = bool(request.form.get('prevent_registration', None)) prevent_name_change = bool(request.form.get('prevent_name_change', None)) view_after_ctf = bool(request.form.get('view_after_ctf', None)) verify_emails = bool(request.form.get('verify_emails', None)) mail_tls = bool(request.form.get('mail_tls', None)) mail_ssl = bool(request.form.get('mail_ssl', None)) except (ValueError, TypeError): view_challenges_unregistered = None view_scoreboard_if_authed = None prevent_registration = None prevent_name_change = None view_after_ctf = None verify_emails = None mail_tls = None mail_ssl = None finally: view_challenges_unregistered = set_config('view_challenges_unregistered', view_challenges_unregistered) view_scoreboard_if_authed = set_config('view_scoreboard_if_authed', view_scoreboard_if_authed) prevent_registration = set_config('prevent_registration', prevent_registration) prevent_name_change = set_config('prevent_name_change', prevent_name_change) view_after_ctf = set_config('view_after_ctf', view_after_ctf) verify_emails = set_config('verify_emails', verify_emails) mail_tls = set_config('mail_tls', mail_tls) mail_ssl = set_config('mail_ssl', mail_ssl) mail_server = set_config("mail_server", request.form.get('mail_server', None)) mail_port = set_config("mail_port", request.form.get('mail_port', None)) mail_username = set_config("mail_username", request.form.get('mail_username', None)) mail_password = set_config("mail_password", request.form.get('mail_password', None)) ctf_name = set_config("ctf_name", request.form.get('ctf_name', None)) mg_base_url = set_config("mg_base_url", request.form.get('mg_base_url', None)) mg_api_key = set_config("mg_api_key", request.form.get('mg_api_key', None)) max_tries = set_config("max_tries", request.form.get('max_tries', None)) 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(url_for('admin.admin_config')) ctf_name = get_config('ctf_name') max_tries = get_config('max_tries') mail_server = get_config('mail_server') mail_port = get_config('mail_port') mail_username = get_config('mail_username') mail_password = get_config('mail_password') mg_api_key = get_config('mg_api_key') mg_base_url = get_config('mg_base_url') if not max_tries: set_config('max_tries', 0) max_tries = 0 view_after_ctf = get_config('view_after_ctf') start = get_config('start') end = get_config('end') mail_tls = get_config('mail_tls') mail_ssl = get_config('mail_ssl') view_challenges_unregistered = get_config('view_challenges_unregistered') view_scoreboard_if_authed = get_config('view_scoreboard_if_authed') prevent_registration = get_config('prevent_registration') prevent_name_change = get_config('prevent_name_change') verify_emails = get_config('verify_emails') db.session.commit() db.session.close() months = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ] curr_year = datetime.date.today().year start_days = 0 end_days = 0 if start: start = datetime.datetime.fromtimestamp(float(start)) start_days = calendar.monthrange(start.year, start.month)[1] if end: end = datetime.datetime.fromtimestamp(float(end)) end_days = calendar.monthrange(end.year, end.month)[1] return render_template('admin/config.html', ctf_name=ctf_name, start=start, end=end, max_tries=max_tries, mail_server=mail_server, mail_port=mail_port, mail_username=mail_username, mail_password=mail_password, mail_tls=mail_tls, mail_ssl=mail_ssl, view_challenges_unregistered=view_challenges_unregistered, view_scoreboard_if_authed=view_scoreboard_if_authed, prevent_registration=prevent_registration, mg_base_url=mg_base_url, mg_api_key=mg_api_key, prevent_name_change=prevent_name_change, verify_emails=verify_emails, view_after_ctf=view_after_ctf, months=months, curr_year=curr_year, start_days=start_days, end_days=end_days)
def setup(): # with app.app_context(): # admin = Teams.query.filter_by(admin=True).first() if not utils.is_setup(): if not session.get('nonce'): session['nonce'] = utils.sha512(os.urandom(10)) if request.method == 'POST': ctf_name = request.form['ctf_name'] ctf_name = utils.set_config('ctf_name', ctf_name) # CSS css = utils.set_config('start', '') # Admin user name = request.form['name'] email = request.form['email'] password = request.form['password'] admin = Teams(name, email, password) admin.admin = True admin.banned = True # Index page page = Pages('index', """<div class="container main-container"> <img class="logo" src="themes/original/static/img/logo.png" /> <h3 class="text-center"> <p>A cool CTF platform from <a href="https://ctfd.io">ctfd.io</a></p> <p>Follow us on social media:</p> <a href="https://twitter.com/ctfdio"><i class="fa fa-twitter fa-2x" aria-hidden="true"></i></a> <a href="https://facebook.com/ctfdio"><i class="fa fa-facebook-official fa-2x" aria-hidden="true"></i></a> <a href="https://github.com/ctfd"><i class="fa fa-github fa-2x" aria-hidden="true"></i></a> </h3> <br> <h4 class="text-center"> <a href="admin">Click here</a> to login and setup your CTF </h4> </div>""".format(request.script_root)) # max attempts per challenge max_tries = utils.set_config('max_tries', 0) # Start time start = utils.set_config('start', None) end = utils.set_config('end', None) freeze = utils.set_config('freeze', None) # Challenges cannot be viewed by unregistered users view_challenges_unregistered = utils.set_config('view_challenges_unregistered', None) # Allow/Disallow registration prevent_registration = utils.set_config('prevent_registration', None) # Verify emails verify_emails = utils.set_config('verify_emails', None) mail_server = utils.set_config('mail_server', None) mail_port = utils.set_config('mail_port', None) mail_tls = utils.set_config('mail_tls', None) mail_ssl = utils.set_config('mail_ssl', None) mail_username = utils.set_config('mail_username', None) mail_password = utils.set_config('mail_password', None) setup = utils.set_config('setup', True) db.session.add(page) db.session.add(admin) db.session.commit() session['username'] = admin.name session['id'] = admin.id session['admin'] = admin.admin session['nonce'] = utils.sha512(os.urandom(10)) db.session.close() app.setup = False with app.app_context(): cache.clear() return redirect(url_for('views.static_html')) return render_template('setup.html', nonce=session.get('nonce')) return redirect(url_for('views.static_html'))