def ctftime(): """ Checks whether it's CTF time or not. """ start = get_config("start") end = get_config("end") if start: start = int(start) else: start = 0 if end: end = int(end) else: end = 0 if start and end: if start < time.time() < end: # Within the two time bounds return True if start < time.time() and end == 0: # CTF starts on a date but never ends return True if start == 0 and time.time() < end: # CTF started but ends at a date return True if start == 0 and end == 0: # CTF has no time requirements return True return False
def sendmail(addr, text, subject): ctf_name = get_config("ctf_name") mailfrom_addr = get_config("mailfrom_addr") or get_app_config( "MAILFROM_ADDR") mailfrom_addr = "{} <{}>".format(ctf_name, mailfrom_addr) mailgun_base_url = get_config("mailgun_base_url") or get_app_config( "MAILGUN_BASE_URL") mailgun_api_key = get_config("mailgun_api_key") or get_app_config( "MAILGUN_API_KEY") try: r = requests.post( mailgun_base_url + "/messages", auth=("api", mailgun_api_key), data={ "from": mailfrom_addr, "to": [addr], "subject": subject, "text": text, }, timeout=1.0, ) except requests.RequestException as e: return ( False, "{error} exception occured while handling your request".format( error=type(e).__name__), ) if r.status_code == 200: return True, "Email sent" else: return False, "Mailgun settings are incorrect"
def test_successful_registration_email(mock_smtp): """Does successful_registration_notification send emails""" app = create_kmactf() 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 KMActf!" 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_kmactf(app)
def test_sendmail_with_smtp_from_config_file(mock_smtp): """Does sendmail work properly with simple SMTP mail servers using file configuration""" app = create_kmactf() with app.app_context(): app.config["MAIL_SERVER"] = "localhost" app.config["MAIL_PORT"] = "25" app.config["MAIL_USEAUTH"] = "True" app.config["MAIL_USERNAME"] = "******" app.config["MAIL_PASSWORD"] = "******" 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 = "*****@*****.**" 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()) destroy_kmactf(app)
def test_sendmail_with_smtp_from_db_config(mock_smtp): """Does sendmail work properly with simple SMTP mail servers using database configuration""" app = create_kmactf() 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") 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 = "*****@*****.**" 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()) destroy_kmactf(app)
def test_update_check_identifies_update(fake_get_request): """Update checks properly identify new versions""" app = create_kmactf() with app.app_context(): app.config["UPDATE_CHECK"] = True fake_response = Mock() fake_get_request.return_value = fake_response fake_response.json = lambda: { "resource": { "download_url": "https://api.github.com/repos/KMActf/KMActf/zipball/9.9.9", "html_url": "https://github.com/KMActf/KMActf/releases/tag/9.9.9", "id": 12, "latest": True, "next": 1542212248, "prerelease": False, "published_at": "Wed, 25 Oct 2017 19:39:42 -0000", "tag": "9.9.9", } } update_check() assert (get_config("version_latest") == "https://github.com/KMActf/KMActf/releases/tag/9.9.9") assert get_config("next_update_check") == 1542212248 destroy_kmactf(app)
def mailgun(): if app.config.get("MAILGUN_API_KEY") and app.config.get( "MAILGUN_BASE_URL"): return True if get_config("mailgun_api_key") and get_config("mailgun_base_url"): return True return False
def oauth_login(): endpoint = (get_app_config("OAUTH_AUTHORIZATION_ENDPOINT") or get_config("oauth_authorization_endpoint") or "https://auth.majorleaguecyber.org/oauth/authorize") if get_config("user_mode") == "teams": scope = "profile team" else: scope = "profile" client_id = get_app_config("OAUTH_CLIENT_ID") or get_config( "oauth_client_id") if client_id is None: error_for( endpoint="auth.login", message="OAuth Settings not configured. " "Ask your CTF administrator to configure MajorLeagueCyber integration.", ) return redirect(url_for("auth.login")) redirect_url = "{endpoint}?response_type=code&client_id={client_id}&scope={scope}&state={state}".format( endpoint=endpoint, client_id=client_id, scope=scope, state=session["nonce"]) return redirect(redirect_url)
def update_check(force=False): """ Makes a request to kmactf to check if there is a new version of KMActf available. The service is provided in return for users opting in to anonymous usage data collection. Users can opt-out of update checks by specifying UPDATE_CHECK = False in config.py :param force: :return: """ # If UPDATE_CHECK is disabled don't check for updates at all. if app.config.get("UPDATE_CHECK") is False: return # Don't do an update check if not setup if is_setup() is False: return # Get when we should check for updates next. next_update_check = get_config("next_update_check") or 0 # If we have passed our saved time or we are forcing we should check. update = (next_update_check < time.time()) or force if update: try: name = str(get_config("ctf_name")) or "" params = { "ctf_id": sha256(name), "current": app.VERSION, "python_version_raw": sys.hexversion, "python_version": python_version(), "db_driver": db.session.bind.dialect.name, "challenge_count": Challenges.query.count(), "user_mode": get_config("user_mode"), "user_count": Users.query.count(), "team_count": Teams.query.count(), "theme": get_config("ctf_theme"), "upload_provider": get_app_config("UPLOAD_PROVIDER"), } check = requests.post("https://actvn.edu.vn/", json=params, timeout=0.1).json() except requests.exceptions.RequestException: pass except ValueError: pass else: try: latest = check["resource"]["tag"] html_url = check["resource"]["html_url"] if StrictVersion(latest) > StrictVersion(app.VERSION): set_config("version_latest", html_url) elif StrictVersion(latest) <= StrictVersion(app.VERSION): set_config("version_latest", None) next_update_check_time = check["resource"].get( "next", int(time.time() + 43200)) set_config("next_update_check", next_update_check_time) except KeyError: set_config("version_latest", None)
def test_get_config_and_set_config(): """Does get_config and set_config work properly""" app = create_kmactf() 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" destroy_kmactf(app)
def validate_email(self, data): email = data.get("email") if email is None: return email = email.strip() existing_user = Users.query.filter_by(email=email).first() current_user = get_current_user() if is_admin(): user_id = data.get("id") if user_id: if existing_user and existing_user.id != user_id: raise ValidationError( "Email address has already been used", field_names=["email"]) else: if existing_user: if current_user: if current_user.id != existing_user.id: raise ValidationError( "Email address has already been used", field_names=["email"], ) else: raise ValidationError( "Email address has already been used", field_names=["email"]) else: if email == current_user.email: return data else: confirm = data.get("confirm") if bool(confirm) is False: raise ValidationError( "Please confirm your current password", field_names=["confirm"]) test = verify_password(plaintext=confirm, ciphertext=current_user.password) if test is False: raise ValidationError( "Your previous password is incorrect", field_names=["confirm"]) if existing_user: raise ValidationError( "Email address has already been used", field_names=["email"]) if check_email_is_whitelisted(email) is False: raise ValidationError( "Only email addresses under {domains} may register". format(domains=get_config("domain_whitelist")), field_names=["email"], ) if get_config("verify_emails"): current_user.verified = False
def get_mail_provider(): if app.config.get("MAIL_SERVER") and app.config.get("MAIL_PORT"): return "smtp" if get_config("mail_server") and get_config("mail_port"): return "smtp" if app.config.get("MAILGUN_API_KEY") and app.config.get( "MAILGUN_BASE_URL"): return "mailgun" if get_config("mailgun_api_key") and get_config("mailgun_base_url"): return "mailgun"
def generate_account_url(account_id, admin=False): if get_config("user_mode") == USERS_MODE: if admin: return url_for("admin.users_detail", user_id=account_id) else: return url_for("users.public", user_id=account_id) elif get_config("user_mode") == TEAMS_MODE: if admin: return url_for("admin.teams_detail", team_id=account_id) else: return url_for("teams.public", team_id=account_id)
def forgot_password(email): text = safe_format( get_config("password_reset_body") or DEFAULT_PASSWORD_RESET_BODY, ctf_name=get_config("ctf_name"), ctf_description=get_config("ctf_description"), url=url_for("auth.reset_password", data=serialize(email), _external=True), ) subject = safe_format( get_config("password_reset_subject") or DEFAULT_PASSWORD_RESET_SUBJECT, ctf_name=get_config("ctf_name"), ) return sendmail(addr=email, text=text, subject=subject)
def successful_registration_notification(addr): text = safe_format( get_config("successful_registration_email_body") or DEFAULT_SUCCESSFUL_REGISTRATION_EMAIL_BODY, ctf_name=get_config("ctf_name"), ctf_description=get_config("ctf_description"), url=url_for("views.static_html", _external=True), ) subject = safe_format( get_config("successful_registration_email_subject") or DEFAULT_SUCCESSFUL_REGISTRATION_EMAIL_SUBJECT, ctf_name=get_config("ctf_name"), ) return sendmail(addr=addr, text=text, subject=subject)
def config(): # Clear the config cache so that we don't get stale values clear_config() database_tables = sorted(db.metadata.tables.keys()) configs = Configs.query.all() configs = dict([(c.key, get_config(c.key)) for c in configs]) themes = ctf_config.get_themes() themes.remove(get_config("ctf_theme")) return render_template( "admin/config.html", database_tables=database_tables, themes=themes, **configs )
def password_change_alert(email): text = safe_format( get_config("password_change_alert_body") or DEFAULT_PASSWORD_CHANGE_ALERT_BODY, ctf_name=get_config("ctf_name"), ctf_description=get_config("ctf_description"), url=url_for("auth.reset_password", _external=True), ) subject = safe_format( get_config("password_change_alert_subject") or DEFAULT_PASSWORD_CHANGE_ALERT_SUBJECT, ctf_name=get_config("ctf_name"), ) return sendmail(addr=email, text=text, subject=subject)
def test_update_check_ignores_downgrades(fake_post_request): """Update checks do nothing on old or same versions""" app = create_kmactf() with app.app_context(): app.config["UPDATE_CHECK"] = True fake_response = Mock() fake_post_request.return_value = fake_response fake_response.json = lambda: { u"resource": { u"html_url": u"https://github.com/KMActf/KMActf/releases/tag/0.0.1", u"download_url": u"https://api.github.com/repos/KMActf/KMActf/zipball/0.0.1", u"published_at": u"Wed, 25 Oct 2017 19:39:42 -0000", u"tag": u"0.0.1", u"prerelease": False, u"id": 6, u"latest": True, } } update_check() assert get_config("version_latest") is None fake_response = Mock() fake_post_request.return_value = fake_response fake_response.json = lambda: { u"resource": { u"html_url": u"https://github.com/KMActf/KMActf/releases/tag/{}".format( app.VERSION), u"download_url": u"https://api.github.com/repos/KMActf/KMActf/zipball/{}". format(app.VERSION), u"published_at": u"Wed, 25 Oct 2017 19:39:42 -0000", u"tag": u"{}".format(app.VERSION), u"prerelease": False, u"id": 6, u"latest": True, } } update_check() assert get_config("version_latest") is None destroy_kmactf(app)
def _check_score_visibility(*args, **kwargs): v = get_config("score_visibility") if v == "public": return f(*args, **kwargs) elif v == "private": if authed(): return f(*args, **kwargs) else: if request.content_type == "application/json": abort(403) else: return redirect( url_for("auth.login", next=request.full_path)) elif v == "hidden": return ( render_template("errors/403.html", error="Scores are currently hidden"), 403, ) elif v == "admins": if is_admin(): return f(*args, **kwargs) else: abort(404)
def test_api_configs_get_non_admin(): """Can a user get /api/v1/configs if not admin""" app = create_kmactf() with app.app_context(): with app.test_client() as client: r = client.get("/api/v1/configs") assert r.status_code == 302 # test_api_configs_post_non_admin """Can a user post /api/v1/configs if not admin""" r = client.post("/api/v1/configs", json="") assert r.status_code == 403 # test_api_configs_patch_non_admin """Can a user patch /api/v1/configs if not admin""" r = client.patch("/api/v1/configs", json="") assert r.status_code == 403 # test_api_config_get_non_admin """Can a user get /api/v1/configs/<config_key> if not admin""" r = client.get("/api/v1/configs/ctf_name") assert r.status_code == 302 # test_api_config_patch_non_admin """Can a user patch /api/v1/configs/<config_key> if not admin""" r = client.patch("/api/v1/configs/ctf_name", json="") assert r.status_code == 403 # test_api_config_delete_non_admin """Can a user delete /api/v1/configs/<config_key> if not admin""" r = client.delete("/api/v1/configs/ctf_name", json="") assert r.status_code == 403 assert get_config("ctf_name") == "KMActf" destroy_kmactf(app)
def test_sendmail_with_mailgun_from_db_config(fake_post_request): """Does sendmail work properly with Mailgun using database configuration""" app = create_kmactf() with app.app_context(): app.config["MAILGUN_API_KEY"] = "key-1234567890-file-config" app.config[ "MAILGUN_BASE_URL"] = "https://api.mailgun.net/v3/file.faked.com" # db values should take precedence over file values set_config("mailgun_api_key", "key-1234567890-db-config") set_config("mailgun_base_url", "https://api.mailgun.net/v3/db.faked.com") 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 fake_response = Mock() fake_post_request.return_value = fake_response fake_response.status_code = 200 status, message = sendmail(to_addr, msg) args, kwargs = fake_post_request.call_args assert args[0] == "https://api.mailgun.net/v3/db.faked.com/messages" assert kwargs["auth"] == ("api", u"key-1234567890-db-config") assert kwargs["timeout"] == 1.0 assert kwargs["data"] == { "to": ["*****@*****.**"], "text": "this is a test", "from": "KMActf <*****@*****.**>", "subject": "Message from KMActf", } assert fake_response.status_code == 200 assert status is True assert message == "Email sent" destroy_kmactf(app)
def user_created_notification(addr, name, password): text = safe_format( get_config("user_creation_email_body") or DEFAULT_USER_CREATION_EMAIL_BODY, ctf_name=get_config("ctf_name"), ctf_description=get_config("ctf_description"), url=url_for("views.static_html", _external=True), name=name, password=password, ) subject = safe_format( get_config("user_creation_email_subject") or DEFAULT_USER_CREATION_EMAIL_SUBJECT, ctf_name=get_config("ctf_name"), ) return sendmail(addr=addr, text=text, subject=subject)
def registration_visible(): v = get_config("registration_visibility") if v == "public": return True elif v == "private": return False else: return False
def check_email_is_whitelisted(email_address): local_id, _, domain = email_address.partition("@") domain_whitelist = get_config("domain_whitelist") if domain_whitelist: domain_whitelist = [d.strip() for d in domain_whitelist.split(",")] if domain not in domain_whitelist: return False return True
def accounts_visible(): v = get_config("account_visibility") if v == "public": return True elif v == "private": return authed() elif v == "admins": return is_admin()
def sendmail(addr, text, subject="Message from {ctf_name}"): subject = safe_format(subject, ctf_name=get_config("ctf_name")) provider = get_mail_provider() if provider == "smtp": return smtp.sendmail(addr, text, subject) if provider == "mailgun": return mailgun.sendmail(addr, text, subject) return False, "No mail settings configured"
def challenges_visible(): v = get_config("challenge_visibility") if v == "public": return True elif v == "private": return authed() elif v == "admins": return is_admin()
def confirm(data=None): if not get_config("verify_emails"): # If the CTF doesn't care about confirming email addresses then redierct to challenges return redirect(url_for("challenges.listing")) # User is confirming email account if data and request.method == "GET": try: user_email = unserialize(data, max_age=1800) except (BadTimeSignature, SignatureExpired): return render_template( "confirm.html", errors=["Your confirmation link has expired"]) except (BadSignature, TypeError, base64.binascii.Error): return render_template( "confirm.html", errors=["Your confirmation token is invalid"]) user = Users.query.filter_by(email=user_email).first_or_404() if user.verified: return redirect(url_for("views.settings")) user.verified = True log( "registrations", format="[{date}] {ip} - successful confirmation for {name}", name=user.name, ) db.session.commit() email.successful_registration_notification(user.email) db.session.close() if current_user.authed(): return redirect(url_for("challenges.listing")) return redirect(url_for("auth.login")) # User is trying to start or restart the confirmation flow if not current_user.authed(): return redirect(url_for("auth.login")) user = Users.query.filter_by(id=session["id"]).first_or_404() if user.verified: return redirect(url_for("views.settings")) if data is None: if request.method == "POST": # User wants to resend their confirmation email email.verify_email_address(user.email) log( "registrations", format= "[{date}] {ip} - {name} initiated a confirmation email resend", ) return render_template( "confirm.html", user=user, infos=["Your confirmation email has been resent!"], ) elif request.method == "GET": # User has been directed to the confirm page return render_template("confirm.html", user=user)
def test_api_config_delete_admin(): """Can a user delete /api/v1/configs/<config_key> if admin""" app = create_kmactf() with app.app_context(): with login_as_user(app, "admin") as admin: r = admin.delete("/api/v1/configs/ctf_name", json="") assert r.status_code == 200 assert get_config("ctf_name") is None destroy_kmactf(app)
def test_api_config_patch_admin(): """Can a user patch /api/v1/configs/<config_key> if admin""" app = create_kmactf() with app.app_context(): with login_as_user(app, "admin") as admin: r = admin.patch("/api/v1/configs/ctf_name", json={"value": "Changed_Name"}) assert r.status_code == 200 assert get_config("ctf_name") == "Changed_Name" destroy_kmactf(app)