Skip to content

Get ExposedPorts from Config if it does not exist in ContainerConfig #26

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,5 @@ dmypy.json

# Pyre type checker
.pyre/

**/.DS_Store
127 changes: 69 additions & 58 deletions docker_challenges/__init__.py
Original file line number Diff line number Diff line change
@@ -1,51 +1,36 @@
import traceback

from CTFd.plugins.challenges import BaseChallenge, CHALLENGE_CLASSES, get_chal_class
from CTFd.plugins.challenges import BaseChallenge, CHALLENGE_CLASSES
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.plugins import register_plugin_assets_directory
from CTFd.models import db, Challenges, Teams, Users, Solves, Fails, Flags, Hints, Tags, ChallengeFiles
from CTFd.utils.decorators import admins_only, authed_only
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_restx import Namespace, Resource
from flask import request, Blueprint, jsonify, abort, render_template, url_for, redirect, session
# from flask_wtf import FlaskForm
from flask import request, Blueprint, abort, render_template, session
from wtforms import (
FileField,
HiddenField,
PasswordField,
IntegerField,
RadioField,
SelectField,
StringField,
TextAreaField,
SelectMultipleField,
BooleanField,
)
# from wtforms import TextField, SubmitField, BooleanField, HiddenField, FileField, SelectMultipleField
from wtforms.validators import DataRequired, ValidationError, InputRequired
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

from CTFd.forms import BaseForm
from CTFd.forms.fields import SubmitField
from CTFd.utils.config import get_themes


class DockerConfig(db.Model):
Expand All @@ -58,6 +43,9 @@ class DockerConfig(db.Model):
ca_cert = db.Column("ca_cert", db.String(2200), index=True)
client_cert = db.Column("client_cert", db.String(2000), index=True)
client_key = db.Column("client_key", db.String(3300), index=True)
memory_limit = db.Column("memory_limit", db.BigInteger)
cpu_period = db.Column("cpu_period", db.BigInteger)
cpu_quota = db.Column("cpu_quota", db.BigInteger)
repositories = db.Column("repositories", db.String(1024), index=True)


Expand Down Expand Up @@ -85,6 +73,9 @@ class DockerConfigForm(BaseForm):
ca_cert = FileField('CA Cert')
client_cert = FileField('Client Cert')
client_key = FileField('Client Key')
memory_limit = IntegerField('Memory Limit per Container (Bytes)')
cpu_period = IntegerField('CPU Period per Container (Microseconds)')
cpu_quota = IntegerField('CPU Quota per Container (Microseconds)')
repositories = SelectMultipleField('Repositories')
submit = SubmitField('Submit')

Expand Down Expand Up @@ -131,6 +122,9 @@ def docker_config():
b.ca_cert = None
b.client_cert = None
b.client_key = None
b.memory_limit = None if request.form['memory_limit'] in [None, ''] else request.form['memory_limit']
b.cpu_period = None if request.form['cpu_period'] in [None, ''] else request.form['cpu_period']
b.cpu_quota = None if request.form['cpu_quota'] in [None, ''] else request.form['cpu_quota']
try:
b.repositories = ','.join(request.form.to_dict(flat=False)['repositories'])
except:
Expand Down Expand Up @@ -217,10 +211,11 @@ def do_request(docker, url, headers=None, method='GET'):
URL_TEMPLATE = '%s://%s' % (prefix, host)
try:
if tls:
cert = get_client_cert(docker)
if (method == 'GET'):
r = requests.get(url=f"%s{url}" % URL_TEMPLATE, cert=get_client_cert(docker), verify=False, headers=headers)
r = requests.get(url=f"%s{url}" % URL_TEMPLATE, cert=cert[0:2], verify=cert[2], headers=headers)
elif (method == 'DELETE'):
r = requests.delete(url=f"%s{url}" % URL_TEMPLATE, cert=get_client_cert(docker), verify=False, headers=headers)
r = requests.delete(url=f"%s{url}" % URL_TEMPLATE, cert=cert[0:2], verify=cert[2], headers=headers)
else:
if (method == 'GET'):
r = requests.get(url=f"%s{url}" % URL_TEMPLATE, headers=headers)
Expand All @@ -238,15 +233,15 @@ def get_client_cert(docker):
client = docker.client_cert
ckey = docker.client_key
ca_file = tempfile.NamedTemporaryFile(delete=False)
ca_file.write(ca)
ca_file.write(ca.encode('utf-8'))
ca_file.seek(0)
client_file = tempfile.NamedTemporaryFile(delete=False)
client_file.write(client)
client_file.write(client.encode('utf-8'))
client_file.seek(0)
key_file = tempfile.NamedTemporaryFile(delete=False)
key_file.write(ckey)
key_file.write(ckey.encode('utf-8'))
key_file.seek(0)
CERT = (client_file.name, key_file.name)
CERT = (client_file.name, key_file.name, ca_file.name)
except:
print(traceback.print_exc())
CERT = None
Expand Down Expand Up @@ -276,43 +271,27 @@ def get_unavailable_ports(docker):
for i in r.json():
if not i['Ports'] == []:
for p in i['Ports']:
result.append(p['PublicPort'])
if 'PublicPort'in p:
result.append(p['PublicPort'])
return result


