From 8c061ba1b7e780bd8fb9436af0927cd7632e7a24 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 23 Sep 2026 15:44:53 +0800 Subject: [PATCH] fix: port scheme 30000 (avoid syncthing 22000), reuse base images (no per-team rebuild), recover corrupted receiver/main.py, compose -p project isolation - PORT_BASE 20000->30000: team1=31xxx team2=32xxx; syncthing owns 22000 - create_team replaces build: with image: services- so teams reuse base images (was rebuilding 6 images per team, disk 100%) - recovery: receiver/main.py was corrupted by bad patch (write_file with read_file format); restored from team1 copy + original GitHub - docker compose -p teamN: project isolation so team compose doesn't overlap (was showing team1 containers for team2) --- panel/teams.py | 29 +++++-- receiver/main.py | 192 ++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 178 insertions(+), 43 deletions(-) diff --git a/panel/teams.py b/panel/teams.py index ac686d2..1dc6c9e 100644 --- a/panel/teams.py +++ b/panel/teams.py @@ -15,9 +15,9 @@ Team N layout: state.json (team metadata: ports, admin user/pass, ssh creds, created ts) Port scheme (base offset per team index, index 1-based): - team i: chall ports = 20000 + i*1000 + 0..5 (blogpost,carbeat,cdn,phew,sheesh,warmup) - ssh ports = 20000 + i*1000 + 22..27 (10022-style) - receiver = 20000 + i*1000 + 80 (receiver API, e.g. 21080, 22080) + team i: chall ports = 30000 + i*1000 + 0..5 (blogpost,carbeat,cdn,phew,sheesh,warmup) + ssh ports = 30000 + i*1000 + 22..27 (10022-style) + receiver = 30000 + i*1000 + 80 (receiver API, e.g. 31080, 32080) """ import json import os @@ -45,7 +45,7 @@ CHALLENGES = [ ("warmup", 5, 27), ] -PORT_BASE = 20000 +PORT_BASE = 30000 STEP = 1000 def team_ports(idx: int) -> dict: @@ -100,6 +100,19 @@ def create_team(idx: int, label: str = None): "host.docker.internal:18080", f"host.docker.internal:{recv_port}")) compose = svc_dir / "docker-compose.yml" text = compose.read_text() + # --- replace build: blocks with image: so teams reuse the base images (no rebuild) --- + # Each service's build block looks like: + # build: + # context: + # args: + # - PASSWORD=$PASSWORD_XXXXX + # Replace the whole block with " image: services-". + for name, coff, soff in CHALLENGES: + text = re.sub( + rf" build:\n context: {name}\n args:\n - PASSWORD=\$PASSWORD_[0-9]+\n", + f" image: services-{name}\n", + text) + compose.write_text(text) # rewrite container names + ports per challenge for name, coff, soff in CHALLENGES: cont_old = f"{name}_container" @@ -167,7 +180,7 @@ def start_team(idx: int): if not (team_dir / "state.json").exists(): raise FileNotFoundError(f"Team {idx} not created") svc_dir = team_dir / "services" - subprocess.run(["docker", "compose", "-f", svc_dir / "docker-compose.yml", "up", "-d", "--build"], + subprocess.run(["docker", "compose", "-p", f"team{idx}", "-f", svc_dir / "docker-compose.yml", "up", "-d", "--build"], cwd=str(svc_dir), check=False, capture_output=True) _start_receiver(idx) st = json.loads((team_dir / "state.json").read_text()) @@ -178,7 +191,7 @@ def start_team(idx: int): def stop_team(idx: int): team_dir = TEAMS_DIR / f"team{idx}" svc_dir = team_dir / "services" - subprocess.run(["docker", "compose", "-f", svc_dir / "docker-compose.yml", "down"], + subprocess.run(["docker", "compose", "-p", f"team{idx}", "-f", svc_dir / "docker-compose.yml", "down"], cwd=str(svc_dir), check=False, capture_output=True) _stop_receiver(idx) st = json.loads((team_dir / "state.json").read_text()) @@ -271,9 +284,9 @@ def randomize_flags(idx: int) -> dict: # rotate into containers: compose mounts the flag files read-only, so # drop+recreate the challenge containers to pick up new flags svc_dir = team_dir / "services" - subprocess.run(["docker", "compose", "-f", svc_dir / "docker-compose.yml", + subprocess.run(["docker", "compose", "-p", f"team{idx}", "-f", svc_dir / "docker-compose.yml", "down"], cwd=str(svc_dir), check=False, capture_output=True) - subprocess.run(["docker", "compose", "-f", svc_dir / "docker-compose.yml", + subprocess.run(["docker", "compose", "-p", f"team{idx}", "-f", svc_dir / "docker-compose.yml", "up", "-d"], cwd=str(svc_dir), check=False, capture_output=True) _start_receiver(idx) st = json.loads((team_dir / "state.json").read_text()) diff --git a/receiver/main.py b/receiver/main.py index 53121dc..efe2c7b 100644 --- a/receiver/main.py +++ b/receiver/main.py @@ -1,28 +1,28 @@ -1|from fastapi import Depends, FastAPI, HTTPException -2|from pydantic import BaseModel -3|from fastapi.security import HTTPBasic, HTTPBasicCredentials -4|from config import get_settings -5| -6|from challenges.Blogpost import Blogpost -7|from challenges.Carbeat import Carbeat -8|from challenges.CDN import CDN -9|from challenges.Phew import Phew -10|from challenges.Sheesh import Sheesh -11|from challenges.Warmup import Warmup -12| -13|import os -14|import asyncio -15|import logging -16| -17|# Setup logging -18|logging.basicConfig(level=logging.INFO) -19|logger = logging.getLogger(__name__) -20| -21|app = FastAPI() -22|security = HTTPBasic() -23|settings = get_settings() -24| -25|def _ch_port(name: str, default: int) -> int: +from fastapi import Depends, FastAPI, HTTPException +from pydantic import BaseModel +from fastapi.security import HTTPBasic, HTTPBasicCredentials +from config import get_settings + +from challenges.Blogpost import Blogpost +from challenges.Carbeat import Carbeat +from challenges.CDN import CDN +from challenges.Phew import Phew +from challenges.Sheesh import Sheesh +from challenges.Warmup import Warmup + +import os +import asyncio +import logging + +# Setup logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +app = FastAPI() +security = HTTPBasic() +settings = get_settings() + +def _ch_port(name: str, default: int) -> int: # read from .env manually (pydantic settings has fixed fields) val = os.environ.get(f"CHALLENGE_PORT_{name.upper()}") if not val: @@ -46,14 +46,136 @@ def _ch_container(name: str, default: str) -> str: except Exception: pass return val or default + challenges = { -32| "blogpost": Blogpost(_ch_port("blogpost", 10000)), -33| "carbeat": Carbeat(_ch_port("carbeat", 11000)), -34| "cdn": CDN(_ch_port("cdn", 12000)), -35| "phew": Phew(_ch_port("phew", 13000)), -36| "sheesh": Sheesh(_ch_port("sheesh", 14000)), -37| "warmup": Warmup(_ch_port("warmup", 15000)), -38|} -39| -40|async def run_challenge_checks(): -41| \ No newline at end of file + "blogpost": Blogpost(_ch_port("blogpost", 10000)), + "carbeat": Carbeat(_ch_port("carbeat", 11000)), + "cdn": CDN(_ch_port("cdn", 12000)), + "phew": Phew(_ch_port("phew", 13000)), + "sheesh": Sheesh(_ch_port("sheesh", 14000)), + "warmup": Warmup(_ch_port("warmup", 15000)), +} + +async def run_challenge_checks(): + """Run check function on all challenges at startup""" + logger.info("\n" + "="*60) + logger.info("Running challenge checks...") + logger.info("="*60 + "\n") + + results = {} + + for name, challenge in challenges.items(): + logger.info(f"\n[{name}] Starting check...") + try: + # Give service time between checks + await asyncio.sleep(2) + + result = challenge.check() + results[name] = result + + if result: + logger.info(f"[{name}] ✓ Check PASSED") + else: + logger.warning(f"[{name}] ✗ Check FAILED") + except Exception as e: + logger.error(f"[{name}] ✗ Check ERROR: {e}") + results[name] = False + + # Print summary + logger.info("\n" + "="*60) + logger.info("Challenge Check Summary:") + logger.info("="*60) + passed = sum(1 for r in results.values() if r) + total = len(results) + for name, result in results.items(): + status = "✓ PASS" if result else "✗ FAIL" + logger.info(f" {name:20} {status}") + logger.info(f"\nTotal: {passed}/{total} passed") + logger.info("="*60 + "\n") + + return results + +@app.on_event("startup") +async def startup_event(): + """Run challenge checks on application startup""" + asyncio.create_task(run_challenge_checks()) + +class Flag(BaseModel): + flag: str + challenge: str + +class History(BaseModel): + log: str + +@app.get("/") +def read_root(): + return {"service": "receiver-service"} + + +@app.get("/restart/{challenge}") +def restart(challenge: str, credentials: HTTPBasicCredentials = Depends(security)): + validate(credentials, challenge) + os.system(f"docker compose -f {settings.COMPOSE_LOCATION} restart {challenge}") + return {"message": "Challenge restarted"} + + +@app.get("/rollback/{challenge}") +def rollback(challenge: str, credentials: HTTPBasicCredentials = Depends(security)): + validate(credentials, challenge) + os.system(f"docker compose -f {settings.COMPOSE_LOCATION} up -d --force-recreate {challenge}") + return {"message": "Challenge restarted"} + + +@app.get("/activate/{challenge}") +def activate(challenge: str, credentials: HTTPBasicCredentials = Depends(security)): + validate(credentials, challenge) + os.system(f"docker compose -f {settings.COMPOSE_LOCATION} up -d {challenge}") + return {"message": "Challenge activated"} + + +@app.get("/deactivate/{challenge}") +def deactive(challenge: str, credentials: HTTPBasicCredentials = Depends(security)): + validate(credentials, challenge) + os.system(f"docker compose -f {settings.COMPOSE_LOCATION} down {challenge}") + return {"message": "Challenge deactivated"} + + +@app.get("/credential/{challenge}") +def credential(challenge: str, credentials: HTTPBasicCredentials = Depends(security)): + validate(credentials, challenge) + return challenges[challenge].credentials() + + +@app.post("/flag") +def receive(data: Flag, credentials: HTTPBasicCredentials = Depends(security)): + validate(credentials, data.challenge) + challenge = challenges[data.challenge] + if challenge.distribute(data.flag): + return {"message": "Flag received"} + + raise HTTPException(status_code=500, detail="Error receiving flag") + + +@app.get("/check/{challenge}") +def check(challenge: str, credentials: HTTPBasicCredentials = Depends(security)): + validate(credentials, challenge) + return {"success": challenges[challenge].check()} + +@app.post("/history") +def history(data: History): + with open('history/command.txt', 'a') as f: + f.write(data.log + '\n') + return {"message": "Command received"} + +def is_admin(credentials): + if credentials.username != settings.ADMIN_USERNAME or credentials.password != settings.ADMIN_PASSWORD: + return False + return True + + +def validate(credentials, challenge): + if not is_admin(credentials): + raise HTTPException(status_code=401, detail="Invalid credentials") + + if challenge not in challenges: + raise HTTPException(status_code=400, detail="Invalid challenge")