Skip to content

Commit

Permalink
Workspace: Add dojofs (#585)
Browse files Browse the repository at this point in the history
* Workspace: Add dojofs

* More

* Executable

* log more

* fix

* fix

* Hide dojofs files when no container

* log

* update

* change fstab

* update

* fix

* improve error message
  • Loading branch information
ConnorNelson authored Oct 4, 2024
1 parent 191eb83 commit 99f0c1e
Show file tree
Hide file tree
Showing 6 changed files with 150 additions and 1 deletion.
2 changes: 2 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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 && \
Expand Down
12 changes: 12 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -139,6 +149,8 @@ services:
condition: service_completed_successfully
workspacefs:
condition: service_started
dojofs:
condition: service_started
db:
condition: service_healthy
restart: true
Expand Down
7 changes: 7 additions & 0 deletions dojo_plugin/api/v1/docker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
14 changes: 14 additions & 0 deletions dojofs/Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
110 changes: 110 additions & 0 deletions dojofs/dojofs
Original file line number Diff line number Diff line change
@@ -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]} <mountpoint>")
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)
6 changes: 5 additions & 1 deletion workspace/core/sudo.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down

0 comments on commit 99f0c1e

Please sign in to comment.