Initial Commit

master
stitch 2020-05-04 14:16:16 -04:00
parent a52b749528
commit 7d779af2bc
11 changed files with 1446 additions and 0 deletions

64
README.md Normal file
View File

@ -0,0 +1,64 @@
# CTFd Docker Plugin
This plugin for CTFd will allow your competing teams/users to start dockerized images for presented challenges. It adds a challenge type "docker" that can be assigned a specific docker image/tag. A few notable requirements:
* Docker Config must be set first. You can access this via `/admin/docker_config`. Currently supported config is pure http (no encryption/authentication) or full TLS with client certificate validation. Configuration information for TLS can be found here: https://docs.docker.com/engine/security/https/
* This plugin is written so that challenges are stored by tags. For example, StormCTF stores all docker challenges for InfoSeCon2019 in the `stormctf/infosecon2019` repository. A challenge example would be `stormctf/infosecon2019:arbit`. This is how you would call the challenge when creating a new challenge.
## Important Notes
* It is unknown if using the same tag twice will cause issues. This plugin was written to avoid this issue, but it has not been fully tested.
* As with all plugins, please security test your Scoreboard before launching the CTF. This plugin has been tested and vetted in the StormCTF environment, but yours may vary.
* In 2.3.3 a CTFd Configuration change is *REQUIRED*. Specifically, https://github.com/CTFd/CTFd/issues/1370. You will need to replace the function `get_configurable_plugins` with the one in the solution. This allows `config.json` to be a list, which allows multiple Menu items per plugin for the Plugins dropdown. You may want to change any other plugins you install to accommodate this. It's as simple as enclosing the curly braces with square braces. Example below.
```
# Original config.json
{
"name": "Another Plugin",
"route": "/admin/plugin/route"
}
```
```
# Modified config.json
[{
"name": "Another Plugin",
"route": "/admin/plugin/route"
}]
```
*NOTE: The above config.json modification only applies to OTHER plugins installed.*
*Requires flask_wtf*
`pip install flask_wtf`
## Features
* Allows players to create their own docker container for docker challenges.
* 5 minute rever timer.
* 2 hour stale container nuke.
* Status panel for Admins to manage docker containers currently active.
* Support for client side validation TLS docker api connections (HIGHLY RECOMMENDED).
* Docker container kill on solve.
* (Mostly) Seamless integration with CTFd.
* *Untested*: _Should_ be able to seamlessly integrate with other challenge types.
## Installation / Configuration
* Make the above required code change in CTFd 2.3.3 (`get_configurable_plugins`).
* Drop the folder `docker_challenges` into `CTFd/CTFd/plugins` (Exactly this name).
* Restart CTFd.
* Navigate to `/admin/docker_config`. Add your configuration information. Click Submit.
* Add your required repositories for this CTF. You can select multiple by holding CTRL when clicking. Click Submit.
* Click Challenges, Select `docker` for challenge type. Create a challenge as normal, but select the correct docker tag for this challenge.
* Double check the front end shows "Start Docker Instance" on the challenge.
* Confirm users are able to start/revert and access docker challenges.
* Host an awesome CTF!
### Update: 20200504
Works with 2.3.3
* Updated the entire plugin to work with the new CTFd.
#### Credits
* https://github.com/offsecginger (Twitter: @offsec_ginger)
* Jaime Geiger (For Original Plugin assistance) (Twitter: @jgeigerm)

View File

@ -0,0 +1,655 @@
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)
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
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
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
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
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')

View File

