From 30219cd549e0031ebd5031b6f53be80fc9098e06 Mon Sep 17 00:00:00 2001 From: John Hammond Date: Wed, 6 May 2020 21:12:50 -0400 Subject: [PATCH] Ensured testing of TLS usage for each Docker API call. Stopped unintentional KeyError in finding unused ports --- docker_challenges/__init__.py | 1292 +++++++++++++++++++-------------- 1 file changed, 765 insertions(+), 527 deletions(-) diff --git a/docker_challenges/__init__.py b/docker_challenges/__init__.py index a118fe6..6e02fbc 100644 --- a/docker_challenges/__init__.py +++ b/docker_challenges/__init__.py @@ -4,9 +4,30 @@ from CTFd.utils.user import get_ip from CTFd.utils.uploads import delete_file from CTFd.plugins import register_plugin_assets_directory, bypass_csrf_protection from CTFd.schemas.tags import TagSchema -from CTFd.models import db, ma, Challenges, Teams, Users, Solves, Fails, Flags, Files, Hints, Tags, ChallengeFiles -from CTFd.utils.decorators import admins_only, authed_only, during_ctf_time_only, require_verified_emails -from CTFd.utils.decorators.visibility import check_challenge_visibility, check_score_visibility +from CTFd.models import ( + db, + ma, + Challenges, + Teams, + Users, + Solves, + Fails, + Flags, + Files, + Hints, + Tags, + ChallengeFiles, +) +from CTFd.utils.decorators import ( + admins_only, + authed_only, + during_ctf_time_only, + require_verified_emails, +) +from CTFd.utils.decorators.visibility import ( + check_challenge_visibility, + check_score_visibility, +) from CTFd.utils.user import get_current_team from CTFd.utils.user import get_current_user from CTFd.utils.user import is_admin, authed @@ -16,9 +37,25 @@ from CTFd.api.v1.scoreboard import ScoreboardDetail import CTFd.utils.scores from CTFd.api.v1.challenges import ChallengeList, Challenge from flask_restplus import Namespace, Resource -from flask import request, Blueprint, jsonify, abort, render_template, url_for, redirect, session +from flask import ( + request, + Blueprint, + jsonify, + abort, + render_template, + url_for, + redirect, + session, +) from flask_wtf import FlaskForm -from wtforms import TextField, SubmitField, BooleanField, HiddenField, FileField, SelectMultipleField +from wtforms import ( + TextField, + SubmitField, + BooleanField, + HiddenField, + FileField, + SelectMultipleField, +) from wtforms.validators import DataRequired, ValidationError from werkzeug.utils import secure_filename import requests @@ -30,338 +67,491 @@ import hashlib import random from CTFd.plugins import register_admin_plugin_menu_bar + class DockerConfig(db.Model): - """ + """ Docker Config Model. This model stores the config for docker API connections. """ - id = db.Column(db.Integer, primary_key=True) - hostname = db.Column("hostname",db.String(64), index=True) - tls_enabled = db.Column("tls_enabled",db.Boolean, index=True) - ca_cert = db.Column("ca_cert",db.String, index=True) - client_cert = db.Column("client_cert",db.String, index=True) - client_key = db.Column("client_key",db.String, index=True) - repositories = db.Column("repositories",db.String, index=True) + + id = db.Column(db.Integer, primary_key=True) + hostname = db.Column("hostname", db.String(64), index=True) + tls_enabled = db.Column("tls_enabled", db.Boolean, index=True) + ca_cert = db.Column("ca_cert", db.String, index=True) + client_cert = db.Column("client_cert", db.String, index=True) + client_key = db.Column("client_key", db.String, index=True) + repositories = db.Column("repositories", db.String, index=True) + class DockerChallengeTracker(db.Model): - """ + """ Docker Container Tracker. This model stores the users/teams active docker containers. """ - id = db.Column(db.Integer, primary_key=True) - team_id = db.Column("team_id",db.String, index=True) - user_id = db.Column("user_id",db.String, index=True) - docker_image = db.Column("docker_image",db.String, index=True) - timestamp = db.Column("timestamp",db.Integer, index=True) - revert_time = db.Column("revert_time",db.Integer, index=True) - instance_id = db.Column("instance_id",db.String, index=True) - ports = db.Column('ports', db.String, index=True) - host = db.Column('host', db.String, index=True) + + id = db.Column(db.Integer, primary_key=True) + team_id = db.Column("team_id", db.String, index=True) + user_id = db.Column("user_id", db.String, index=True) + docker_image = db.Column("docker_image", db.String, index=True) + timestamp = db.Column("timestamp", db.Integer, index=True) + revert_time = db.Column("revert_time", db.Integer, index=True) + instance_id = db.Column("instance_id", db.String, index=True) + ports = db.Column("ports", db.String, index=True) + host = db.Column("host", db.String, index=True) + class DockerConfigForm(FlaskForm): - """ + """ Docker Config Form. This Form Handles the Docker Config data. """ - id = HiddenField() - hostname = TextField('Docker Hostname', render_kw={"placeholder": "10.10.10.10:2376", "autofocus" : "true"}, validators=[DataRequired("Hostname name is required")]) - tls_enabled = BooleanField('TLS Enabled?') - ca_cert = FileField('CA Cert') - client_cert = FileField('Client Cert') - client_key = FileField('Client Key') - repositories = SelectMultipleField('Repositories') - submit = SubmitField('Submit') + + id = HiddenField() + hostname = TextField( + "Docker Hostname", + render_kw={"placeholder": "10.10.10.10:2376", "autofocus": "true"}, + validators=[DataRequired("Hostname name is required")], + ) + tls_enabled = BooleanField("TLS Enabled?") + ca_cert = FileField("CA Cert") + client_cert = FileField("Client Cert") + client_key = FileField("Client Key") + repositories = SelectMultipleField("Repositories") + submit = SubmitField("Submit") + def define_docker_admin(app): - admin_docker_config = Blueprint('admin_docker_config', __name__, template_folder='templates', static_folder='assets') - @admin_docker_config.route("/admin/docker_config", methods=["GET", "POST"]) - @admins_only - def docker_config(): - docker = DockerConfig.query.filter_by(id=1).first() - form = DockerConfigForm() - if request.method == "POST": - if docker: - b = docker - else: - b = DockerConfig() - try: ca_cert = request.files['ca_cert'].stream.read() - except: ca_cert = '' - try: client_cert = request.files['client_cert'].stream.read() - except: client_cert = '' - try: client_key = request.files['client_key'].stream.read() - except: client_key = '' - if len(ca_cert) != 0: b.ca_cert = ca_cert - if len(client_cert) != 0: b.client_cert = client_cert - if len(client_key) != 0: b.client_key = client_key - b.hostname = form.hostname.data - b.tls_enabled = form.tls_enabled.data - if not b.tls_enabled: - b.ca_cert = None - b.client_cert = None - b.client_key = None - b.repositories = ','.join(form.repositories.data) or None - db.session.add(b) - db.session.commit() - docker = DockerConfig.query.filter_by(id=1).first() - try: - repos = get_repositories(docker) - except: - repos = list() - if len(repos) == 0: - form.repositories.choices = [("ERROR","Failed to Connect to Docker")] - else: - form.repositories.choices = [(d, d) for d in repos] - dconfig = DockerConfig.query.first() - try: - selected_repos = dconfig.repositories.split(',') - except: - selected_repos = [] - return render_template("docker_config.html", config=dconfig, form=form, repos=selected_repos) - app.register_blueprint(admin_docker_config) + admin_docker_config = Blueprint( + "admin_docker_config", + __name__, + template_folder="templates", + static_folder="assets", + ) + + @admin_docker_config.route("/admin/docker_config", methods=["GET", "POST"]) + @admins_only + def docker_config(): + docker = DockerConfig.query.filter_by(id=1).first() + form = DockerConfigForm() + if request.method == "POST": + if docker: + b = docker + else: + b = DockerConfig() + try: + ca_cert = request.files["ca_cert"].stream.read() + except: + ca_cert = "" + try: + client_cert = request.files["client_cert"].stream.read() + except: + client_cert = "" + try: + client_key = request.files["client_key"].stream.read() + except: + client_key = "" + if len(ca_cert) != 0: + b.ca_cert = ca_cert + if len(client_cert) != 0: + b.client_cert = client_cert + if len(client_key) != 0: + b.client_key = client_key + b.hostname = form.hostname.data + b.tls_enabled = form.tls_enabled.data + if not b.tls_enabled: + b.ca_cert = None + b.client_cert = None + b.client_key = None + b.repositories = ",".join(form.repositories.data) or None + db.session.add(b) + db.session.commit() + docker = DockerConfig.query.filter_by(id=1).first() + try: + repos = get_repositories(docker) + except: + repos = list() + if len(repos) == 0: + form.repositories.choices = [("ERROR", "Failed to Connect to Docker")] + else: + form.repositories.choices = [(d, d) for d in repos] + dconfig = DockerConfig.query.first() + try: + selected_repos = dconfig.repositories.split(",") + except: + selected_repos = [] + return render_template( + "docker_config.html", config=dconfig, form=form, repos=selected_repos + ) + + app.register_blueprint(admin_docker_config) def define_docker_status(app): - admin_docker_status = Blueprint('admin_docker_status', __name__, template_folder='templates', static_folder='assets') - @admin_docker_status.route("/admin/docker_status", methods=["GET", "POST"]) - @admins_only - def docker_admin(): - docker_config = DockerConfig.query.filter_by(id=1).first() - docker_tracker = DockerChallengeTracker.query.all() - for i in docker_tracker: - if is_teams_mode(): - name = Teams.query.filter_by(id=i.team_id).first() - i.team_id = name.name - else: - name = Users.query.filter_by(id=i.user_id).first() - i.user_id = name.name - return render_template("admin_docker_status.html", dockers=docker_tracker) - app.register_blueprint(admin_docker_status) + admin_docker_status = Blueprint( + "admin_docker_status", + __name__, + template_folder="templates", + static_folder="assets", + ) + + @admin_docker_status.route("/admin/docker_status", methods=["GET", "POST"]) + @admins_only + def docker_admin(): + docker_config = DockerConfig.query.filter_by(id=1).first() + docker_tracker = DockerChallengeTracker.query.all() + for i in docker_tracker: + if is_teams_mode(): + name = Teams.query.filter_by(id=i.team_id).first() + i.team_id = name.name + else: + name = Users.query.filter_by(id=i.user_id).first() + i.user_id = name.name + return render_template("admin_docker_status.html", dockers=docker_tracker) + + app.register_blueprint(admin_docker_status) -kill_container = Namespace("nuke", description='Endpoint to nuke containers') -@kill_container.route("", methods=['POST','GET']) +kill_container = Namespace("nuke", description="Endpoint to nuke containers") + + +@kill_container.route("", methods=["POST", "GET"]) class KillContainerAPI(Resource): - @admins_only - def get(self): - container = request.args.get('container') - full = request.args.get('all') - docker_config = DockerConfig.query.filter_by(id=1).first() - docker_tracker = DockerChallengeTracker.query.all() - if full == "true": - for c in docker_tracker: - delete_container(docker_config, c.instance_id) - DockerChallengeTracker.query.filter_by(instance_id=c.instance_id).delete() - db.session.commit() - db.session.close() - elif container != 'null' and container in [c.instance_id for c in docker_tracker]: - delete_container(docker_config, container) - DockerChallengeTracker.query.filter_by(instance_id=container).delete() - db.session.commit() - db.session.close() - else: - return False - return True + @admins_only + def get(self): + container = request.args.get("container") + full = request.args.get("all") + docker_config = DockerConfig.query.filter_by(id=1).first() + docker_tracker = DockerChallengeTracker.query.all() + if full == "true": + for c in docker_tracker: + delete_container(docker_config, c.instance_id) + DockerChallengeTracker.query.filter_by( + instance_id=c.instance_id + ).delete() + db.session.commit() + db.session.close() + elif container != "null" and container in [ + c.instance_id for c in docker_tracker + ]: + delete_container(docker_config, container) + DockerChallengeTracker.query.filter_by(instance_id=container).delete() + db.session.commit() + db.session.close() + else: + return False + return True + # For the Docker Config Page. Gets the Current Repositories available on the Docker Server. def get_repositories(docker, tags=False, repos=False): - tls = docker.tls_enabled - if not tls: - prefix = 'http' - else: - prefix = 'https' - try: - ca = docker.ca_cert - client = docker.client_cert - ckey = docker.client_key - ca_file = tempfile.NamedTemporaryFile(delete=False) - ca_file.write(ca) - ca_file.seek(0) - client_file = tempfile.NamedTemporaryFile(delete=False) - client_file.write(client) - client_file.seek(0) - key_file = tempfile.NamedTemporaryFile(delete=False) - key_file.write(ckey) - key_file.seek(0) - CERT = (client_file.name,key_file.name) - except: - return [] - host = docker.hostname - URL_TEMPLATE = '%s://%s' % (prefix, host) - if tls: - try: - r = requests.get(url="%s/images/json?all=1" % URL_TEMPLATE, cert=CERT, verify=ca_file.name) - except: - return [] - else: - try: - r = requests.get(url="%s/images/json?all=1" % URL_TEMPLATE) - except: - return [] - result = list() - for i in r.json(): - if not i['RepoTags'] == None: - if not i['RepoTags'][0].split(':')[0] == '': - if repos: - if not i['RepoTags'][0].split(':')[0] in repos: - continue - if not tags: - result.append(i['RepoTags'][0].split(':')[0]) - else: - result.append(i['RepoTags'][0]) - return list(set(result)) + tls = docker.tls_enabled + if not tls: + prefix = "http" + else: + prefix = "https" + try: + ca = docker.ca_cert + client = docker.client_cert + ckey = docker.client_key + ca_file = tempfile.NamedTemporaryFile(delete=False) + ca_file.write(ca) + ca_file.seek(0) + client_file = tempfile.NamedTemporaryFile(delete=False) + client_file.write(client) + client_file.seek(0) + key_file = tempfile.NamedTemporaryFile(delete=False) + key_file.write(ckey) + key_file.seek(0) + CERT = (client_file.name, key_file.name) + except: + return [] + host = docker.hostname + URL_TEMPLATE = "%s://%s" % (prefix, host) + if tls: + try: + r = requests.get( + url="%s/images/json?all=1" % URL_TEMPLATE, + cert=CERT, + verify=ca_file.name, + ) + except: + return [] + else: + try: + r = requests.get(url="%s/images/json?all=1" % URL_TEMPLATE) + except: + return [] + result = list() + for i in r.json(): + if not i["RepoTags"] == None: + if not i["RepoTags"][0].split(":")[0] == "": + if repos: + if not i["RepoTags"][0].split(":")[0] in repos: + continue + if not tags: + result.append(i["RepoTags"][0].split(":")[0]) + else: + result.append(i["RepoTags"][0]) + return list(set(result)) + def get_unavailable_ports(docker): - tls = docker.tls_enabled - if not tls: - prefix = 'http' - else: - prefix = 'https' - try: - ca = docker.ca_cert - client = docker.client_cert - ckey = docker.client_key - ca_file = tempfile.NamedTemporaryFile(delete=False) - ca_file.write(ca) - ca_file.seek(0) - client_file = tempfile.NamedTemporaryFile(delete=False) - client_file.write(client) - client_file.seek(0) - key_file = tempfile.NamedTemporaryFile(delete=False) - key_file.write(ckey) - key_file.seek(0) - CERT = (client_file.name,key_file.name) - except: - return [] - host = docker.hostname - URL_TEMPLATE = '%s://%s' % (prefix, host) - r = requests.get(url="%s/containers/json?all=1" % URL_TEMPLATE, cert=CERT, verify=ca_file.name) - result = list() - for i in r.json(): - if not i['Ports'] == []: - for p in i['Ports']: - result.append(p['PublicPort']) - return result + tls = docker.tls_enabled + if not tls: + prefix = "http" + else: + prefix = "https" + try: + ca = docker.ca_cert + client = docker.client_cert + ckey = docker.client_key + ca_file = tempfile.NamedTemporaryFile(delete=False) + ca_file.write(ca) + ca_file.seek(0) + client_file = tempfile.NamedTemporaryFile(delete=False) + client_file.write(client) + client_file.seek(0) + key_file = tempfile.NamedTemporaryFile(delete=False) + key_file.write(ckey) + key_file.seek(0) + CERT = (client_file.name, key_file.name) + except: + return [] + host = docker.hostname + URL_TEMPLATE = "%s://%s" % (prefix, host) + + if tls: + try: + r = requests.get( + url="%s/containers/json?all=1" % URL_TEMPLATE, + cert=CERT, + verify=ca_file.name, + ) + except: + return [] + else: + try: + r = requests.get(url="%s/containers/json?all=1" % URL_TEMPLATE) + except: + return [] + + result = list() + + for i in r.json(): + if not i["Ports"] == []: + for p in i["Ports"]: + if "PublicPort" in p: + result.append(p["PublicPort"]) + return result + def get_required_ports(docker, image): - tls = docker.tls_enabled - if not tls: - prefix = 'http' - else: - prefix = 'https' - try: - ca = docker.ca_cert - client = docker.client_cert - ckey = docker.client_key - ca_file = tempfile.NamedTemporaryFile(delete=False) - ca_file.write(ca) - ca_file.seek(0) - client_file = tempfile.NamedTemporaryFile(delete=False) - client_file.write(client) - client_file.seek(0) - key_file = tempfile.NamedTemporaryFile(delete=False) - key_file.write(ckey) - key_file.seek(0) - CERT = (client_file.name,key_file.name) - except: - return [] - host = docker.hostname - URL_TEMPLATE = '%s://%s' % (prefix, host) - r = requests.get(url="%s/images/%s/json?all=1" % (URL_TEMPLATE, image), cert=CERT, verify=ca_file.name) - result = r.json()['ContainerConfig']['ExposedPorts'].keys() - return result + tls = docker.tls_enabled + if not tls: + prefix = "http" + else: + prefix = "https" + try: + ca = docker.ca_cert + client = docker.client_cert + ckey = docker.client_key + ca_file = tempfile.NamedTemporaryFile(delete=False) + ca_file.write(ca) + ca_file.seek(0) + client_file = tempfile.NamedTemporaryFile(delete=False) + client_file.write(client) + client_file.seek(0) + key_file = tempfile.NamedTemporaryFile(delete=False) + key_file.write(ckey) + key_file.seek(0) + CERT = (client_file.name, key_file.name) + except: + return [] + host = docker.hostname + URL_TEMPLATE = "%s://%s" % (prefix, host) + + if tls: + try: + r = requests.get( + url="%s/images/%s/json?all=1" % (URL_TEMPLATE, image), + cert=CERT, + verify=ca_file.name, + ) + except: + return [] + else: + try: + r = requests.get(url="%s/images/%s/json?all=1" % (URL_TEMPLATE, image)) + except: + return [] + + result = r.json()["ContainerConfig"]["ExposedPorts"].keys() + return result def create_container(docker, image, team, portbl): - tls = docker.tls_enabled - if not tls: - prefix = 'http' - else: - prefix = 'https' - try: - ca = docker.ca_cert - client = docker.client_cert - ckey = docker.client_key - ca_file = tempfile.NamedTemporaryFile(delete=False) - ca_file.write(ca) - ca_file.seek(0) - client_file = tempfile.NamedTemporaryFile(delete=False) - client_file.write(client) - client_file.seek(0) - key_file = tempfile.NamedTemporaryFile(delete=False) - key_file.write(ckey) - key_file.seek(0) - CERT = (client_file.name,key_file.name) - except: - return [] - host = docker.hostname - URL_TEMPLATE = '%s://%s' % (prefix, host) - needed_ports = get_required_ports(docker, image) - team = hashlib.md5(team.encode("utf-8")).hexdigest()[:10] - container_name = "%s_%s" % (image.split(':')[1], team) - assigned_ports = dict() - for i in needed_ports: - while True: - assigned_port = random.choice(range(30000,60000)) - if assigned_port not in portbl: - assigned_ports['%s/tcp' % assigned_port] = { } - break - ports = dict() - bindings = dict() - tmp_ports = assigned_ports.keys() - for i in needed_ports: - ports[i] = { } - bindings[i] = [{ "HostPort": tmp_ports.pop()}] - headers = {'Content-Type': "application/json"} - data = json.dumps({"Image": image, "ExposedPorts": ports, "HostConfig" : { "PortBindings" : bindings } }) - r = requests.post(url="%s/containers/create?name=%s" % (URL_TEMPLATE, container_name), cert=CERT, verify=ca_file.name, data=data, headers=headers) - result = r.json() - s = requests.post(url="%s/containers/%s/start" % (URL_TEMPLATE, result['Id']), cert=CERT, verify=ca_file.name, headers=headers) - return result,data + tls = docker.tls_enabled + if not tls: + prefix = "http" + else: + prefix = "https" + try: + ca = docker.ca_cert + client = docker.client_cert + ckey = docker.client_key + ca_file = tempfile.NamedTemporaryFile(delete=False) + ca_file.write(ca) + ca_file.seek(0) + client_file = tempfile.NamedTemporaryFile(delete=False) + client_file.write(client) + client_file.seek(0) + key_file = tempfile.NamedTemporaryFile(delete=False) + key_file.write(ckey) + key_file.seek(0) + CERT = (client_file.name, key_file.name) + except: + return [] + host = docker.hostname + URL_TEMPLATE = "%s://%s" % (prefix, host) + needed_ports = get_required_ports(docker, image) + team = hashlib.md5(team.encode("utf-8")).hexdigest()[:10] + container_name = "%s_%s" % (image.split(":")[1], team) + assigned_ports = dict() + for i in needed_ports: + while True: + assigned_port = random.choice(range(30000, 60000)) + if assigned_port not in portbl: + assigned_ports["%s/tcp" % assigned_port] = {} + break + ports = dict() + bindings = dict() + tmp_ports = assigned_ports.keys() + for i in needed_ports: + ports[i] = {} + bindings[i] = [{"HostPort": tmp_ports.pop()}] + headers = {"Content-Type": "application/json"} + data = json.dumps( + { + "Image": image, + "ExposedPorts": ports, + "HostConfig": {"PortBindings": bindings}, + } + ) + + if tls: + try: + r = requests.get( + url="%s/containers/create?name=%s" % (URL_TEMPLATE, container_name), + cert=CERT, + verify=ca_file.name, + data=data, + headers=headers, + ) + except: + return [] + else: + try: + r = requests.get( + url="%s/containers/create?name=%s" % (URL_TEMPLATE, container_name), + data=data, + headers=headers, + ) + except: + return [] + + result = r.json() + + if tls: + try: + s = requests.post( + url="%s/containers/%s/start" % (URL_TEMPLATE, result["Id"]), + cert=CERT, + verify=ca_file.name, + headers=headers, + ) + except: + return [] + else: + try: + s = requests.post( + url="%s/containers/%s/start" % (URL_TEMPLATE, result["Id"]), + headers=headers, + ) + except: + return [] + + return result, data + def delete_container(docker, instance_id): - tls = docker.tls_enabled - if not tls: - prefix = 'http' - else: - prefix = 'https' - try: - ca = docker.ca_cert - client = docker.client_cert - ckey = docker.client_key - ca_file = tempfile.NamedTemporaryFile(delete=False) - ca_file.write(ca) - ca_file.seek(0) - client_file = tempfile.NamedTemporaryFile(delete=False) - client_file.write(client) - client_file.seek(0) - key_file = tempfile.NamedTemporaryFile(delete=False) - key_file.write(ckey) - key_file.seek(0) - CERT = (client_file.name,key_file.name) - except: - return [] - host = docker.hostname - URL_TEMPLATE = '%s://%s' % (prefix, host) - headers = {'Content-Type': "application/json"} - r = requests.delete(url="%s/containers/%s?force=true" % (URL_TEMPLATE, instance_id), cert=CERT, verify=ca_file.name, headers=headers) - return True + tls = docker.tls_enabled + if not tls: + prefix = "http" + else: + prefix = "https" + try: + ca = docker.ca_cert + client = docker.client_cert + ckey = docker.client_key + ca_file = tempfile.NamedTemporaryFile(delete=False) + ca_file.write(ca) + ca_file.seek(0) + client_file = tempfile.NamedTemporaryFile(delete=False) + client_file.write(client) + client_file.seek(0) + key_file = tempfile.NamedTemporaryFile(delete=False) + key_file.write(ckey) + key_file.seek(0) + CERT = (client_file.name, key_file.name) + except: + return [] + host = docker.hostname + URL_TEMPLATE = "%s://%s" % (prefix, host) + headers = {"Content-Type": "application/json"} + + if tls: + try: + r = requests.delete( + url="%s/containers/%s?force=true" % (URL_TEMPLATE, instance_id), + cert=CERT, + verify=ca_file.name, + headers=headers, + ) + except: + return [] + else: + try: + r = requests.delete( + url="%s/containers/%s?force=true" % (URL_TEMPLATE, instance_id), + headers=headers, + ) + except: + return [] + + return True + class DockerChallengeType(BaseChallenge): - id = "docker" - name = "docker" - templates = { - 'create': '/plugins/docker_challenges/assets/create.html', - 'update': '/plugins/docker_challenges/assets/update.html', - 'view': '/plugins/docker_challenges/assets/view.html', - } - scripts = { - 'create': '/plugins/docker_challenges/assets/create.js', - 'update': '/plugins/docker_challenges/assets/update.js', - 'view': '/plugins/docker_challenges/assets/view.js', - } - route = '/plugins/docker_challenges/assets' - blueprint = Blueprint('docker_challenges', __name__, template_folder='templates', static_folder='assets') + id = "docker" + name = "docker" + templates = { + "create": "/plugins/docker_challenges/assets/create.html", + "update": "/plugins/docker_challenges/assets/update.html", + "view": "/plugins/docker_challenges/assets/view.html", + } + scripts = { + "create": "/plugins/docker_challenges/assets/create.js", + "update": "/plugins/docker_challenges/assets/update.js", + "view": "/plugins/docker_challenges/assets/view.js", + } + route = "/plugins/docker_challenges/assets" + blueprint = Blueprint( + "docker_challenges", + __name__, + template_folder="templates", + static_folder="assets", + ) - @staticmethod - def update(challenge, request): - """ + @staticmethod + def update(challenge, request): + """ This method is used to update the information associated with a challenge. This should be kept strictly to the Challenges table and any child tables. @@ -369,80 +559,80 @@ class DockerChallengeType(BaseChallenge): :param request: :return: """ - data = request.form or request.get_json() - for attr, value in data.items(): - setattr(challenge, attr, value) + data = request.form or request.get_json() + for attr, value in data.items(): + setattr(challenge, attr, value) - db.session.commit() - return challenge + db.session.commit() + return challenge - @staticmethod - def delete(challenge): - """ + @staticmethod + def delete(challenge): + """ This method is used to delete the resources used by a challenge. NOTE: Will need to kill all containers here :param challenge: :return: """ - Fails.query.filter_by(challenge_id=challenge.id).delete() - Solves.query.filter_by(challenge_id=challenge.id).delete() - Flags.query.filter_by(challenge_id=challenge.id).delete() - files = ChallengeFiles.query.filter_by(challenge_id=challenge.id).all() - for f in files: - delete_file(f.id) - ChallengeFiles.query.filter_by(challenge_id=challenge.id).delete() - Tags.query.filter_by(challenge_id=challenge.id).delete() - Hints.query.filter_by(challenge_id=challenge.id).delete() - Challenges.query.filter_by(id=challenge.id).delete() - DockerChallenge.query.filter_by(id=challenge.id).delete() - db.session.commit() + Fails.query.filter_by(challenge_id=challenge.id).delete() + Solves.query.filter_by(challenge_id=challenge.id).delete() + Flags.query.filter_by(challenge_id=challenge.id).delete() + files = ChallengeFiles.query.filter_by(challenge_id=challenge.id).all() + for f in files: + delete_file(f.id) + ChallengeFiles.query.filter_by(challenge_id=challenge.id).delete() + Tags.query.filter_by(challenge_id=challenge.id).delete() + Hints.query.filter_by(challenge_id=challenge.id).delete() + Challenges.query.filter_by(id=challenge.id).delete() + DockerChallenge.query.filter_by(id=challenge.id).delete() + db.session.commit() - @staticmethod - def read(challenge): - """ + @staticmethod + def read(challenge): + """ This method is in used to access the data of a challenge in a format processable by the front end. :param challenge: :return: Challenge object, data dictionary to be returned to the user """ - challenge = DockerChallenge.query.filter_by(id=challenge.id).first() - data = { - 'id': challenge.id, - 'name': challenge.name, - 'value': challenge.value, - 'docker_image': challenge.docker_image, - 'description': challenge.description, - 'category': challenge.category, - 'state': challenge.state, - 'max_attempts': challenge.max_attempts, - 'type': challenge.type, - 'type_data': { - 'id': DockerChallengeType.id, - 'name': DockerChallengeType.name, - 'templates': DockerChallengeType.templates, - 'scripts': DockerChallengeType.scripts, - } - } - return data + challenge = DockerChallenge.query.filter_by(id=challenge.id).first() + data = { + "id": challenge.id, + "name": challenge.name, + "value": challenge.value, + "docker_image": challenge.docker_image, + "description": challenge.description, + "category": challenge.category, + "state": challenge.state, + "max_attempts": challenge.max_attempts, + "type": challenge.type, + "type_data": { + "id": DockerChallengeType.id, + "name": DockerChallengeType.name, + "templates": DockerChallengeType.templates, + "scripts": DockerChallengeType.scripts, + }, + } + return data - @staticmethod - def create(request): - """ + @staticmethod + def create(request): + """ This method is used to process the challenge creation request. :param request: :return: """ - data = request.form or request.get_json() - challenge = DockerChallenge(**data) - db.session.add(challenge) - db.session.commit() - return challenge + data = request.form or request.get_json() + challenge = DockerChallenge(**data) + db.session.add(challenge) + db.session.commit() + return challenge - @staticmethod - def attempt(challenge, request): - """ + @staticmethod + def attempt(challenge, request): + """ This method is used to check whether a given input is right or wrong. It does not make any changes and should return a boolean for correctness and a string to be shown to the user. It is also in charge of parsing the user's input from the request itself. @@ -451,50 +641,64 @@ class DockerChallengeType(BaseChallenge): :param request: The request the user submitted :return: (boolean, string) """ - data = request.form or request.get_json() - submission = data["submission"].strip() - flags = Flags.query.filter_by(challenge_id=challenge.id).all() - for flag in flags: - if get_flag_class(flag.type).compare(flag, submission): - return True, "Correct" - return False, "Incorrect" + data = request.form or request.get_json() + submission = data["submission"].strip() + flags = Flags.query.filter_by(challenge_id=challenge.id).all() + for flag in flags: + if get_flag_class(flag.type).compare(flag, submission): + return True, "Correct" + return False, "Incorrect" - @staticmethod - def solve(user, team, challenge, request): - """ + @staticmethod + def solve(user, team, challenge, request): + """ This method is used to insert Solves into the database in order to mark a challenge as solved. :param team: The Team object from the database :param chal: The Challenge object from the database :param request: The request the user submitted :return: - """ - data = request.form or request.get_json() - submission = data["submission"].strip() - docker = DockerConfig.query.filter_by(id=1).first() - try: - if is_teams_mode(): - docker_containers = DockerChallengeTracker.query.filter_by(docker_image=challenge.docker_image).filter_by(team_id=team.id).first() - else: - docker_containers = DockerChallengeTracker.query.filter_by(docker_image=challenge.docker_image).filter_by(user_id=user.id).first() - delete_container(docker, docker_containers.instance_id) - DockerChallengeTracker.query.filter_by(instance_id=docker_containers.instance_id).delete() - except: - pass - solve = Solves( - user_id=user.id, - team_id=team.id if team else None, - challenge_id=challenge.id, - ip=get_ip(req=request), - provided=submission, - ) - db.session.add(solve) - db.session.commit() - db.session.close() - - @staticmethod - def fail(user, team, challenge, request): """ + data = request.form or request.get_json() + submission = data["submission"].strip() + docker = DockerConfig.query.filter_by(id=1).first() + try: + if is_teams_mode(): + docker_containers = ( + DockerChallengeTracker.query.filter_by( + docker_image=challenge.docker_image + ) + .filter_by(team_id=team.id) + .first() + ) + else: + docker_containers = ( + DockerChallengeTracker.query.filter_by( + docker_image=challenge.docker_image + ) + .filter_by(user_id=user.id) + .first() + ) + delete_container(docker, docker_containers.instance_id) + DockerChallengeTracker.query.filter_by( + instance_id=docker_containers.instance_id + ).delete() + except: + pass + solve = Solves( + user_id=user.id, + team_id=team.id if team else None, + challenge_id=challenge.id, + ip=get_ip(req=request), + provided=submission, + ) + db.session.add(solve) + db.session.commit() + db.session.close() + + @staticmethod + def fail(user, team, challenge, request): + """ This method is used to insert Fails into the database in order to mark an answer incorrect. :param team: The Team object from the database @@ -502,154 +706,188 @@ class DockerChallengeType(BaseChallenge): :param request: The request the user submitted :return: """ - data = request.form or request.get_json() - submission = data["submission"].strip() - wrong = Fails( - user_id=user.id, - team_id=team.id if team else None, - challenge_id=challenge.id, - ip=get_ip(request), - provided=submission, - ) - db.session.add(wrong) - db.session.commit() - db.session.close() + data = request.form or request.get_json() + submission = data["submission"].strip() + wrong = Fails( + user_id=user.id, + team_id=team.id if team else None, + challenge_id=challenge.id, + ip=get_ip(request), + provided=submission, + ) + db.session.add(wrong) + db.session.commit() + db.session.close() + class DockerChallenge(Challenges): - __mapper_args__ = {'polymorphic_identity': 'docker'} - id = db.Column(None, db.ForeignKey('challenges.id'), primary_key=True) - docker_image = db.Column(db.String, index=True) + __mapper_args__ = {"polymorphic_identity": "docker"} + id = db.Column(None, db.ForeignKey("challenges.id"), primary_key=True) + docker_image = db.Column(db.String, index=True) + # API -container_namespace = Namespace("container", description='Endpoint to interact with containers') -@container_namespace.route("", methods=['POST','GET']) -class ContainerAPI(Resource): - @authed_only - # I wish this was Post... Issues with API/CSRF and whatnot. Open to a Issue solving this. - def get(self): - container = request.args.get('name') - if not container: - return abort(403) - docker = DockerConfig.query.filter_by(id=1).first() - containers = DockerChallengeTracker.query.all() - if container not in get_repositories(docker, tags=True): - return abort(403) - if is_teams_mode(): - session = get_current_team() - # First we'll delete all old docker containers (+2 hours) - for i in containers: - if int(session.id) == int(i.team_id) and (unix_time(datetime.utcnow()) - int(i.timestamp)) >= 7200: - delete_container(docker, i.instance_id) - DockerChallengeTracker.query.filter_by(instance_id=i.instance_id).delete() - db.session.commit() - check = DockerChallengeTracker.query.filter_by(team_id=session.id).filter_by(docker_image=container).first() - else: - session = get_current_user() - for i in containers: - if int(session.id) == int(i.user_id) and (unix_time(datetime.utcnow()) - int(i.timestamp)) >= 7200: - delete_container(docker, i.instance_id) - DockerChallengeTracker.query.filter_by(instance_id=i.instance_id).delete() - db.session.commit() - check = DockerChallengeTracker.query.filter_by(user_id=session.id).filter_by(docker_image=container).first() - # If this container is already created, we don't need another one. - if check != None and not (unix_time(datetime.utcnow()) - int(check.timestamp)) >= 300: - return abort(403) - # The exception would be if we are reverting a box. So we'll delete it if it exists and has been around for more than 5 minutes. - elif check != None: - delete_container(docker, check.instance_id) - if is_teams_mode(): - DockerChallengeTracker.query.filter_by(team_id=session.id).filter_by(docker_image=container).delete() - else: - DockerChallengeTracker.query.filter_by(user_id=session.id).filter_by(docker_image=container).delete() - db.session.commit() - portsbl = get_unavailable_ports(docker) - create = create_container(docker,container,session.name,portsbl) - ports = json.loads(create[1])['HostConfig']['PortBindings'].values() - entry = DockerChallengeTracker( - team_id = session.id if is_teams_mode() else None, - user_id = session.id if not is_teams_mode() else None, - docker_image = container, - timestamp = unix_time(datetime.utcnow()), - revert_time = unix_time(datetime.utcnow()) + 300, - instance_id = create[0]['Id'], - ports = ','.join([p[0]['HostPort'] for p in ports]), - host = str(docker.hostname).split(':')[0] - ) - db.session.add(entry) - db.session.commit() - db.session.close() - return +container_namespace = Namespace( + "container", description="Endpoint to interact with containers" +) -active_docker_namespace = Namespace("docker", description='Endpoint to retrieve User Docker Image Status') -@active_docker_namespace.route("", methods=['POST','GET']) + +@container_namespace.route("", methods=["POST", "GET"]) +class ContainerAPI(Resource): + @authed_only + # I wish this was Post... Issues with API/CSRF and whatnot. Open to a Issue solving this. + def get(self): + container = request.args.get("name") + if not container: + return abort(403) + docker = DockerConfig.query.filter_by(id=1).first() + containers = DockerChallengeTracker.query.all() + if container not in get_repositories(docker, tags=True): + return abort(403) + if is_teams_mode(): + session = get_current_team() + # First we'll delete all old docker containers (+2 hours) + for i in containers: + if ( + int(session.id) == int(i.team_id) + and (unix_time(datetime.utcnow()) - int(i.timestamp)) >= 7200 + ): + delete_container(docker, i.instance_id) + DockerChallengeTracker.query.filter_by( + instance_id=i.instance_id + ).delete() + db.session.commit() + check = ( + DockerChallengeTracker.query.filter_by(team_id=session.id) + .filter_by(docker_image=container) + .first() + ) + else: + session = get_current_user() + for i in containers: + if ( + int(session.id) == int(i.user_id) + and (unix_time(datetime.utcnow()) - int(i.timestamp)) >= 7200 + ): + delete_container(docker, i.instance_id) + DockerChallengeTracker.query.filter_by( + instance_id=i.instance_id + ).delete() + db.session.commit() + check = ( + DockerChallengeTracker.query.filter_by(user_id=session.id) + .filter_by(docker_image=container) + .first() + ) + # If this container is already created, we don't need another one. + if ( + check != None + and not (unix_time(datetime.utcnow()) - int(check.timestamp)) >= 300 + ): + return abort(403) + # The exception would be if we are reverting a box. So we'll delete it if it exists and has been around for more than 5 minutes. + elif check != None: + delete_container(docker, check.instance_id) + if is_teams_mode(): + DockerChallengeTracker.query.filter_by(team_id=session.id).filter_by( + docker_image=container + ).delete() + else: + DockerChallengeTracker.query.filter_by(user_id=session.id).filter_by( + docker_image=container + ).delete() + db.session.commit() + portsbl = get_unavailable_ports(docker) + create = create_container(docker, container, session.name, portsbl) + ports = json.loads(create[1])["HostConfig"]["PortBindings"].values() + entry = DockerChallengeTracker( + team_id=session.id if is_teams_mode() else None, + user_id=session.id if not is_teams_mode() else None, + docker_image=container, + timestamp=unix_time(datetime.utcnow()), + revert_time=unix_time(datetime.utcnow()) + 300, + instance_id=create[0]["Id"], + ports=",".join([p[0]["HostPort"] for p in ports]), + host=str(docker.hostname).split(":")[0], + ) + db.session.add(entry) + db.session.commit() + db.session.close() + return + + +active_docker_namespace = Namespace( + "docker", description="Endpoint to retrieve User Docker Image Status" +) + + +@active_docker_namespace.route("", methods=["POST", "GET"]) class DockerStatus(Resource): - """ + """ The Purpose of this API is to retrieve a public JSON string of all docker containers in use by the current team/user. """ - @authed_only - def get(self): - docker = DockerConfig.query.filter_by(id=1).first() - if is_teams_mode(): - session = get_current_team() - tracker = DockerChallengeTracker.query.filter_by(team_id=session.id) - else: - session = get_current_user() - tracker = DockerChallengeTracker.query.filter_by(user_id=session.id) - data = list() - for i in tracker: - data.append({ - 'id' : i.id, - 'team_id' : i.team_id, - 'user_id' : i.user_id, - 'docker_image' : i.docker_image, - 'timestamp' : i.timestamp, - 'revert_time' : i.revert_time, - 'instance_id' : i.instance_id, - 'ports' : i.ports.split(','), - 'host' : str(docker.hostname).split(':')[0] - }) - return { - 'success' : True, - 'data' : data - } -docker_namespace = Namespace("docker", description='Endpoint to retrieve dockerstuff') -@docker_namespace.route("", methods=['POST','GET']) + @authed_only + def get(self): + docker = DockerConfig.query.filter_by(id=1).first() + if is_teams_mode(): + session = get_current_team() + tracker = DockerChallengeTracker.query.filter_by(team_id=session.id) + else: + session = get_current_user() + tracker = DockerChallengeTracker.query.filter_by(user_id=session.id) + data = list() + for i in tracker: + data.append( + { + "id": i.id, + "team_id": i.team_id, + "user_id": i.user_id, + "docker_image": i.docker_image, + "timestamp": i.timestamp, + "revert_time": i.revert_time, + "instance_id": i.instance_id, + "ports": i.ports.split(","), + "host": str(docker.hostname).split(":")[0], + } + ) + return {"success": True, "data": data} + + +docker_namespace = Namespace("docker", description="Endpoint to retrieve dockerstuff") + + +@docker_namespace.route("", methods=["POST", "GET"]) class DockerAPI(Resource): - """ + """ This is for creating Docker Challenges. The purpose of this API is to populate the Docker Image Select form object in the Challenge Creation Screen. """ - @admins_only - def get(self): - docker = DockerConfig.query.filter_by(id=1).first() - images = get_repositories(docker, tags=True, repos=docker.repositories) - if images: - data = list() - for i in images: - data.append({'name':i}) - return { - 'success' : True, - 'data' : data - } - else: - return { - 'success' : False, - 'data' : [ - { - 'name':'Error in Docker Config!' - } - ] - }, 400 + + @admins_only + def get(self): + docker = DockerConfig.query.filter_by(id=1).first() + images = get_repositories(docker, tags=True, repos=docker.repositories) + if images: + data = list() + for i in images: + data.append({"name": i}) + return {"success": True, "data": data} + else: + return ( + {"success": False, "data": [{"name": "Error in Docker Config!"}]}, + 400, + ) + def load(app): - app.db.create_all() - CHALLENGE_CLASSES['docker'] = DockerChallengeType - register_plugin_assets_directory(app, base_path='/plugins/docker_challenges/assets') - define_docker_admin(app) - define_docker_status(app) - CTFd_API_v1.add_namespace(docker_namespace, '/docker') - CTFd_API_v1.add_namespace(container_namespace, '/container') - CTFd_API_v1.add_namespace(active_docker_namespace, '/docker_status') - CTFd_API_v1.add_namespace(kill_container, '/nuke') \ No newline at end of file + app.db.create_all() + CHALLENGE_CLASSES["docker"] = DockerChallengeType + register_plugin_assets_directory(app, base_path="/plugins/docker_challenges/assets") + define_docker_admin(app) + define_docker_status(app) + CTFd_API_v1.add_namespace(docker_namespace, "/docker") + CTFd_API_v1.add_namespace(container_namespace, "/container") + CTFd_API_v1.add_namespace(active_docker_namespace, "/docker_status") + CTFd_API_v1.add_namespace(kill_container, "/nuke")