diff --git a/Dockerfile b/Dockerfile index bef81fbb2..46381815d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -32,6 +32,8 @@ RUN cd /tmp && \ RUN git clone --branch 3.6.0 https://github.com/CTFd/CTFd /opt/CTFd +RUN echo 'tmpfs /run/dojofs tmpfs defaults,mode=755,shared 0 0' > /etc/fstab + RUN ln -s /opt/pwn.college/etc/systemd/system/pwn.college.service /etc/systemd/system/pwn.college.service && \ ln -s /opt/pwn.college/etc/systemd/system/pwn.college.backup.service /etc/systemd/system/pwn.college.backup.service && \ ln -s /opt/pwn.college/etc/systemd/system/pwn.college.backup.timer /etc/systemd/system/pwn.college.backup.timer && \ diff --git a/docker-compose.yml b/docker-compose.yml index c402aedbc..ee51dcb64 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -53,6 +53,16 @@ services: workspace-builder: condition: service_completed_successfully + dojofs: + container_name: dojofs + privileged: true + pid: host + build: + context: ./dojofs + volumes: + - /run/dojofs:/run/dojofs:shared + - /var/run/docker.sock:/var/run/docker.sock:ro + ctfd: container_name: ctfd profiles: @@ -139,6 +149,8 @@ services: condition: service_completed_successfully workspacefs: condition: service_started + dojofs: + condition: service_started db: condition: service_healthy restart: true diff --git a/dojo_plugin/api/v1/docker.py b/dojo_plugin/api/v1/docker.py index cdabe4770..193638059 100644 --- a/dojo_plugin/api/v1/docker.py +++ b/dojo_plugin/api/v1/docker.py @@ -125,6 +125,13 @@ def start_container(docker_client, user, as_user, mounts, dojo_challenge, practi read_only=True, propagation="shared", ), + docker.types.Mount( + "/run/dojo/sys", + "/run/dojofs", + "bind", + read_only=True, + propagation="slave", + ), ] + [ docker.types.Mount( diff --git a/dojofs/Dockerfile b/dojofs/Dockerfile new file mode 100644 index 000000000..8c9d46697 --- /dev/null +++ b/dojofs/Dockerfile @@ -0,0 +1,14 @@ +FROM python:3.12-slim + +RUN apt-get update && \ + apt-get install -y \ + fuse && \ + rm -rf /var/lib/apt/lists/* && \ + pip install \ + docker \ + fusepy && \ + mkdir -p /run/dojofs/workspace + +COPY ./dojofs /usr/local/bin/dojofs + +CMD ["dojofs", "/run/dojofs/workspace"] diff --git a/dojofs/dojofs b/dojofs/dojofs new file mode 100755 index 000000000..b6ea7f289 --- /dev/null +++ b/dojofs/dojofs @@ -0,0 +1,110 @@ +#!/usr/bin/env python + +import stat +import sys +import time +from datetime import datetime +from pathlib import Path + +import fuse +import docker +from fuse import FUSE, Operations, fuse_get_context + + +docker_client = docker.from_env() + + +class DojoFS(Operations): + def __init__(self): + self.files = {} + + def path(self, path): + def decorator(cls): + self.files[path] = cls() + return cls + return decorator + + def getattr(self, path, fh=None): + if path == "/": + return dict(st_mode=(stat.S_IFDIR | 0o755), st_nlink=2) + file = self.files.get(path) + if not file: + raise fuse.FuseOSError(fuse.errno.ENOENT) + return file.getattr(path, fh) + + def readdir(self, path, fh): + if path == "/": + return [".", "..", *(path.lstrip("/") for path, file in self.files.items() if file)] + else: + raise fuse.FuseOSError(fuse.errno.ENOENT) + + def read(self, path, size, offset, fh): + file = self.files.get(path) + if not file: + raise fuse.FuseOSError(fuse.errno.ENOENT) + return file.read(path, size, offset, fh) + + +dojo_fs = DojoFS() + + +def get_container_context(): + uid, gid, pid = fuse_get_context() + import pathlib + import re + + container_re = re.compile(r"/docker/containers/([0-9a-f]+)/hostname") + mount_info = pathlib.Path(f"/proc/{pid}/mountinfo").read_text() + container_id = match.group(1) if (match := container_re.search(mount_info)) else None + if not container_id: + return None + + try: + container = docker_client.containers.get(container_id) + except docker.errors.NotFound: + return None + return container + + +def unix_time(timestamp): + if "." in timestamp: + timestamp = timestamp[:timestamp.index(".") + 7] + "Z" + created_time = datetime.strptime(timestamp, "%Y-%m-%dT%H:%M:%S.%fZ") + created_unix_time = time.mktime(created_time.timetuple()) + return created_unix_time + + +@dojo_fs.path("/privileged") +class PrivilegedFile: + def getattr(self, path, fh=None): + container = get_container_context() + created_unix_time = unix_time(container.attrs["Created"]) + return dict( + st_mode=(stat.S_IFREG | 0o444), + st_nlink=1, + st_size=4096, + st_ctime=created_unix_time, + st_mtime=created_unix_time, + st_atime=created_unix_time, + ) + + def read(self, path, size, offset, fh): + container = get_container_context() + mode = container.labels.get("dojo.mode") + content = b"1\n" if mode == "privileged" else b"0\n" + return content[offset:offset + size] + + def __bool__(self): + container = get_container_context() + return bool(container) + + +if __name__ == "__main__": + if len(sys.argv) != 2: + print(f"Usage: {sys.argv[0]} ") + sys.exit(1) + + mountpoint = sys.argv[1] + Path(mountpoint).mkdir(parents=True, exist_ok=True) + dojo_fs.__class__.__name__ = "dojofs" + FUSE(dojo_fs, mountpoint, foreground=True, allow_other=True) diff --git a/workspace/core/sudo.py b/workspace/core/sudo.py index 2c887f3ee..6d0b86ff4 100644 --- a/workspace/core/sudo.py +++ b/workspace/core/sudo.py @@ -12,7 +12,11 @@ def error(message): def main(): program = os.path.basename(sys.argv[0]) - if not os.path.exists("/run/dojo/var/root/privileged"): + try: + privileged = int(open("/run/dojo/sys/workspace/privileged", "r").read()) + except FileNotFoundError: + error(f"{program}: dojofs is unavailable") + if not privileged: error(f"{program}: workspace is not privileged") struct_passwd = pwd.getpwuid(os.geteuid())