def get_required_ports(docker, image):
r = do_request(docker, f'/images/{image}/json?all=1')
result = r.json()['ContainerConfig']['ExposedPorts'].keys()
return result
result = r.json()
ports = (result['ContainerConfig']['ExposedPorts']\
if 'ExposedPorts' in result['ContainerConfig']\
else result['Config']['ExposedPorts']).keys()
return ports


def create_container(docker, image, team, portbl):
tls = docker.tls_enabled
CERT = None
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:
print(traceback.print_exc())
return []
host = docker.hostname
URL_TEMPLATE = '%s://%s' % (prefix, host)
needed_ports = get_required_ports(docker, image)
prefix = 'https' if tls else 'http'
URL_TEMPLATE = '%s://%s' % (prefix, docker.hostname)
challenge = DockerChallenge.query.filter_by(docker_image=image).first()
needed_ports = challenge.ports.split(',') if challenge.ports else 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()
Expand All @@ -329,12 +308,20 @@ def create_container(docker, image, team, portbl):
ports[i] = {}
bindings[i] = [{"HostPort": tmp_ports.pop()}]
headers = {'Content-Type': "application/json"}
data = json.dumps({"Image": image, "ExposedPorts": ports, "HostConfig": {"PortBindings": bindings}})
params = {"Image": image, "ExposedPorts": ports, "HostConfig": {"PortBindings": bindings}}
if docker.memory_limit:
params["HostConfig"]["Memory"] = docker.memory_limit
if docker.cpu_period:
params["HostConfig"]["CpuPeriod"] = docker.cpu_period
if docker.cpu_quota:
params["HostConfig"]["CpuQuota"] = docker.cpu_quota
data = json.dumps(params)
if tls:
r = requests.post(url="%s/containers/create?name=%s" % (URL_TEMPLATE, container_name), cert=CERT,
verify=False, data=data, headers=headers)
cert = get_client_cert(docker)
r = requests.post(url="%s/containers/create?name=%s" % (URL_TEMPLATE, container_name), cert=cert[0:2],
verify=cert[2], data=data, headers=headers)
result = r.json()
s = requests.post(url="%s/containers/%s/start" % (URL_TEMPLATE, result['Id']), cert=CERT, verify=False,
s = requests.post(url="%s/containers/%s/start" % (URL_TEMPLATE, result['Id']), cert=cert[0:2], verify=cert[2],
headers=headers)
else:
r = requests.post(url="%s/containers/create?name=%s" % (URL_TEMPLATE, container_name),
Expand Down Expand Up @@ -422,6 +409,7 @@ def read(challenge):
'name': challenge.name,
'value': challenge.value,
'docker_image': challenge.docker_image,
'ports': challenge.ports,
'description': challenge.description,
'category': challenge.category,
'state': challenge.state,
Expand Down Expand Up @@ -536,6 +524,7 @@ 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(128), index=True)
ports = db.Column(db.String(128), nullable=True)


# API
Expand Down Expand Up @@ -672,13 +661,35 @@ def get(self):
}, 400


docker_ports_namespace = Namespace("docker", description='Endpoint to retrieve docker image exposed ports')


@docker_ports_namespace.route("", methods=['POST', 'GET'])
class DockerPortsAPI(Resource):
"""
This is for creating Docker Challenges. The purpose of this API is to populate the Docker Image exposed ports Select form
object in the Challenge Creation Screen.
"""