@ -0,0 +1,70 @@
<form method="POST" action="{{ script_root }}/admin/challenge/new" enctype="multipart/form-data" name='docker_form'>
<div class="form-group">
<label>
Name:<br>
<small class="form-text text-muted">
The name of your challenge
</small>
</label>
<input type="text" class="form-control" name="name" placeholder="Enter challenge name">
</div>
<div class="form-group">
<label>
Category:<br>
<small class="form-text text-muted">
The category of your challenge
</small>
</label>
<input type="text" class="form-control" name="category" placeholder="Enter challenge category">
</div>
<div class="form-group">
<label for="DockerImage" id='dockerimage_label'>Docker Image:
<i class="far fa-question-circle text-muted cursor-help" data-toggle="tooltip" data-placement="right" title="The docker image for your challenge"></i>
</label>
<select id="dockerimage_select" name="docker_image" class="form-control" required></select>
</div>
<ul class="nav nav-tabs" role="tablist" id="new-desc-edit">
<li class="nav-item">
<a class="nav-link active" href="#new-desc-write" aria-controls="home" role="tab"
data-toggle="tab">Write</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#new-desc-preview" aria-controls="home" role="tab" data-toggle="tab">Preview</a>
</li>
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="new-desc-write">
<div class="form-group">
<label>
Message:<br>
<small class="form-text text-muted">
Use this to give a brief introduction to your challenge.
</small>
</label>
<textarea id="new-desc-editor" class="form-control" name="description" rows="10"></textarea>
</div>
</div>
<div role="tabpanel" class="tab-pane content" id="new-desc-preview" style="height:234px; overflow-y: scroll;">
</div>
</div>
<div class="form-group">
<label>
Value:<br>
<small class="form-text text-muted">
This is how many points are rewarded for solving this challenge.
</small>
</label>
<input type="number" class="form-control" name="value" placeholder="Enter value" required>
</div>
<input type="hidden" name="state" value="hidden">
<input type="hidden" name="type" value="standard">
<input type="hidden" value="docker" name="type" id="chaltype">
<div class="form-group">
<button class="btn btn-primary float-right create-challenge-submit" type="submit">Create</button>
</div>
</form>

View File

@ -0,0 +1,26 @@
CTFd.plugin.run((_CTFd) => {
const $ = _CTFd.lib.$
const md = _CTFd.lib.markdown()
$('a[href="#new-desc-preview"]').on('shown.bs.tab', function (event) {
if (event.target.hash == '#new-desc-preview') {
var editor_value = $('#new-desc-editor').val();
$(event.target.hash).html(
md.render(editor_value)
);
}
});
$(document).ready(function(){
$('[data-toggle="tooltip"]').tooltip();
$.getJSON("/api/v1/docker", function(result){
$.each(result['data'], function(i, item){
if (item.name == 'Error in Docker Config!') {
document.docker_form.dockerimage_select.disabled = true;
$("label[for='DockerImage']").text('Docker Image ' + item.name)
}
else {
$("#dockerimage_select").append($("<option />").val(item.name).text(item.name));
}
});
});
});
});

View File

@ -0,0 +1,65 @@
<form method="POST">
<div class="form-group">
<label>
Name<br>
<small class="form-text text-muted">Challenge Name</small>
</label>
<input type="text" class="form-control chal-name" name="name" value="{{ challenge.name }}">
</div>
<div class="form-group">
<label>
Category<br>
<small class="form-text text-muted">Challenge Category</small>
</label>
<input type="text" class="form-control chal-category" name="category" value="{{ challenge.category }}">
</div>
<div class="form-group">
<label for="DockerImage" id='dockerimage_label'>Docker Image:
<i class="far fa-question-circle text-muted cursor-help" data-toggle="tooltip" data-placement="right" title="The docker image for your challenge"></i>
</label>
<select id="dockerimage_select" name="docker_image" class="form-control" required></select>
</div>
<div class="form-group">
<label>
Message<br>
<small class="form-text text-muted">
Use this to give a brief introduction to your challenge.
</small>
</label>
<textarea id="desc-editor" class="form-control chal-desc-editor" name="description" rows="10">{{ challenge.description }}</textarea>
</div>
<div class="form-group">
<label for="value">
Value<br>
<small class="form-text text-muted">
This is how many points teams will receive once they solve this challenge.
</small>
</label>
<input type="number" class="form-control chal-value" name="value" value="{{ challenge.value }}" required>
</div>
<div class="form-group">
<label>
Max Attempts<br>
<small class="form-text text-muted">Maximum amount of attempts users receive. Leave at 0 for unlimited.</small>
</label>
<input type="number" class="form-control chal-attempts" name="max_attempts" value="{{ challenge.max_attempts }}">
</div>
<div class="form-group">
<label>
State<br>
<small class="form-text text-muted">Changes the state of the challenge (e.g. visible, hidden)</small>
</label>
<select class="form-control" name="state">
<option value="visible" {% if challenge.state=="visible" %}selected{% endif %}>Visible</option>
<option value="hidden" {% if challenge.state=="hidden" %}selected{% endif %}>Hidden</option>
</select>
</div>
<div>
<button class="btn btn-success btn-outlined float-right" type="submit">
Update
</button>
</div>
</form>
<script>
var DOCKER_IMAGE = '{{ challenge.docker_image }}';
</script>

