diff --git a/.gitignore b/.gitignore
index f18f7f1..46dde39 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,5 +7,10 @@ receiver/lib
receiver/lib64
receiver/pyenv.cfg
receiver/include
-receiver/pwntools-docreceiver/.venv/
+receiver/pwntools-doc
+receiver/.venv/
panel/.env
+panel/__pycache__/
+teams/
+*.pid
+__pycache__/
\ No newline at end of file
diff --git a/panel/__pycache__/main.cpython-312.pyc b/panel/__pycache__/main.cpython-312.pyc
deleted file mode 100644
index 7bd0f1f..0000000
Binary files a/panel/__pycache__/main.cpython-312.pyc and /dev/null differ
diff --git a/panel/main.py b/panel/main.py
index c092bd0..0798dc4 100644
--- a/panel/main.py
+++ b/panel/main.py
@@ -14,6 +14,8 @@ from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
from fastapi.staticfiles import StaticFiles
from typing import Optional
+import teams as orch
+
RECEIVER_URL = os.environ.get("RECEIVER_URL", "http://127.0.0.1:18080")
ADMIN_USER = os.environ.get("PANEL_ADMIN_USER", "admin")
ADMIN_PASS = os.environ.get("PANEL_ADMIN_PASS", "admin")
@@ -88,6 +90,11 @@ async def login_page(req: Request):
return RedirectResponse("/")
return HTMLResponse((BASE_DIR / "static" / "login.html").read_text())
+@app.get("/submit", response_class=HTMLResponse)
+async def submit_page(req: Request):
+ """Public flag submission page for teams (no login)."""
+ return HTMLResponse((BASE_DIR / "static" / "submit.html").read_text())
+
@app.post("/api/login")
async def api_login(req: Request):
data = await req.json()
@@ -190,4 +197,166 @@ async def api_history(req: Request):
lines = (BASE_DIR.parent / "history" / "command.txt").read_text().splitlines()
except Exception:
lines = []
- return {"lines": lines[-200:]}
\ No newline at end of file
+ return {"lines": lines[-200:]}
+
+# ============ Multi-team orchestrator endpoints ============
+
+@app.get("/api/teams")
+async def api_teams(req: Request):
+ require_login(req)
+ return {"teams": orch.list_teams()}
+
+@app.post("/api/teams/set")
+async def api_teams_set(req: Request):
+ """Set/create N teams (idempotent: creates missing, keeps existing)."""
+ require_login(req)
+ data = await req.json()
+ n = int(data.get("count", 0))
+ if n < 0 or n > 50:
+ raise HTTPException(400, "Team count must be 0-50")
+ created = []
+ for i in range(1, n + 1):
+ td = orch.TEAMS_DIR / f"team{i}"
+ if not td.exists():
+ st = orch.create_team(i, data.get("label_prefix", "Tim"))
+ created.append(st["index"])
+ return {"created": created, "total": len(orch.list_teams())}
+
+@app.post("/api/teams/start")
+async def api_teams_start(req: Request):
+ require_login(req)
+ data = await req.json()
+ idx = data.get("index")
+ if idx is None:
+ # start all
+ results = []
+ for t in orch.list_teams():
+ try:
+ results.append({"team": t["index"], "ok": True})
+ orch.start_team(t["index"])
+ except Exception as e:
+ results.append({"team": t["index"], "ok": False, "err": str(e)})
+ return {"results": results}
+ try:
+ st = orch.start_team(int(idx))
+ return {"ok": True, "team": st}
+ except Exception as e:
+ raise HTTPException(500, str(e))
+
+@app.post("/api/teams/stop")
+async def api_teams_stop(req: Request):
+ require_login(req)
+ data = await req.json()
+ idx = data.get("index")
+ if idx is None:
+ results = []
+ for t in orch.list_teams():
+ try:
+ orch.stop_team(t["index"])
+ results.append({"team": t["index"], "ok": True})
+ except Exception as e:
+ results.append({"team": t["index"], "ok": False, "err": str(e)})
+ return {"results": results}
+ try:
+ st = orch.stop_team(int(idx))
+ return {"ok": True, "team": st}
+ except Exception as e:
+ raise HTTPException(500, str(e))
+
+@app.get("/api/teams/{idx}/logs")
+async def api_team_logs(idx: int, req: Request, service: Optional[str] = None, tail: int = 100):
+ require_login(req)
+ try:
+ logs = orch.team_logs(idx, service, tail)
+ return {"team": idx, "logs": logs}
+ except Exception as e:
+ raise HTTPException(500, str(e))
+
+@app.get("/api/teams/{idx}/creds")
+async def api_team_creds(idx: int, req: Request):
+ require_login(req)
+ try:
+ td = orch.TEAMS_DIR / f"team{idx}"
+ st = json.loads((td / "state.json").read_text())
+ return {"team": st}
+ except Exception as e:
+ raise HTTPException(404, str(e))
+
+@app.get("/api/topology")
+async def api_topology(req: Request):
+ """Return topology graph data (nodes + edges) for the UI."""
+ require_login(req)
+ teams = orch.list_teams()
+ nodes = [
+ {"id": "panel", "label": "Panel A/D", "type": "panel", "url": "https://panel.gemastik.imrnes.team"},
+ {"id": "traefik", "label": "Traefik / Coolify", "type": "infra"},
+ {"id": "dns", "label": "*.imrnes.team β 43.134.105.109", "type": "infra"},
+ ]
+ edges = [{"from": "dns", "to": "traefik"}, {"from": "traefik", "to": "panel"}]
+ for t in teams:
+ nid = f"team{t['index']}"
+ nodes.append({
+ "id": nid, "label": t.get("label", f"Team {t['index']}"),
+ "type": "team", "index": t["index"], "status": t.get("status", "unknown"),
+ "receiver_port": t["ports"]["receiver"], "ssh_pass": t.get("ssh_pass", ""),
+ })
+ edges.append({"from": "traefik", "to": nid, "label": f":{t['ports']['receiver']}"})
+ for name, coff, soff in orch.CHALLENGES:
+ cn = f"team{t['index']}-{name}"
+ nodes.append({"id": cn, "label": f"{name}", "type": "challenge",
+ "port": t["ports"][name]["chall"], "ssh": t["ports"][name]["ssh"]})
+ edges.append({"from": nid, "to": cn, "label": f":{t['ports'][name]['chall']}"})
+ return {"nodes": nodes, "edges": edges}
+
+
+# ============ Flag randomization + team submission ============
+
+@app.post("/api/teams/{idx}/randomize")
+async def api_randomize(idx: int, req: Request):
+ """Randomize all flags for a team (recreates containers to pick up new flags)."""
+ require_login(req)
+ try:
+ mapping = orch.randomize_flags(idx)
+ return {"ok": True, "team": idx, "flags": mapping}
+ except Exception as e:
+ raise HTTPException(500, str(e))
+
+
+@app.post("/api/flag/submit")
+async def api_flag_submit(req: Request):
+ """Public endpoint: teams submit flags. No login needed."""
+ data = await req.json()
+ team = int(data.get("team", 0))
+ chall = data.get("challenge", "")
+ flag = data.get("flag", "").strip()
+ team_name = data.get("team_name", "").strip()[:64] or f"Team {team}"
+ if team <= 0:
+ return {"success": False, "error": "Select your team"}
+ if chall not in [c[0] for c in orch.CHALLENGES]:
+ return {"success": False, "error": "Challenge not found"}
+ if not flag:
+ return {"success": False, "error": "Flag is required"}
+ return orch.submit_flag(team, chall, flag, team_name)
+
+
+@app.get("/api/leaderboard")
+async def api_leaderboard(req: Request):
+ """Leaderboard of solves so far."""
+ lb_path = orch.TEAMS_DIR / "leaderboard.json"
+ if lb_path.exists():
+ lb = json.loads(lb_path.read_text())
+ else:
+ lb = {"solves": []}
+ # aggregate per team
+ teams = {}
+ for e in lb["solves"]:
+ t = teams.setdefault(e["team"], {"team": e["team"], "name": e["team_name"], "solves": 0, "challs": []})
+ t["solves"] += 1
+ t["challs"].append(e["challenge"])
+ return {"solves": lb["solves"], "teams": sorted(teams.values(), key=lambda x: -x["solves"])}
+
+
+@app.get("/api/public/teams")
+async def api_public_teams():
+ """Public list of team names (for the submit dropdown)."""
+ return {"teams": [{"index": t["index"], "label": t.get("label", f"Team {t['index']}")} for t in orch.list_teams()]}
\ No newline at end of file
diff --git a/panel/static/index.html b/panel/static/index.html
index 433081b..f6d7684 100644
--- a/panel/static/index.html
+++ b/panel/static/index.html
@@ -12,11 +12,18 @@
color: #c9d4e3; min-height:100vh; padding:24px;
}
header {
- display:flex; justify-content:space-between; align-items:center; margin-bottom:24px;
+ display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;
border-bottom:1px solid #1e3a5f; padding-bottom:16px; flex-wrap:wrap; gap:12px;
}
h1 { font-size:22px; color:#5ad1ff; letter-spacing:1px; }
.actions { display:flex; gap:10px; align-items:center; }
+ .tabs { display:flex; gap:6px; margin-bottom:20px; flex-wrap:wrap; }
+ .tab {
+ background:#0e1526; border:1px solid #1e3a5f; color:#8aa0b8; padding:9px 16px;
+ border-radius:8px; cursor:pointer; font-family:inherit; font-size:13px; transition:all .2s;
+ }
+ .tab.active { background:#2563eb33; border-color:#3b82f6; color:#fff; }
+ .tab:hover { border-color:#3b82f6; }
.pill {
background:#0e1526; border:1px solid #1e3a5f; color:#8aa0b8; font-size:12px;
padding:7px 13px; border-radius:20px;
@@ -28,11 +35,11 @@
font-size:12px; padding:8px 14px; border-radius:8px; cursor:pointer; transition:all .2s;
}
button:hover { border-color:#3b82f6; color:#fff; }
- button.primary {
- background:linear-gradient(135deg,#0ea5e9,#2563eb); border:none; color:#fff; font-weight:700;
- }
+ button.primary { background:linear-gradient(135deg,#0ea5e9,#2563eb); border:none; color:#fff; font-weight:700; }
button.danger { border-color:#f8717155; color:#f87171; }
button.danger:hover { background:#f8717122; }
+ .view { display:none; }
+ .view.active { display:block; }
.grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(340px,1fr)); gap:16px; }
.card {
background:#0e1526cc; border:1px solid #1e3a5f; border-radius:12px; padding:18px;
@@ -56,11 +63,11 @@
.creds b { color:#a8c3e0; font-weight:600; }
.btn-row { display:flex; gap:6px; flex-wrap:wrap; margin-top:2px; }
.btn-row button { font-size:11px; padding:6px 10px; }
- textarea, input[type=text] {
+ textarea, input[type=text], input[type=number], select {
width:100%; background:#0a101f; border:1px solid #1e3a5f; color:#dbe6f4;
border-radius:8px; padding:10px; font-size:13px; font-family:inherit; outline:none; resize:vertical;
}
- textarea:focus, input:focus { border-color:#3b82f6; }
+ textarea:focus, input:focus, select:focus { border-color:#3b82f6; }
.modal-back {
position:fixed; inset:0; background:#000a; display:none; align-items:center; justify-content:center; z-index:50;
}
@@ -83,6 +90,14 @@
.footer-note { color:#4a5a70; font-size:11px; margin-top:20px; text-align:center; }
.spin { display:inline-block; width:12px; height:12px; border:2px solid #2a4a6f; border-top-color:#5ad1ff; border-radius:50%; animation:sp .7s linear infinite; }
@keyframes sp { to { transform:rotate(360deg); } }
+ .topo-wrap { background:#0a0f1c; border:1px solid #1e3a5f; border-radius:12px; padding:20px; overflow-x:auto; }
+ .topo-svg { width:100%; min-width:800px; }
+ .team-head { display:flex; justify-content:space-between; align-items:center; gap:10px; flex-wrap:wrap; }
+ .team-status { font-size:11px; }
+ .kv { display:grid; grid-template-columns:120px 1fr; gap:6px 12px; font-size:12px; }
+ .kv b { color:#8aa0b8; font-weight:600; }
+ .kv span { color:#dbe6f4; word-break:break-all; }
+ .mono { font-family:inherit; }
@@ -95,9 +110,77 @@
-
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
Jumlah Team (auto-create node per team)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | # | Tim | Solved | Challenges |
+
+
+
+
+
+
+
+
+
+
Log Server docker logs per team/service
+
+
+
+
+
+
+
+
Pilih team & serviceβ¦
+
+
+
+
+
+
@@ -122,13 +205,13 @@
-
-
-
-
Command History (preexec)
-
loading...
+
+
+
+
Kredensial Team
+
-
+
@@ -157,6 +240,12 @@ function esc(s) {
return (s||'').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
}
+function showView(v) {
+ document.querySelectorAll('.tab').forEach(b => b.classList.toggle('active', b.dataset.view === v));
+ document.querySelectorAll('.view').forEach(x => x.classList.toggle('active', x.id === 'view-' + v));
+}
+
+// ---------- Challenges ----------
async function refresh() {
const grid = document.getElementById('grid');
try {
@@ -186,7 +275,6 @@ async function refresh() {
`;
- // fetch creds lazily
fetch(`/api/credential/${ch.name}`).then(r=>r.json()).then(c => {
const el = document.getElementById('creds-' + ch.name);
if (el && c.username) el.innerHTML = `ctfuser / ${esc(c.password)}`;
@@ -230,7 +318,8 @@ async function viewCred(ch) {
try {
const c = await api(`/api/credential/${ch}`);
document.getElementById('mcTitle').textContent = 'SSH Credentials β ' + ch;
- let cmd = `ssh ctfuser@warmup.gemastik.imrnes.team -p 10022`.replace('warmup.gemastik.imrnes.team', ch + '.gemastik.imrnes.team').replace('10022', String(ch==='blogpost'?10022:ch==='carbeat'?11022:ch==='cdn'?12022:ch==='phew'?13022:ch==='sheesh'?14022:15022));
+ const sshPort = {blogpost:10022, carbeat:11022, cdn:12022, phew:13022, sheesh:14022, warmup:15022}[ch] || 10022;
+ const cmd = `ssh ctfuser@${ch}.gemastik.imrnes.team -p ${sshPort}`;
document.getElementById('mcBody').innerHTML =
`User: ctfuser
Pass: ${esc(c.password)}
@@ -240,23 +329,250 @@ async function viewCred(ch) {
} catch (e) { toast(e.message, true); }
}
-async function openHist() {
+// ---------- Topology ----------
+let topoLoaded = false;
+async function loadTopo() {
try {
- const d = await api('/api/history');
- document.getElementById('mhBody').textContent = d.lines.join('\n') || '(belum ada log)';
- document.getElementById('modalHist').classList.add('open');
+ const d = await api('/api/topology');
+ renderTopo(d.nodes, d.edges);
+ } catch (e) { toast('Topologi gagal: ' + e.message, true); }
+}
+
+function renderTopo(nodes, edges) {
+ const svg = document.getElementById('topoSvg');
+ const W = Math.max(900, nodes.length * 130);
+ const H = 400;
+ svg.setAttribute('width', W); svg.setAttribute('height', H);
+ const cx = W / 2;
+ const ns = {};
+ nodes.forEach(n => { ns[n.id] = n; });
+
+ // layout: panel/infra top center, teams in circle, challenges below each team
+ const pos = {};
+ pos['dns'] = {x: cx, y: 30};
+ pos['traefik'] = {x: cx, y: 100};
+ pos['panel'] = {x: cx - 140, y: 100};
+ const teams = nodes.filter(n => n.type === 'team');
+ const teamPos = {};
+ teams.forEach((t, i) => {
+ const ang = (i / Math.max(1, teams.length)) * Math.PI * 2 - Math.PI / 2;
+ const rx = W * 0.36, ry = 100;
+ const tx = cx + rx * Math.cos(ang);
+ const ty = 210 + ry * Math.sin(ang) * 0.6;
+ teamPos[t.index] = {x: tx, y: ty};
+ pos[t.id] = {x: tx, y: ty};
+ });
+ nodes.filter(n => n.type === 'challenge').forEach(n => {
+ const ti = n.id.split('-')[0].replace('team','');
+ const base = teamPos[ti] || {x: cx, y: 250};
+ const chIdx = ['blogpost','carbeat','cdn','phew','sheesh','warmup'].indexOf(n.label.split('-').pop() || n.label);
+ pos[n.id] = {x: base.x + (chIdx - 2.5) * 70, y: base.y + 110};
+ });
+
+ // edges
+ let html = '';
+ edges.forEach(e => {
+ const a = pos[e.from], b = pos[e.to];
+ if (!a || !b) return;
+ html += ``;
+ if (e.label) {
+ const mx = (a.x + b.x) / 2, my = (a.y + b.y) / 2 - 6;
+ html += `${e.label}`;
+ }
+ });
+
+ // nodes
+ nodes.forEach(n => {
+ const p = pos[n.id];
+ if (!p) return;
+ let fill = '#0e1526', stroke = '#2a4a6f', color = '#a8c3e0', r = 34;
+ if (n.type === 'panel') { fill = '#0ea5e933'; stroke = '#0ea5e9'; color = '#7dd3fc'; r = 42; }
+ if (n.type === 'infra') { r = 30; color = '#8aa0b8'; }
+ if (n.type === 'team') { fill = '#2563eb22'; stroke = '#3b82f6'; color = '#93c5fd'; r = 48; }
+ if (n.type === 'challenge') { r = 26; color = '#c9d4e3'; }
+ const status = n.status === 'running' ? ' fill="#34d399"' : '';
+ const sub = n.type === 'team' ? `:${n.receiver_port}` : (n.port ? `:${n.port}` : '');
+ html += `
+
+
+ ${esc(n.label)}
+ ${sub ? `${sub}` : ''}
+ ${status ? `` : ''}
+ `;
+ });
+ svg.innerHTML = `` + html;
+}
+
+// ---------- Teams ----------
+async function loadTeams() {
+ try {
+ const d = await api('/api/teams');
+ document.getElementById('teamCount').value = d.teams.length;
+ loadLeaderboard();
+ const grid = document.getElementById('teamsGrid');
+ if (!d.teams.length) { grid.innerHTML = 'Belum ada team. Set jumlah team untuk auto-create.
'; return; }
+ let html = '';
+ for (const t of d.teams) {
+ const alive = t.status === 'running';
+ html += `
+
+ ${esc(t.label || ('Team ' + t.index))}
+ ${alive ? 'β RUNNING' : 'β STOPPED'}
+
+
+ Receiver${t.ports.receiver}
+ Admin${esc(t.admin_user)}
+ SSH userctfuser
+ Status${esc(t.status)}
+
+
+
+
+
+
+
+
+
`;
+ }
+ grid.innerHTML = html;
+ } catch (e) { toast('Teams gagal: ' + e.message, true); }
+}
+
+async function setTeams() {
+ const n = parseInt(document.getElementById('teamCount').value || '0', 10);
+ try {
+ const d = await api('/api/teams/set', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({count: n})});
+ toast(`Team dibuat: ${d.created.join(', ') || 'tidak ada yang baru'} Β· total ${d.total}`, false);
+ loadTeams();
} catch (e) { toast(e.message, true); }
}
+async function startTeam(idx) {
+ try { await api('/api/teams/start', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({index: idx})}); toast(`Team ${idx} start (build bisa makan waktu)`, false); setTimeout(loadTeams, 3000); }
+ catch (e) { toast(e.message, true); }
+}
+async function stopTeam(idx) {
+ try { await api('/api/teams/stop', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({index: idx})}); toast(`Team ${idx} stop`, false); loadTeams(); }
+ catch (e) { toast(e.message, true); }
+}
+async function startAllTeams() {
+ try { const d = await api('/api/teams/start', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({})}); toast(`Start semua: ${d.results.filter(r=>r.ok).length}/${d.results.length} ok`, false); setTimeout(loadTeams, 4000); }
+ catch (e) { toast(e.message, true); }
+}
+async function stopAllTeams() {
+ try { const d = await api('/api/teams/stop', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({})}); toast(`Stop semua: ${d.results.filter(r=>r.ok).length}/${d.results.length} ok`, false); loadTeams(); }
+ catch (e) { toast(e.message, true); }
+}
+
+async function randomizeTeam(idx) {
+ if (!confirm(`Randomize SEMUA flag untuk Team ${idx}? Container akan di-recreate.`)) return;
+ try {
+ const d = await api(`/api/teams/${idx}/randomize`, {method:'POST'});
+ const flags = Object.values(d.flags || {}).join('\n');
+ toast(`Flag Team ${idx} di-randomize!`, false);
+ console.log('NEW FLAGS team', idx, flags);
+ setTimeout(loadTeams, 4000);
+ } catch (e) { toast(e.message, true); }
+}
+
+async function loadLeaderboard() {
+ try {
+ const d = await api('/api/leaderboard');
+ const body = document.getElementById('lbBody');
+ if (!body) return;
+ if (!d.teams.length) { body.innerHTML = '| Belum ada solve. |
'; return; }
+ body.innerHTML = d.teams.map((t,i) => `
+ | ${i+1} | ${esc(t.name)} | ${t.solves} |
+ ${t.challs.map(c=>`${esc(c)}`).join('')} |
+
`).join('');
+ } catch (e) {}
+}
+setInterval(loadLeaderboard, 15000);
+
+async function viewTeamCred(idx) {
+ try {
+ const t = await api(`/api/teams/${idx}/creds`);
+ let html = `
+ Label${esc(t.team.label || ('Team ' + idx))}
+ Admin receiver${esc(t.team.admin_user)} / ${esc(t.team.admin_pass)}
+ Receiver port${esc(t.team.ports.receiver)}
+ SSH userctfuser
+ SSH pass${esc(t.team.ssh_pass)}
+
Per-challenge SSH:
`;
+ for (const [name, p] of Object.entries(t.team.ports)) {
+ if (name === 'receiver' || name === 'panel') continue;
+ html += `
+ ${name}ssh ctfuser@${name}.gemastik.imrnes.team -p ${p.ssh} Β· pass: ${esc(t.team.chall_passwords[name])}
+
`;
+ }
+ document.getElementById('mtcTitle').textContent = `Kredensial Team ${idx}`;
+ document.getElementById('mtcBody').innerHTML = html;
+ document.getElementById('modalTeamCred').classList.add('open');
+ } catch (e) { toast(e.message, true); }
+}
+
+function openTeamLogs(idx) {
+ document.getElementById('logTeam').value = idx;
+ showView('logs'); loadLogs();
+}
+
+// ---------- Logs ----------
+async function loadLogs() {
+ const idx = document.getElementById('logTeam').value;
+ const svc = document.getElementById('logService').value;
+ await populateLogTeams();
+ if (!idx) { document.getElementById('logsBox').textContent = 'Pilih team dulu.'; return; }
+ try {
+ const d = await api(`/api/teams/${idx}/logs` + (svc ? `?service=${svc}` : '') + '&tail=200');
+ let out = '';
+ for (const [name, log] of Object.entries(d.logs)) {
+ out += `\nβββ ${name} (team ${idx}) βββ\n${log}\n`;
+ }
+ document.getElementById('logsBox').textContent = out || '(kosong)';
+ } catch (e) { document.getElementById('logsBox').textContent = 'Error: ' + e.message; }
+}
+async function populateLogTeams() {
+ try {
+ const d = await api('/api/teams');
+ const sel = document.getElementById('logTeam');
+ if (sel.options.length === 0 || sel.value === '') {
+ sel.innerHTML = d.teams.map(t => ``).join('');
+ }
+ } catch (e) {}
+}
+
+// ---------- Creds ----------
+async function loadCreds() {
+ try {
+ const d = await api('/api/teams');
+ const grid = document.getElementById('credsGrid');
+ if (!d.teams.length) { grid.innerHTML = 'Belum ada team.
'; return; }
+ let html = '';
+ for (const t of d.teams) {
+ html += `
+
+ ${esc(t.label || ('Team ' + t.index))}
+
+
+
+ Admin${esc(t.admin_user)} / ${esc(t.admin_pass)}
+ SSHctfuser / ${esc(t.ssh_pass)}
+
+
`;
+ }
+ grid.innerHTML = html;
+ } catch (e) { toast(e.message, true); }
+}
+
+// ---------- misc ----------
function closeModal(id) { document.getElementById(id).classList.remove('open'); }
async function logout() {
await fetch('/api/logout', {method:'POST'});
location.href = '/login';
}
-
-document.getElementById('modalFlag').addEventListener('click', e => { if (e.target === e.currentTarget) closeModal('modalFlag'); });
-document.getElementById('modalCred').addEventListener('click', e => { if (e.target === e.currentTarget) closeModal('modalCred'); });
-document.getElementById('modalHist').addEventListener('click', e => { if (e.target === e.currentTarget) closeModal('modalHist'); });
+['modalFlag','modalCred','modalTeamCred'].forEach(id => {
+ document.getElementById(id).addEventListener('click', e => { if (e.target === e.currentTarget) closeModal(id); });
+});
function tick() {
document.getElementById('clock').textContent = new Date().toLocaleTimeString('id-ID');
diff --git a/panel/static/submit.html b/panel/static/submit.html
new file mode 100644
index 0000000..fc90b54
--- /dev/null
+++ b/panel/static/submit.html
@@ -0,0 +1,131 @@
+
+
+
+
+
+Gemastik A/D β Submit Flag
+
+
+
+
+
+
+
+
π© Submit Flag
+
+
+
+
+
+
+
+
+
+
+
+
π Leaderboard
+
+ | # | Tim | Solved | Challenges |
+ | Belum ada solve. |
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/panel/teams.py b/panel/teams.py
new file mode 100644
index 0000000..ac686d2
--- /dev/null
+++ b/panel/teams.py
@@ -0,0 +1,324 @@
+#!/usr/bin/env python3
+"""
+Gemastik A/D multi-team orchestrator.
+
+Each team gets an isolated stack: its own docker-compose (unique ports +
+SSH passwords), its own receiver (unique admin creds + ports), and its own
+flags. The orchestrator clones the base services dir, rewrites ports and
+passwords, and manages lifecycle via docker compose project per team.
+
+Team N layout:
+ /opt/gemastik18-final/teams/team/
+ services/ (docker-compose.yml with team ports + passwords)
+ receiver/ (.env with team admin creds + container overrides)
+ receiver/flags/ (per-team flag files)
+ 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)
+"""
+import json
+import os
+import re
+import secrets
+import shutil
+import subprocess
+import time
+import uuid
+from datetime import datetime
+from pathlib import Path
+
+BASE = Path("/opt/gemastik18-final")
+TEAMS_DIR = BASE / "teams"
+SERVICES_SRC = BASE / "services"
+RECEIVER_SRC = BASE / "receiver"
+
+# Challenge definitions: (service name, chall port offset 0-5, ssh offset 22-27)
+CHALLENGES = [
+ ("blogpost", 0, 22),
+ ("carbeat", 1, 23),
+ ("cdn", 2, 24),
+ ("phew", 3, 25),
+ ("sheesh", 4, 26),
+ ("warmup", 5, 27),
+]
+
+PORT_BASE = 20000
+STEP = 1000
+
+def team_ports(idx: int) -> dict:
+ """Return {service_name: {'chall': port, 'ssh': port}} for 1-based team idx."""
+ base = PORT_BASE + idx * STEP
+ out = {}
+ for name, coff, soff in CHALLENGES:
+ out[name] = {"chall": base + coff, "ssh": base + soff}
+ out["receiver"] = base + 80
+ out["panel"] = base + 81 # reserved, not used
+ return out
+
+def gen_password(n=16):
+ return uuid.uuid4().hex[:n]
+
+def create_team(idx: int, label: str = None):
+ """Build a full team stack dir with unique ports/passwords."""
+ ports = team_ports(idx)
+ team_dir = TEAMS_DIR / f"team{idx}"
+ label = label or f"Tim {idx}"
+ state = {
+ "index": idx,
+ "label": label,
+ "ports": ports,
+ "admin_user": f"admin_team{idx}",
+ "admin_pass": gen_password(20),
+ "ssh_user": "ctfuser",
+ "ssh_pass": gen_password(20),
+ "chall_passwords": {name: gen_password(20) for name, *_ in CHALLENGES},
+ "container_suffix": f"team{idx}",
+ "created": __import__("datetime").datetime.now().isoformat(),
+ "status": "created",
+ }
+
+ # --- copy services with rewrite ---
+ svc_dir = team_dir / "services"
+ if svc_dir.exists():
+ shutil.rmtree(svc_dir)
+ shutil.copytree(SERVICES_SRC, svc_dir, ignore=shutil.ignore_patterns("__pycache__", ".git", "exploits", "exploit"))
+ # compose references ../utils/bashrc etc β copy utils next to services
+ utils_src = BASE / "utils"
+ utils_dst = team_dir / "utils"
+ if utils_src.exists():
+ if utils_dst.exists():
+ shutil.rmtree(utils_dst)
+ shutil.copytree(utils_src, utils_dst)
+ # redirect preexec URL to the team's receiver port
+ recv_port = ports["receiver"]
+ bashrc = utils_dst / "bashrc"
+ if bashrc.exists():
+ bashrc.write_text(bashrc.read_text().replace(
+ "host.docker.internal:18080", f"host.docker.internal:{recv_port}"))
+ compose = svc_dir / "docker-compose.yml"
+ text = compose.read_text()
+ # rewrite container names + ports per challenge
+ for name, coff, soff in CHALLENGES:
+ cont_old = f"{name}_container"
+ cont_new = f"{name}_container_team{idx}"
+ text = text.replace(f"container_name: {cont_old}", f"container_name: {cont_new}")
+ text = text.replace(f"hostname: {name}", f"hostname: {name}_team{idx}")
+ # ports mapping: "10000:8000" -> ":8000"
+ old_chall = str(10000 + coff * 1000) # 10000,11000,12000,13000,14000,15000
+ old_ssh = str(10022 + coff * 1000) # 10022,11022,...
+ text = re.sub(rf'"({old_chall}):', f'"{ports[name]["chall"]}:', text)
+ text = re.sub(rf'"({old_ssh}):', f'"{ports[name]["ssh"]}:', text)
+ # PASSWORD_* args -> team passwords
+ for name, coff, soff in CHALLENGES:
+ old_env = f"PASSWORD_{10000 + coff * 1000}"
+ text = re.sub(rf"\${{{old_env}}}", state["chall_passwords"][name], text)
+ # compose file references services/.env β we'll create it below
+ compose.write_text(text)
+
+ # team services/.env (PASSWORD_* in same shape as starter.py)
+ env_lines = [f"ADMIN_USERNAME={state['admin_user']}", f"ADMIN_PASSWORD={state['admin_pass']}"]
+ env_lines.append(f"COMPOSE_LOCATION={svc_dir}/docker-compose.yml")
+ for i in range(20):
+ env_lines.append(f"PASSWORD_{(i*1000)+10000}={gen_password(20)}")
+ # force the six used passwords to team ones
+ for name, coff, soff in CHALLENGES:
+ for j, line in enumerate(env_lines):
+ if line.startswith(f"PASSWORD_{10000+coff*1000}="):
+ env_lines[j] = f"PASSWORD_{10000+coff*1000}={state['chall_passwords'][name]}"
+ (svc_dir / ".env").write_text("\n".join(env_lines) + "\n")
+
+ # --- copy receiver with team env ---
+ recv_dir = team_dir / "receiver"
+ if recv_dir.exists():
+ shutil.rmtree(recv_dir)
+ shutil.copytree(RECEIVER_SRC, recv_dir, ignore=shutil.ignore_patterns("__pycache__", ".venv", ".env", "history"))
+ (recv_dir / "history").mkdir(exist_ok=True)
+ # receiver .env: same admin + passwords + container overrides
+ recv_env = [f"ADMIN_USERNAME={state['admin_user']}", f"ADMIN_PASSWORD={state['admin_pass']}",
+ f"COMPOSE_LOCATION={svc_dir}/docker-compose.yml"]
+ for i in range(20):
+ recv_env.append(f"PASSWORD_{(i*1000)+10000}={gen_password(20)}")
+ for name, coff, soff in CHALLENGES:
+ for j, line in enumerate(recv_env):
+ if line.startswith(f"PASSWORD_{10000+coff*1000}="):
+ recv_env[j] = f"PASSWORD_{10000+coff*1000}={state['chall_passwords'][name]}"
+ recv_env.append(f"CHALLENGE_PORT_{name.upper()}={ports[name]['chall']}")
+ recv_env.append(f"CHALLENGE_CONTAINER_{name.upper()}={name}_container_team{idx}")
+ (recv_dir / ".env").write_text("\n".join(recv_env) + "\n")
+
+ # --- flags (randomized per team so each team has unique flags) ---
+ flags_dir = team_dir / "receiver" / "flags"
+ flags_dir.mkdir(parents=True, exist_ok=True)
+ flags_map = {}
+ for name, coff, soff in CHALLENGES:
+ flag = f"GEMASTIK18{{TEAM{idx}_{name.upper()}_{secrets.token_hex(6)}}}"
+ (flags_dir / f"{name}.txt").write_text(flag)
+ flags_map[name] = flag
+ state["flags"] = flags_map
+
+ (team_dir / "state.json").write_text(json.dumps(state, indent=2))
+ return state
+
+def start_team(idx: int):
+ team_dir = TEAMS_DIR / f"team{idx}"
+ 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"],
+ cwd=str(svc_dir), check=False, capture_output=True)
+ _start_receiver(idx)
+ st = json.loads((team_dir / "state.json").read_text())
+ st["status"] = "running"
+ (team_dir / "state.json").write_text(json.dumps(st, indent=2))
+ return st
+
+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"],
+ cwd=str(svc_dir), check=False, capture_output=True)
+ _stop_receiver(idx)
+ st = json.loads((team_dir / "state.json").read_text())
+ st["status"] = "stopped"
+ (team_dir / "state.json").write_text(json.dumps(st, indent=2))
+ return st
+
+def _start_receiver(idx: int):
+ """Launch team's receiver as a detached uvicorn process with team env."""
+ team_dir = TEAMS_DIR / f"team{idx}"
+ recv_dir = team_dir / "receiver"
+ st = json.loads((team_dir / "state.json").read_text())
+ port = st["ports"]["receiver"]
+ pidfile = team_dir / "receiver.pid"
+ # kill existing
+ _stop_receiver(idx)
+ env = dict(os.environ)
+ env["PYTHONPATH"] = str(recv_dir)
+ env["COMPOSE_LOCATION"] = str(team_dir / "services" / "docker-compose.yml")
+ # expose team ports/containers so challenge classes bind the right targets
+ for ch in st["ports"]:
+ if ch in ("receiver", "panel"):
+ continue
+ env[f"CHALLENGE_PORT_{ch.upper()}"] = str(st["ports"][ch]["chall"])
+ env[f"CHALLENGE_CONTAINER_{ch.upper()}"] = f"{ch}_container_team{idx}"
+ proc = subprocess.Popen(
+ [str(RECEIVER_SRC.parent / "receiver" / ".venv" / "bin" / "python"),
+ "-m", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", str(port)],
+ cwd=str(recv_dir), env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
+ start_new_session=True)
+ pidfile.write_text(str(proc.pid))
+
+def _stop_receiver(idx: int):
+ team_dir = TEAMS_DIR / f"team{idx}"
+ pidfile = team_dir / "receiver.pid"
+ if pidfile.exists():
+ try:
+ pid = int(pidfile.read_text().strip())
+ os.kill(pid, 15)
+ except Exception:
+ pass
+ pidfile.unlink(missing_ok=True)
+
+def team_logs(idx: int, service: str = None, tail: int = 100):
+ team_dir = TEAMS_DIR / f"team{idx}"
+ svc_dir = team_dir / "services"
+ data = {}
+ services = [s for s, *_ in CHALLENGES] if service is None else [service]
+ for s in services:
+ try:
+ out = subprocess.run(
+ ["docker", "logs", "--tail", str(tail), f"{s}_container_team{idx}"],
+ capture_output=True, text=True, timeout=15)
+ data[s] = (out.stdout + out.stderr)[-4000:]
+ except Exception as e:
+ data[s] = f"ERR: {e}"
+ return data
+
+def list_teams() -> list:
+ out = []
+ if not TEAMS_DIR.exists():
+ return out
+ for d in sorted(TEAMS_DIR.glob("team*")):
+ if (d / "state.json").exists():
+ st = json.loads((d / "state.json").read_text())
+ # compute alive status quickly
+ st["alive_count"] = 0
+ st["up_count"] = 0
+ out.append(st)
+ return out
+
+
+# ---------------------------------------------------------------------------
+# Flag randomization + team submission tracking
+# ---------------------------------------------------------------------------
+
+def randomize_flags(idx: int) -> dict:
+ """Generate fresh unique flags for every challenge of a team."""
+ team_dir = TEAMS_DIR / f"team{idx}"
+ if not (team_dir / "state.json").exists():
+ raise FileNotFoundError(f"Team {idx} not created")
+ flags_dir = team_dir / "receiver" / "flags"
+ flags_dir.mkdir(parents=True, exist_ok=True)
+ mapping = {}
+ for name, coff, soff in CHALLENGES:
+ token = secrets.token_hex(6)
+ flag = f"GEMASTIK18{{TEAM{idx}_{name.upper()}_{token}}}"
+ (flags_dir / f"{name}.txt").write_text(flag)
+ mapping[name] = flag
+ # 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",
+ "down"], cwd=str(svc_dir), check=False, capture_output=True)
+ subprocess.run(["docker", "compose", "-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())
+ st["flags"] = mapping
+ (team_dir / "state.json").write_text(json.dumps(st, indent=2))
+ return mapping
+
+
+def submit_flag(team_idx: int, chall: str, flag: str, team_name: str = "") -> dict:
+ """Validate a submitted flag against the owning team's challenge flag."""
+ team_dir = TEAMS_DIR / f"team{team_idx}"
+ if not (team_dir / "state.json").exists():
+ return {"success": False, "error": "unknown team"}
+ flags_dir = team_dir / "receiver" / "flags"
+ expected = (flags_dir / f"{chall}.txt").read_text().strip() if (flags_dir / f"{chall}.txt").exists() else None
+ if not expected:
+ return {"success": False, "error": "challenge not found"}
+ if flag.strip() != expected:
+ return {"success": False, "error": "wrong flag"}
+ # record leaderboard entry
+ lb_path = TEAMS_DIR / "leaderboard.json"
+ lb = json.loads(lb_path.read_text()) if lb_path.exists() else {"solves": []}
+ entry = {
+ "team": team_idx,
+ "team_name": team_name or f"Team {team_idx}",
+ "challenge": chall,
+ "flag": flag,
+ "ts": time.time(),
+ "ts_human": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
+ }
+ # avoid double-solve duplicates (same flag + same team)
+ if not any(e["team"] == team_idx and e["challenge"] == chall for e in lb["solves"]):
+ lb["solves"].append(entry)
+ lb_path.write_text(json.dumps(lb, indent=2))
+ return {"success": True, "team": team_idx, "challenge": chall}
+
+if __name__ == "__main__":
+ import sys
+ cmd = sys.argv[1] if len(sys.argv) > 1 else "list"
+ if cmd == "create" and len(sys.argv) > 2:
+ st = create_team(int(sys.argv[2]))
+ print(json.dumps(st, indent=2))
+ elif cmd == "list":
+ print(json.dumps(list_teams(), indent=2))
+ elif cmd == "start" and len(sys.argv) > 2:
+ print(json.dumps(start_team(int(sys.argv[2])), indent=2))
+ elif cmd == "stop" and len(sys.argv) > 2:
+ print(json.dumps(stop_team(int(sys.argv[2])), indent=2))
\ No newline at end of file
diff --git a/receiver/challenges/Blogpost.py b/receiver/challenges/Blogpost.py
index 6f0f2b6..b1eadb0 100644
--- a/receiver/challenges/Blogpost.py
+++ b/receiver/challenges/Blogpost.py
@@ -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 = (
diff --git a/receiver/challenges/CDN.py b/receiver/challenges/CDN.py
index 17e6535..e1a2d83 100644
--- a/receiver/challenges/CDN.py
+++ b/receiver/challenges/CDN.py
@@ -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 = (
diff --git a/receiver/challenges/Carbeat.py b/receiver/challenges/Carbeat.py
index ab2f414..fe8685d 100644
--- a/receiver/challenges/Carbeat.py
+++ b/receiver/challenges/Carbeat.py
@@ -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]+$')
diff --git a/receiver/challenges/Phew.py b/receiver/challenges/Phew.py
index e069b59..444d036 100644
--- a/receiver/challenges/Phew.py
+++ b/receiver/challenges/Phew.py
@@ -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]+$')
diff --git a/receiver/challenges/Sheesh.py b/receiver/challenges/Sheesh.py
index 903481f..37ccdb2 100644
--- a/receiver/challenges/Sheesh.py
+++ b/receiver/challenges/Sheesh.py
@@ -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]+$')
diff --git a/receiver/challenges/Warmup.py b/receiver/challenges/Warmup.py
index ef347f1..33bf3b9 100644
--- a/receiver/challenges/Warmup.py
+++ b/receiver/challenges/Warmup.py
@@ -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'
diff --git a/receiver/main.py b/receiver/main.py
index 21fb543..53121dc 100644
--- a/receiver/main.py
+++ b/receiver/main.py
@@ -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|
\ No newline at end of file