The web SSH terminal and the credential API reported `ctfuser` for all 16
challenges, but only the 6 native GEMASTIK XVIII images provision ctfuser.
Every imported XVI/XVII image does `RUN echo root:${PASSWORD} | chpasswd`,
so 10 of 16 participant logins were refused with "Permission denied".
Root causes (all the same class of bug - login hardcoded in the wrong layer):
- main.py websocket ssh handler read st["ssh_user"], a single team-wide value
defaulting to ctfuser, instead of the per-challenge registry field
- /api/credential proxied the global receiver on :18080, which only knows the
6 native challenges, so the other 10 returned "Invalid challenge"
- team.html hardcoded the challenge picker to those same 6 challenges, making
the other 10 unreachable from the terminal entirely
- index.html rendered `<b>ctfuser</b>` and a stale hardcoded SSH port table
Fixes:
- orch.challenge_credential()/all_teams() read the TEAM's state.json, which
holds the same per-challenge password the panel chpasswds
- gen_receiver_services.py injects SSH_USER_<port> from the registry so the
receiver's /credential endpoint agrees with the panel
- receiver Challenge.credentials() honours SSH_USER_<port> (ctfuser fallback)
- new /api/team/{idx}/own-challenges feeds the picker; targets now carry
challenge + ssh_user
- UI takes user and port from the server instead of hardcoding them
Verified: 32/32 credential payloads correct across teams 1-2, and 32/32 real
paramiko SSH logins succeed with whoami confirming the expected account.
Also adds bulk team delete: POST /api/teams/bulk-delete runs one background
thread and is polled via GET /api/teams/bulk-delete/{job_id}, plus per-team
checkboxes with select-all/clear in the UI. Deletion must stay sequential
because delete_team() regenerates shared artifacts at the end.
54 lines
1.8 KiB
Python
54 lines
1.8 KiB
Python
import logging
|
|
import os
|
|
import random
|
|
import string
|
|
|
|
from config import get_settings
|
|
|
|
|
|
class Challenge(object):
|
|
name = __name__
|
|
settings = get_settings()
|
|
port = 0
|
|
# Host the checker connects to. Same machine as the published team ports;
|
|
# override with RECEIVER_HOST if a receiver ever runs off-host.
|
|
host = os.environ.get("RECEIVER_HOST", "127.0.0.1")
|
|
|
|
def __init__(self, port, host=None):
|
|
self.port = port
|
|
if host:
|
|
self.host = host
|
|
self.add_logger()
|
|
|
|
def url(self, path=""):
|
|
"""Absolute URL for the challenge service (see xvi/Challenge.py)."""
|
|
p = "" if path.startswith("/") else "/"
|
|
return f"http://{self.host}:{self.port}{p}{path}"
|
|
|
|
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}', '')
|
|
# SSH login user is PER-CHALLENGE, not per-package: the imported XVII
|
|
# Dockerfiles also do `echo root:${PASSWORD} | chpasswd`, so reporting
|
|
# ctfuser here handed participants a login that could never work.
|
|
# gen_receiver_services.py injects SSH_USER_<port> from the registry,
|
|
# which is the single source of truth; the literal is only a fallback.
|
|
user = os.environ.get(f'SSH_USER_{self.port}', 'ctfuser')
|
|
return {
|
|
'username': user,
|
|
'password': pwd,
|
|
} |