feat: multi-team orchestrator - topology UI, team management, flag randomizer, public submit + leaderboard
- panel/teams.py: create/start/stop teams with isolated ports+creds, per-team receivers with CHALLENGE_PORT/CONTAINER env, flag randomization, submit validation + leaderboard
- panel/main.py: /api/teams, /api/teams/{idx}/randomize, /api/flag/submit, /api/leaderboard, /api/public/teams, /submit page
- panel/static/submit.html: public flag submission UI for teams
- receiver: _ch_port/_ch_container read .env per team, container name overrides
- .gitignore: exclude teams/, .venv, __pycache__
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import os
|
||||
import io
|
||||
import re
|
||||
import time
|
||||
@@ -29,7 +30,7 @@ class Blogpost(Challenge):
|
||||
flag_location = 'flags/blogpost.txt' # Host copy (used by your orchestrator)
|
||||
history_location = 'history/blogpost.txt'
|
||||
container_flag_path = '/flag.txt'
|
||||
container_name = 'blogpost_container' # <-- set to your actual container name
|
||||
container_name = os.environ.get('CHALLENGE_CONTAINER_BLOGPOST', 'blogpost_container') # <-- set to your actual container name
|
||||
|
||||
# Heuristics to recognize ExifTool output
|
||||
_exif_markers = (
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import os
|
||||
import io
|
||||
import re
|
||||
import random
|
||||
@@ -26,7 +27,7 @@ class CDN(Challenge):
|
||||
flag_location = 'flags/cdn.txt' # host copy (written by your orchestrator)
|
||||
history_location = 'history/cdn.txt'
|
||||
container_flag_path = '/flag.txt'
|
||||
container_name = 'cdn_container' # adjust if your container name differs
|
||||
container_name = os.environ.get('CHALLENGE_CONTAINER_CDN', 'cdn_container') # adjust if your container name differs
|
||||
|
||||
# Canonical ExifTool markers (NO app fallbacks allowed)
|
||||
_exif_must_have = (
|
||||
|
||||
@@ -11,7 +11,7 @@ class Carbeat(Challenge):
|
||||
flag_location = 'flags/carbeat.txt'
|
||||
history_location = 'history/carbeat.txt'
|
||||
|
||||
_CONTAINER = "carbeat_container"
|
||||
_CONTAINER = os.environ.get("CHALLENGE_CONTAINER_CARBEAT", "carbeat_container")
|
||||
_SERVICE_CMD = ["docker", "exec", "-i", _CONTAINER, "/home/ctfuser/chall/mybini"]
|
||||
_HEX_RE = re.compile(r'^[0-9a-fA-F]+$')
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ class Phew(Challenge):
|
||||
flag_location = 'flags/phew.txt'
|
||||
history_location = 'history/phew.txt'
|
||||
|
||||
_CONTAINER = "phew_container"
|
||||
_CONTAINER = os.environ.get("CHALLENGE_CONTAINER_PHEW", "phew_container")
|
||||
_SERVICE_CMD = ["docker", "exec", "-i", _CONTAINER, "python3", "/home/ctfuser/chall/src/chall.py"]
|
||||
_HEX_RE = re.compile(r'^[0-9a-fA-F]+$')
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ class Sheesh(Challenge):
|
||||
flag_location = 'flags/sheesh.txt'
|
||||
history_location = 'history/sheesh.txt'
|
||||
|
||||
_CONTAINER = "sheesh_container"
|
||||
_CONTAINER = os.environ.get("CHALLENGE_CONTAINER_SHEESH", "sheesh_container")
|
||||
_SERVICE_CMD = ["docker", "exec", "-i", _CONTAINER, "python3", "/home/ctfuser/chall/src/chall.py"]
|
||||
_HEX_RE = re.compile(r'^[0-9a-fA-F]+$')
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from .Challenge import Challenge
|
||||
|
||||
import os
|
||||
import io
|
||||
import requests
|
||||
import random
|
||||
@@ -30,7 +31,7 @@ class Warmup(Challenge):
|
||||
with open(self.flag_location, 'r') as f:
|
||||
host_flag = f.read().strip()
|
||||
container_flag = subprocess.run([
|
||||
"docker", "exec", "warmup_container", "cat", "/flag.txt"
|
||||
"docker", "exec", os.environ.get("CHALLENGE_CONTAINER_WARMUP", "warmup_container"), "cat", "/flag.txt"
|
||||
], capture_output=True, text=True).stdout.strip()
|
||||
assert host_flag == container_flag, 'Flag mismatch between host and container'
|
||||
|
||||
|
||||
+57
-154
@@ -1,156 +1,59 @@
|
||||
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()
|
||||
|
||||
challenges = {
|
||||
"blogpost": Blogpost(10000),
|
||||
"carbeat": Carbeat(11000),
|
||||
"cdn": CDN(12000),
|
||||
"phew": Phew(13000),
|
||||
"sheesh": Sheesh(14000),
|
||||
"warmup": 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...")
|
||||
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:
|
||||
# read from .env manually (pydantic settings has fixed fields)
|
||||
val = os.environ.get(f"CHALLENGE_PORT_{name.upper()}")
|
||||
if not val:
|
||||
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
|
||||
with open(os.path.join(os.path.dirname(__file__), ".env")) as f:
|
||||
for line in f:
|
||||
if line.startswith(f"CHALLENGE_PORT_{name.upper()}="):
|
||||
val = line.strip().split("=", 1)[1]
|
||||
except Exception:
|
||||
pass
|
||||
return int(val) if val else default
|
||||
|
||||
@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")
|
||||
def _ch_container(name: str, default: str) -> str:
|
||||
val = os.environ.get(f"CHALLENGE_CONTAINER_{name.upper()}")
|
||||
if not val:
|
||||
try:
|
||||
with open(os.path.join(os.path.dirname(__file__), ".env")) as f:
|
||||
for line in f:
|
||||
if line.startswith(f"CHALLENGE_CONTAINER_{name.upper()}="):
|
||||
val = line.strip().split("=", 1)[1]
|
||||
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|
|
||||
Reference in New Issue
Block a user