Skip to content
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

WIP: support for snapshotting working environment #169

Draft
wants to merge 7 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 58 additions & 5 deletions dojo_plugin/api/v1/docker.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import os
import sys
import subprocess
import pathlib
import traceback
import datetime
import pathlib
import sys
import os

import docker
from flask import request
from flask_restx import Namespace, Resource
from CTFd.utils.user import get_current_user, is_admin
from CTFd.utils.decorators import authed_only
from CTFd.models import db

from ...config import HOST_DATA_PATH, INTERNET_FOR_ALL
from ...models import Dojos, DojoModules, DojoChallenges
Expand Down Expand Up @@ -75,15 +77,19 @@ def start_container(user, dojo, dojo_challenge, practice):
internet = INTERNET_FOR_ALL or any(award.name == "INTERNET" for award in user.awards)

return docker_client.containers.run(
dojo_challenge.image,
dojo_challenge.image or "pwncollege-challenge",
entrypoint=["/bin/sleep", "6h"],
name=f"user_{user.id}",
hostname=hostname,
user="hacker",
working_dir="/home/hacker",
labels={
"dojo": dojo.reference_id,
"module": dojo_challenge.module.id,
"challenge": dojo_challenge.id,
"user_name": user.name,
"user_id": str(user.id),
"practice": str(practice),
},
mounts=[
docker.types.Mount(
Expand All @@ -92,6 +98,7 @@ def start_container(user, dojo, dojo_challenge, practice):
"bind",
propagation="shared",
),
docker.types.Mount( "/tmp", "", "tmpfs"),
],
devices=devices,
network=None if internet else "user_firewall",
Expand Down Expand Up @@ -184,8 +191,54 @@ def initialize_container():

initialize_container()

@docker_namespace.route("/snapshot")
class SnapshotDocker(Resource):
@authed_only
def get(self):
user = get_current_user()
docker_client = docker.from_env()
try:
container = docker_client.containers.get(f"user_{user.id}")
except docker.errors.NotFound:
return {"success": False, "error": "No container running to snapshot. Start one!"}

if container.labels["practice"] != "True":
return {"success": False, "error": "Running container is not in practice mode. Please restart in practice mode."}

label_module = container.labels["module"]
label_challenge = container.labels["challenge"]
label_name = container.labels["user_name"]
snapshot_time = datetime.datetime.now()
snapshot_name = f"{label_module}-{label_challenge}-{label_name}"
container.commit(f"snapshot-{snapshot_name}")

# make a dojo challenge for it
dojo_challenge = get_current_dojo_challenge()
dojo = dojo_challenge.dojo
snapshot_module = DojoModules.query.filter_by(dojo=dojo, id="snapshots").first()
if not snapshot_module:
snapshot_module = DojoModules(
id="snapshots", name="Env Snapshots",
description="These are environment snapshots taken by other dojo practitioners.",
module_index=100,
dojo=dojo, challenges=[], resources=[]
)
db.session.add(snapshot_module)

snapshot_chal = DojoChallenges.query.filter_by(dojo=dojo, id=snapshot_name).first()
if not snapshot_chal:
snapshot_chal = DojoChallenges(
id=snapshot_name, name=snapshot_name, image=f"snapshot-{snapshot_name}",
challenge=dojo_challenge.challenge, challenge_index=len(snapshot_module.challenges),
module=snapshot_module
)
db.session.add(snapshot_chal)
description=f"This is a snapshot of {label_name}'s environment for challenge {label_challenge} of module {label_module} at {snapshot_time.strftime('%m/%d %H:%M')}"

db.session.commit()
return {"success": True}

@docker_namespace.route("")
@docker_namespace.route("/run")
class RunDocker(Resource):
@authed_only
def post(self):
Expand Down
11 changes: 6 additions & 5 deletions dojo_plugin/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -353,9 +353,10 @@ class DojoChallenges(db.Model):
challenge_index = db.Column(db.Integer, primary_key=True)

challenge_id = db.Column(db.Integer, db.ForeignKey("challenges.id", ondelete="CASCADE"))
id = db.Column(db.String(32), index=True)
id = db.Column(db.String(128), index=True)
name = db.Column(db.String(128))
description = db.Column(db.Text)
image = db.Column(db.String(128))

data = db.Column(db.JSON)
data_fields = ["path_override"]
Expand All @@ -372,6 +373,7 @@ class DojoChallenges(db.Model):
back_populates="challenge")

def __init__(self, *args, **kwargs):
kwargs.setdefault("image", "pwncollege-challenge")
default = kwargs.pop("default", None)

data = kwargs.pop("data", {})
Expand Down Expand Up @@ -453,13 +455,12 @@ def path(self):
if not self.path_override else
pathlib.Path(self.path_override))

@property
def image(self):
return "pwncollege-challenge"

def challenge_paths(self, user):
secret = current_app.config["SECRET_KEY"]

if not self.path.exists():
return

for path in self.path.iterdir():
if path.name.startswith("_"):
continue
Expand Down
2 changes: 1 addition & 1 deletion dojo_theme/static/js/dojo/challenges.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ function startChallenge(event) {
"practice": practice,
};

CTFd.fetch('/pwncollege_api/v1/docker', {
CTFd.fetch('/pwncollege_api/v1/docker/run', {
method: 'POST',
credentials: 'same-origin',
headers: {
Expand Down