View File

@ -0,0 +1,12 @@
CTFd.plugin.run((_CTFd) => {
const $ = _CTFd.lib.$
const md = _CTFd.lib.markdown()
$(document).ready(function() {
$.getJSON("/api/v1/docker", function(result) {
$.each(result['data'], function(i, item) {
$("#dockerimage_select").append($("<option />").val(item.name).text(item.name));
});
$("#dockerimage_select").val(DOCKER_IMAGE).change();
});
});
});

View File

@ -0,0 +1,127 @@
<script>
var DOCKER_CONTAINER = '{{ docker_image | safe }}';
(function() {
get_docker_status(DOCKER_CONTAINER);
})();
</script>
<script src='/plugins/docker_challenges/assets/ezq.js'></script>
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-body">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link active" href="#challenge">Challenge</a>
</li>
{% if solves == None %}
{% else %}
<li class="nav-item">
<a class="nav-link challenge-solves" href="#solves">
{{ solves }} {% if solves > 1 %}Solves{% else %}Solves{% endif %}
</a>
</li>
{% endif %}
</ul>
<div role="tabpanel">
<div class="tab-content">
<div role="tabpanel" class="tab-pane fade show active" id="challenge">
<h2 class='challenge-name text-center pt-3'>{{ name }}</h2>
<h3 class="challenge-value text-center">{{ value }}</h3>
<div class="challenge-tags text-center">
{% for tag in tags %}
<span class='badge badge-info challenge-tag'>{{ tag }}</span>
{% endfor %}
</div>
<span class="challenge-desc">{{ description | safe }}</span>
<div class='mb-3 text-center' id='docker_container'>
<span>
<a onclick="start_container('{{ docker_image | safe }}');" class='btn btn-dark'>
<small style='color:white;'><i class="fas fa-play"></i> Start Docker Instance</small>
</a>
</span>
</div>
<div class="challenge-hints hint-row row">
{% for hint in hints %}
<div class='col-md-12 hint-button-wrapper text-center mb-3'>
<a class="btn btn-info btn-hint btn-block load-hint" href="javascript:;" data-hint-id="{{ hint.id }}">
{% if hint.hint %}
<small>
View Hint
</small>
{% else %}
{% if hint.cost %}
<small>
Unlock Hint for {{ hint.cost }} points
</small>
{% else %}
<small>
View Hint
</small>
{% endif %}
{% endif %}
</a>
</div>
{% endfor %}
</div>
<div class="row challenge-files text-center pb-3">
{% for file in files %}
<div class='col-md-4 col-sm-4 col-xs-12 file-button-wrapper d-block'>
<a class='btn btn-info btn-file mb-1 d-inline-block px-2 w-100 text-truncate' href='{{ file }}'>
<i class="fas fa-download"></i>
<small>
{% set segments = file.split('/') %}
{% set file = segments | last %}
{% set token = file.split('?') | last %}
{% if token %}
{{ file | replace("?" + token, "") }}
{% else %}
{{ file }}
{% endif %}
</small>
</a>
</div>
{% endfor %}
</div>
<div class="row submit-row">
<div class="col-md-9 form-group">
<input class="form-control" type="text" name="answer" id="submission-input" placeholder="Flag" />
<input id="challenge-id" type="hidden" value="{{ id }}">
</div>
<div class="col-md-3 form-group key-submit">
<button type="submit" id="submit-key" tabindex="5" class="btn btn-md btn-outline-secondary float-right">Submit
</button>
</div>
</div>
<div class="row notification-row">
<div class="col-md-12">
<div id="result-notification" class="alert alert-dismissable text-center w-100" role="alert" style="display: none;">
<strong id="result-message"></strong>
</div>
</div>
</div>
</div>
<div role="tabpanel" class="tab-pane fade" id="solves">
<div class="row">
<div class="col-md-12">
<table class="table table-striped text-center">
<thead>
<tr>
<td><b>Name</b>
</td>
<td><b>Date</b>
</td>
</tr>
</thead>
<tbody id="challenge-solves-names">
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,125 @@
CTFd._internal.challenge.data = undefined
CTFd._internal.challenge.renderer = CTFd.lib.markdown();
CTFd._internal.challenge.preRender = function() {}
CTFd._internal.challenge.render = function(markdown) {
return CTFd._internal.challenge.renderer.render(markdown)
}
CTFd._internal.challenge.postRender = function() {}
CTFd._internal.challenge.submit = function(preview) {
var challenge_id = parseInt(CTFd.lib.$('#challenge-id').val())
var submission = CTFd.lib.$('#submission-input').val()
var body = {
'challenge_id': challenge_id,
'submission': submission,
}
var params = {}
if (preview) {
params['preview'] = true
}
return CTFd.api.post_challenge_attempt(params, body).then(function(response) {
if (response.status === 429) {
// User was ratelimited but process response
return response
}
if (response.status === 403) {
// User is not logged in or CTF is paused.
return response
}
return response
})
};
function get_docker_status(container) {
$.get("/api/v1/docker_status", function(result) {
$.each(result['data'], function(i, item) {
if (item.docker_image == container) {
var ports = String(item.ports).split(',');
var data = '';
$.each(ports, function(x, port) {
port = String(port)
data = data + 'Host: ' + item.host + ' Port: ' + port + '<br />';
})
$('#docker_container').html('<pre>Docker Container Information:<br />' + data + '<div class="mt-2" id="' + String(item.instance_id).substring(0,10) + '_revert_container"></div>');
var countDownDate = new Date(parseInt(item.revert_time) * 1000).getTime();
var x = setInterval(function() {
var now = new Date().getTime();
var distance = countDownDate - now;
var minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60));
var seconds = Math.floor((distance % (1000 * 60)) / 1000);
if (seconds < 10) {
seconds = "0" + seconds
}
$("#" + String(item.instance_id).substring(0,10) + "_revert_container").html('Next Revert Available in ' + minutes + ':' + seconds);
if (distance < 0) {
clearInterval(x);
$("#" + String(item.instance_id).substring(0,10) + "_revert_container").html('<a onclick="start_container(\'' + item.docker_image + '\');" class=\'btn btn-dark\'><small style=\'color:white;\'><i class="fas fa-redo"></i> Revert</small></a>');
}
}, 1000);
return false;
};
});
});
};
function start_container(container) {
$('#docker_container').html('<div class="text-center"><i class="fas fa-circle-notch fa-spin fa-1x"></i></div>');
$.get("/api/v1/container", { 'name': container }, function(result) {
get_docker_status(container);
})
.fail(function(jqxhr, settings, ex) {
ezal({
title: "Attention!",
body: "You can only revert a container once per 5 minutes! Please be patient.",
button: "Got it!"
});
$(get_docker_status(container));
});
}
var modal =
'<div class="modal fade" tabindex="-1" role="dialog">' +
' <div class="modal-dialog" role="document">' +
' <div class="modal-content">' +
' <div class="modal-header">' +
' <h5 class="modal-title">{0}</h5>' +
' <button type="button" class="close" data-dismiss="modal" aria-label="Close">' +
' <span aria-hidden="true">&times;</span>' +
" </button>" +
" </div>" +
' <div class="modal-body">' +
" <p>{1}</p>" +
" </div>" +
' <div class="modal-footer">' +
" </div>" +
" </div>" +
" </div>" +
"</div>";
function ezal(args) {
var res = modal.format(args.title, args.body);
var obj = $(res);
var button = '<button type="button" class="btn btn-primary" data-dismiss="modal">{0}</button>'.format(
args.button
);
obj.find(".modal-footer").append(button);
$("main").append(obj);
obj.modal("show");
$(obj).on("hidden.bs.modal", function(e) {
$(this).modal("dispose");
});
return obj;
}