@admins_only
def get(self):
image = request.args.get('image')
docker = DockerConfig.query.filter_by(id=1).first()
exposed_ports = list(get_required_ports(docker, image))
return {
'success': True,
'data': exposed_ports
}


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(docker_ports_namespace, '/docker_ports')
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')
6 changes: 6 additions & 0 deletions docker_challenges/assets/create.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@
</label>
<select id="dockerimage_select" name="docker_image" class="form-control" required></select>
</div>
<div class="form-group">
<label for="ports" id="ports_label"><span id="ports_text">Published Ports, Separated With Comma:</span>
<i class="far fa-question-circle text-muted cursor-help" data-toggle="tooltip" data-placement="right" title="Leave empty to publish all exposed ports"></i>
</label>
<input type="text" class="form-control" name="ports">
</div>
{% endblock %}
{% block type %}
<input type="hidden" name="type" value="docker" id="chaltype">
Expand Down
5 changes: 5 additions & 0 deletions docker_challenges/assets/create.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,10 @@ CTFd.plugin.run((_CTFd) => {
}
});
});
$("#dockerimage_select").on("change", function() {
$.getJSON("/api/v1/docker_ports?image=" + this.value, function(result) {
$("#ports_text").text("Published Ports, Separated With Comma (Allowed Values: " + result["data"].join(", ") + "):");
});
});
});
});
7 changes: 7 additions & 0 deletions docker_challenges/assets/update.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,16 @@
</label>
<select id="dockerimage_select" name="docker_image" class="form-control" required></select>
</div>
<div class="form-group">
<label for="ports" id="ports_label"><span id="ports_text">Published Ports, Separated With Comma:</span>
<i id="ports_tooltip" class="far fa-question-circle text-muted cursor-help" data-toggle="tooltip" data-placement="right" title="Leave empty to publish all exposed ports"></i>
</label>
<input id="docker_image_ports" type="text" class="form-control" name="ports">
</div>
{% endblock %}
{% block footer %}
<script>
var DOCKER_IMAGE = '{{ challenge.docker_image }}';
var DOCKER_IMAGE_PORTS = '{{ challenge.ports }}';
</script>
{% endblock %}
6 changes: 6 additions & 0 deletions docker_challenges/assets/update.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ CTFd.plugin.run((_CTFd) => {
$("#dockerimage_select").append($("<option />").val(item.name).text(item.name));
});
$("#dockerimage_select").val(DOCKER_IMAGE).change();
$("#docker_image_ports").attr("value", DOCKER_IMAGE_PORTS);
});
$("#dockerimage_select").on("change", function() {
$.getJSON("/api/v1/docker_ports?image=" + this.value, function(result) {
$("#ports_text").text("Published Ports, Separated With Comma (Allowed Values: " + result["data"].join(", ") + "):");
});
});
});
});
30 changes: 30 additions & 0 deletions docker_challenges/templates/docker_config.html
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,36 @@ <h1>Docker Config</h1>
</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="memory-limit-input">
Memory Limit per Container (Bytes)
</label>
{% if config.memory_limit %}
<input class="form-control" type="number" name="memory_limit" id="memory-limit-input" placeholder="Default: 0" value='{{ config.memory_limit }}'/>
{% else %}
<input class="form-control" type="number" name="memory_limit" id="memory-limit-input" placeholder="Default: 0" />
{% endif %}
</div>
<div class="form-group">
<label for="cpu-period-input">
CPU Period per Container (Microseconds)
</label>
{% if config.cpu_period %}
<input class="form-control" type="number" name="cpu_period" id="cpu-period-input" placeholder="Default: 100000" min="0" value='{{ config.cpu_period }}'/>
{% else %}
<input class="form-control" type="number" name="cpu_period" id="cpu-period-input" placeholder="Default: 100000" min="0" />
{% endif %}
</div>
<div class="form-group">
<label for="cpu-quota-input">
CPU Quota per Container (Microseconds)
</label>
{% if config.cpu_quota %}
<input class="form-control" type="number" name="cpu_quota" id="cpu-quota-input" placeholder="Leave empty to have the default limit" min="0" value='{{ config.cpu_quota }}'/>
{% else %}
<input class="form-control" type="number" name="cpu_quota" id="cpu-quota-input" placeholder="Leave empty to have the default limit" min="0" />
{% endif %}
</div>
<div class="form-group">
<label for="repo-ms">
Repositories
Expand Down