894 lines
29 KiB
Python
894 lines
29 KiB
Python
from CTFd.plugins.challenges import BaseChallenge, CHALLENGE_CLASSES, get_chal_class
|
|
from CTFd.plugins.flags import get_flag_class
|
|
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.utils.user import get_current_team
|
|
from CTFd.utils.user import get_current_user
|
|
from CTFd.utils.user import is_admin, authed
|
|
from CTFd.utils.config import is_teams_mode
|
|
from CTFd.api import CTFd_API_v1
|
|
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_wtf import FlaskForm
|
|
from wtforms import (
|
|
TextField,
|
|
SubmitField,
|
|
BooleanField,
|
|
HiddenField,
|
|
FileField,
|
|
SelectMultipleField,
|
|
)
|
|
from wtforms.validators import DataRequired, ValidationError
|
|
from werkzeug.utils import secure_filename
|
|
import requests
|
|
import tempfile
|
|
from CTFd.utils.dates import unix_time
|
|
from datetime import datetime
|
|
import json
|
|
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)
|
|
|
|
|
|
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)
|
|
|
|
|
|
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")
|
|
|
|
|
|
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)
|
|
|
|
|
|
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)
|
|
|
|
|
|
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
|
|
|
|
|
|
# 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] == "<none>":
|
|
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)
|
|
|
|
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)
|
|
|
|
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},
|
|
}
|
|
)
|
|
|
|
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"}
|
|
|
|
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",
|
|
)
|
|
|
|
@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.
|
|
|
|
:param challenge:
|
|
:param request:
|
|
:return:
|
|
"""
|
|
data = request.form or request.get_json()
|
|
for attr, value in data.items():
|
|
setattr(challenge, attr, value)
|
|
|
|
db.session.commit()
|
|
return 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()
|
|
|
|
@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
|
|
|
|
@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
|
|
|
|
@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.
|
|
|
|
:param challenge: The Challenge object from the database
|
|
: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"
|
|
|
|
@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):
|
|
"""
|
|
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
|
|
: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()
|
|
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)
|
|
|
|
|
|
# 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
|
|
|
|
|
|
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"])
|
|
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,
|
|
)
|
|
|
|
|
|
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")
|