View File

@ -0,0 +1,10 @@
[
{
"name": "Docker Config",
"route": "/admin/docker_config"
},
{
"name": "Docker Status",
"route": "/admin/docker_status"
}
]

View File

@ -0,0 +1,191 @@
{% extends 'admin/base.html' %}
{% block content %}
<div class="jumbotron">
<div class="container">
<h1>Docker Status</h1>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-md-6 offset-md-3">
{% for error in errors %}
<div class="alert alert-danger alert-dismissable" role="alert">
<span class="sr-only">Error:</span>
{{ error }}
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>
</div>
{% endfor %}
{% if dockers %}
<table id='dockers' class="table table-striped">
<thead>
<tr>
<th width="10px" class="text-center" style="cursor: pointer;" onclick="sortTable(0)">ID</th>
<th class="text-left" style="cursor: pointer;" onclick="sortTable(1)">{% if dockers[0].team_id %}Team{% else %}User{% endif %}</th>
<th class="text-left" style="cursor: pointer;" onclick="sortTable(2)">Docker Image</th>
<th class="text-left" style="cursor: pointer;" onclick="sortTable(3)">Instance ID</th>
<th class="text-left">Revoke</th>
</tr>
</thead>
<tbody>
{% for docker in dockers %}
<tr id='tr_{{docker.instance_id}}' name='{{docker.id}}'>
<td class='text-center' value='{{docker.id}}'>{{docker.id}}</td>
{% if docker.team_id %}
<td class='text-center' value='{{docker.team_id | safe }}'>{{docker.team_id | safe }}</td>
{% else %}
<td class='text-center' value='{{docker.team_id | safe }}'>{{docker.user_id | safe }}</td>
{% endif %}
<td class='text-center' value='{{docker.docker_image}}'>{{docker.docker_image}}</td>
<td class='text-center' value='{{docker.instance_id | truncate(15)}}'>{{docker.instance_id | truncate(15)}}</td>
<td class='text-center'><a id="delete_{{docker.instance_id}}" style="cursor: pointer;" class="fas fa-trash" onclick="check_nuke_container('{{docker.instance_id}}', false)"></a></td>
</tr>
{% endfor %}
</tbody>
</table>
<div class='text-center'>
<button type="button" class="btn btn-danger" onclick="check_nuke_container(null, true)">Nuke All Containers</button>
</div>
{% else %}
<h3 class='text-center'> No Docker Containers Active</h3>
{% endif %}
</div>
</div>
</div>
{% endblock content %}
{% block scripts %}
<script>
function sortTable(n) {
var table, rows, switching, i, x, y, shouldSwitch, dir, switchcount = 0;
table = document.getElementById("dockers");
switching = true;
dir = "asc";
while (switching) {
switching = false;
rows = table.rows;
for (i = 1; i < (rows.length - 1); i++) {
shouldSwitch = false;
x = rows[i].getElementsByTagName("TD")[n];
y = rows[i + 1].getElementsByTagName("TD")[n];
if (dir == "asc") {
if (x.innerHTML.toLowerCase() > y.innerHTML.toLowerCase()) {
shouldSwitch = true;
break;
}
} else if (dir == "desc") {
if (x.innerHTML.toLowerCase() < y.innerHTML.toLowerCase()) {
shouldSwitch = true;
break;
}
}
}
if (shouldSwitch) {
rows[i].parentNode.insertBefore(rows[i + 1], rows[i]);
switching = true;
switchcount++;
} else {
if (switchcount == 0 && dir == "asc") {
dir = "desc";
switching = true;
}
}
}
}
</script>
<script>
function check_nuke_container(instance, all) {
ezq({
title: "Attention!",
body: "Are You Sure You want to do this?",
success: function() { nuke_container(instance, all) },
});
}
function nuke_container(instance, all) {
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
if (all == true) {
window.location = '{{ script_root }}/admin/docker_status'
}
else {
document.getElementById("tr_" + instance).style.display = "none";
}
} else if (this.readyState == 4 && this.status != 200) {
ezal({
title: "Attention!",
body: "Error when Deleting Docker Container",
button: "Got it!",
});
}
};
xhttp.open("GET", "/api/v1/nuke?container=" + instance + "&all=" + all, true);
xhttp.send();
}
</script>
<script>
var modal =
'<div class="modal fade" tabindex="-1" role="dialog">' +
' <div class="modal-dialog" role="document">' +
' <div class="modal-content">' +
' <div class="modal-header">' +
' <h5 class="modal-title">{0}</h5>' +
' <button type="button" class="close" data-dismiss="modal" aria-label="Close">' +
' <span aria-hidden="true">&times;</span>' +
" </button>" +
" </div>" +
' <div class="modal-body">' +
" <p>{1}</p>" +
" </div>" +
' <div class="modal-footer">' +
" </div>" +
" </div>" +
" </div>" +
"</div>";
function ezq(args) {
var res = modal.format(args.title, args.body);
var obj = $(res);
var deny =
'<button type="button" class="btn btn-danger" data-dismiss="modal">No</button>';
var confirm = $(
'<button type="button" class="btn btn-primary" data-dismiss="modal">Yes</button>'
);
obj.find(".modal-footer").append(deny);
obj.find(".modal-footer").append(confirm);
$("main").append(obj);
$(obj).on("hidden.bs.modal", function(e) {
$(this).modal("dispose");
});
$(confirm).click(function() {
args.success();
});
obj.modal("show");
return obj;
}
function ezal(args) {
var res = modal.format(args.title, args.body);
var obj = $(res);
var button = '<button type="button" class="btn btn-primary" data-dismiss="modal">{0}</button>'.format(
args.button
);
obj.find(".modal-footer").append(button);
$("main").append(obj);
obj.modal("show");
$(obj).on("hidden.bs.modal", function(e) {
$(this).modal("dispose");
});
return obj;
}
</script>
{% endblock scripts %}

