feat: challenge registry-driven platform + XVI/XVII imports + admin toggle + domain rename
- Rename repo/domain: attack-defense-platform / attackdefense.imrnes.team (all refs replaced)
- challenge_registry.json: single source of truth (28 challs across gemastik18/xvi/xvii)
- teams.py: registry-driven CHALLENGES, set_challenge_enabled, sync_challenge_runtime
(apply enable/disable to live teams: build/up or stop/remove + receiver restart)
- compose_gen.py: render per-team compose from canonical per-challenge templates
(image reuse, per-team ports 30xxx, flag mounts, passwords)
- gen_canonical_composes.py: canonical docker-compose.yml for all services
- import_new_challenges.py: import XVI/XVII services + EOL base image fixes
(debian:buster→bookworm, node:14→20, python:3.7-slim→3.11)
- receiver: xvi package (10 checkers) + xvii package (12 generic checkers),
Challenge base reads PASSWORD_<team_port> from env; gen_receiver_main.py
generates per-team main.py from registry
- main.py: /api/challenges returns full registry; PATCH /api/challenges/<name>
toggles enabled + applies to live teams
- index.html: 🏗️ Challenge Manager tab (toggle per challenge, grouped by set)
- SLA bonus now dynamic (all enabled challenges, not hardcoded 6)
This commit is contained in:
1 parent
c35b23a37f
commit
c6fd9ec268
1317 files changed
+306586
-128
No files matched your search
@@ -0,0 +1,38 @@
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
import string
|
||||
|
||||
from config import get_settings
|
||||
|
||||
|
||||
class Challenge(object):
|
||||
name = __name__
|
||||
settings = get_settings()
|
||||
port = 0
|
||||
|
||||
def __init__(self, port):
|
||||
self.port = port
|
||||
self.add_logger()
|
||||
|
||||
def add_logger(self):
|
||||
self.logger = logging.getLogger()
|
||||
|
||||
def random_string(self, length):
|
||||
charset = string.ascii_uppercase + string.ascii_lowercase + string.digits
|
||||
return ''.join(random.choice(charset) for i in range(length))
|
||||
|
||||
def distribute(self, flag):
|
||||
raise NotImplementedError
|
||||
|
||||
def check(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def credentials(self):
|
||||
pwd = os.environ.get(f'PASSWORD_{self.port}')
|
||||
if not pwd:
|
||||
pwd = getattr(self.settings, f'PASSWORD_{self.port}', '')
|
||||
return {
|
||||
'username': 'ctfuser',
|
||||
'password': pwd,
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
"""SLA checkers for GEMASTIK XVII challenges (imported from
|
||||
github.com/vidner/gemastik-xvii-final — no upstream receiver was provided).
|
||||
|
||||
Each checker validates liveness + flag presence in the container. Protocols:
|
||||
- TCP/netcat : asmr, bit-canvas, go-green (xinetd banner) & ticketer (socat)
|
||||
- HTTP GET : anti-alchemy, fjb, gift-card, gift-voucher, gleam-drive,
|
||||
kode-viewer, more-less, tempest-poc
|
||||
"""
|
||||
import os
|
||||
import socket
|
||||
import subprocess
|
||||
|
||||
import requests
|
||||
|
||||
from .Challenge import Challenge
|
||||
|
||||
|
||||
def _docker_exec(container: str, *args, timeout: int = 10):
|
||||
try:
|
||||
return subprocess.run(["docker", "exec", container, *args],
|
||||
capture_output=True, text=True, timeout=timeout)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _flag_in_container(container: str, path: str = "/flag.txt") -> bool:
|
||||
r = _docker_exec(container, "sh", "-c", f"test -f {path} && cat {path} || echo MISSING")
|
||||
return bool(r and "MISSING" not in (r.stdout or "") and r.returncode == 0)
|
||||
|
||||
|
||||
def _tcp_banner(port: int, timeout: float = 4.0, expect: bytes = None) -> bool:
|
||||
try:
|
||||
s = socket.create_connection(("127.0.0.1", port), timeout=timeout)
|
||||
s.settimeout(timeout)
|
||||
data = s.recv(256)
|
||||
s.close()
|
||||
if expect:
|
||||
return expect.lower() in data.lower()
|
||||
return len(data) > 0
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
class AntiAlchemy(Challenge):
|
||||
flag_location = "flags/anti-alchemy.txt"
|
||||
history_location = "history/anti-alchemy.txt"
|
||||
|
||||
def check(self):
|
||||
try:
|
||||
url = f"http://localhost:{self.port}/"
|
||||
r = requests.get(url, timeout=6)
|
||||
assert r.status_code in (200, 302, 500) or len(r.text) > 0
|
||||
return _flag_in_container(os.environ.get("CHALLENGE_CONTAINER_ANTI_ALCHEMY", "anti-alchemy_container"))
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
class Asmr(Challenge):
|
||||
flag_location = "flags/asmr.txt"
|
||||
history_location = "history/asmr.txt"
|
||||
|
||||
def check(self):
|
||||
try:
|
||||
return _tcp_banner(self.port, expect=None)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
class BitCanvas(Challenge):
|
||||
flag_location = "flags/bit-canvas.txt"
|
||||
history_location = "history/bit-canvas.txt"
|
||||
|
||||
def check(self):
|
||||
try:
|
||||
return _tcp_banner(self.port, expect=None)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
class Fjb(Challenge):
|
||||
flag_location = "flags/fjb.txt"
|
||||
history_location = "history/fjb.txt"
|
||||
|
||||
def check(self):
|
||||
try:
|
||||
url = f"http://localhost:{self.port}/"
|
||||
r = requests.get(url, timeout=6)
|
||||
assert r.status_code < 500
|
||||
return _flag_in_container(os.environ.get("CHALLENGE_CONTAINER_FJB", "fjb_container"))
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
class GiftCard(Challenge):
|
||||
flag_location = "flags/gift-card.txt"
|
||||
history_location = "history/gift-card.txt"
|
||||
|
||||
def check(self):
|
||||
try:
|
||||
url = f"http://localhost:{self.port}/"
|
||||
r = requests.get(url, timeout=6)
|
||||
assert r.status_code < 500
|
||||
return _flag_in_container(os.environ.get("CHALLENGE_CONTAINER_GIFT_CARD", "gift-card_container"),
|
||||
"/ctf/gift-card/flag.txt")
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
class GiftVoucher(Challenge):
|
||||
flag_location = "flags/gift-voucher.txt"
|
||||
history_location = "history/gift-voucher.txt"
|
||||
|
||||
def check(self):
|
||||
try:
|
||||
url = f"http://localhost:{self.port}/"
|
||||
r = requests.get(url, timeout=6)
|
||||
assert r.status_code < 500
|
||||
return _flag_in_container(os.environ.get("CHALLENGE_CONTAINER_GIFT_VOUCHER", "gift-voucher_container"),
|
||||
"/ctf/gift-voucher/flag.txt")
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
class GleamDrive(Challenge):
|
||||
flag_location = "flags/gleam-drive.txt"
|
||||
history_location = "history/gleam-drive.txt"
|
||||
|
||||
def check(self):
|
||||
try:
|
||||
url = f"http://localhost:{self.port}/"
|
||||
r = requests.get(url, timeout=6)
|
||||
assert r.status_code < 500
|
||||
return _flag_in_container(os.environ.get("CHALLENGE_CONTAINER_GLEAM_DRIVE", "gleam-drive_container"))
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
class GoGreen(Challenge):
|
||||
flag_location = "flags/go-green.txt"
|
||||
history_location = "history/go-green.txt"
|
||||
|
||||
def check(self):
|
||||
try:
|
||||
return _tcp_banner(self.port, expect=None)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
class KodeViewer(Challenge):
|
||||
flag_location = "flags/kode-viewer.txt"
|
||||
history_location = "history/kode-viewer.txt"
|
||||
|
||||
def check(self):
|
||||
try:
|
||||
url = f"http://localhost:{self.port}/"
|
||||
r = requests.get(url, timeout=6)
|
||||
assert r.status_code < 500
|
||||
return _flag_in_container(os.environ.get("CHALLENGE_CONTAINER_KODE_VIEWER", "kode-viewer_container"))
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
class MoreLess(Challenge):
|
||||
flag_location = "flags/more-less.txt"
|
||||
history_location = "history/more-less.txt"
|
||||
|
||||
def check(self):
|
||||
try:
|
||||
url = f"http://localhost:{self.port}/"
|
||||
r = requests.get(url, timeout=6)
|
||||
assert r.status_code < 500
|
||||
return _flag_in_container(os.environ.get("CHALLENGE_CONTAINER_MORE_LESS", "more-less_container"))
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
class TempestPoc(Challenge):
|
||||
flag_location = "flags/tempest-poc.txt"
|
||||
history_location = "history/tempest-poc.txt"
|
||||
|
||||
def check(self):
|
||||
try:
|
||||
url = f"http://localhost:{self.port}/"
|
||||
r = requests.get(url, timeout=6)
|
||||
assert r.status_code < 500
|
||||
return _flag_in_container(os.environ.get("CHALLENGE_CONTAINER_TEMPEST_POC", "tempest-poc_container"))
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
class Ticketer(Challenge):
|
||||
flag_location = "flags/ticketer.txt"
|
||||
history_location = "history/ticketer.txt"
|
||||
|
||||
def check(self):
|
||||
try:
|
||||
return _tcp_banner(self.port, expect=None)
|
||||
except Exception:
|
||||
return False
|
||||
Reference in new issue
Block a user