View File

@ -0,0 +1,101 @@
{% extends 'admin/base.html' %}
{% block content %}
<div class="jumbotron">
<div class="container">
<h1>Docker Config</h1>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-md-6 offset-md-3">
{% for error in errors %}
<div class="alert alert-danger alert-dismissable" role="alert">
<span class="sr-only">Error:</span>
{{ error }}
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>
</div>
{% endfor %}
<form method="post" accept-charset="utf-8" autocomplete="off" role="form" name='docker_config' class="form-horizontal" enctype="multipart/form-data">
<div class="form-group">
<label for="hostname-input">
Hostname
</label>
{% if config.hostname %}
<input class="form-control" type="text" name="hostname" id="hostname-input" placeholder="Ex: 10.10.10.10:2376" value='{{ config.hostname }}'/>
{% else %}
<input class="form-control" type="text" name="hostname" id="hostname-input" placeholder="Ex: 10.10.10.10:2376" />
{% endif %}
</div>
<div class="form-group">
{% if config.tls_enabled %}
<input type="checkbox" name="tls_enabled" id="tls-checkbox" value="True" onclick="enable_file_form(this.checked)" checked />
{% else %}
<input type="checkbox" name="tls_enabled" id="tls-checkbox" value="False" onclick="enable_file_form(this.checked)" />
{% endif %}
<label for="tls-checkbox">TLS Enabled?</label>
</div>
<div class="form-group">
<label for="ca-file">
CA Cert {% if config.ca_cert %} (Uploaded. Adding a new file will overwrite.) {% endif %}
</label>
<input class="form-control" type="file" name="ca_cert" id="ca_file" {% if not config.tls_enabled %} disabled {% endif %} {% if config.tls_enabled and not config.ca_cert %} required {% endif %} />
</div>
<div class="form-group">
<label for="client-file">
Client Cert {% if config.client_cert %} (Uploaded. Adding a new file will overwrite.) {% endif %}
</label>
<input class="form-control" type="file" name="client_cert" id="client_file" {% if not config.tls_enabled %} disabled {% endif %} {% if config.tls_enabled and not config.client_cert %} required {% endif %} />
</div>
<div class="form-group">
<label for="key-file">
Client Key {% if config.client_key %} (Uploaded. Adding a new file will overwrite.) {% endif %}
</label>
<input class="form-control" type="file" name="client_key" id="key_file" {% if not config.tls_enabled %} disabled {% endif %} {% if config.tls_enabled and not config.client_key %} required {% endif %} />
</div>
<div class="form-group">
<label for="repo-ms">
Repositories
</label>
<select id='repositories' name="repositories" class='form-control' size='10' multiple>
{% if form.repositories.choices[0][0] == "ERROR" %}
<option value='False' disabled>{{ form.repositories.choices[0][1] }}</option>
{% elif form.repositories %}
{% for key,value in form.repositories.choices %}
{% if key in repos %}
<option value='{{ key }}' selected>{{ value }}</option>
{% else %}
<option value='{{ key }}'>{{ value }}</option>
{% endif %}
{% endfor %}
{% else %}
<option value='False' disabled>Connect Docker API First</option>
{% endif %}
</select>
</div>
<div class="col-md-13 text-center">
<button type="submit" tabindex="0" class="btn btn-md btn-primary btn-outlined">
Submit
</button>
</div>
</div>
<input type="hidden" name="nonce" value="{{ nonce }}">
<input type="hidden" name="id" value="1">
</form>
</div>
</div>
{% endblock content %}
{% block scripts %}
<script>
function enable_file_form(status) {
status=!status;
document.docker_config.ca_file.disabled = status;
document.docker_config.client_file.disabled = status;
document.docker_config.key_file.disabled = status;
document.docker_config.ca_file.required = !status;
document.docker_config.client_file.required = !status;
document.docker_config.key_file.required = !status;
}
</script>
{% endblock scripts %}