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:
+6
-1
@@ -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__/
|
||||
Binary file not shown.
+170
-1
@@ -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:]}
|
||||
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()]}
|
||||
+340
-24
@@ -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; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -95,9 +110,77 @@
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="grid" id="grid"></div>
|
||||
<div class="tabs">
|
||||
<button class="tab active" data-view="challs" onclick="showView('challs')">🎯 Challenges</button>
|
||||
<button class="tab" data-view="topo" onclick="showView('topo'); loadTopo()">🗺️ Topology</button>
|
||||
<button class="tab" data-view="teams" onclick="showView('teams'); loadTeams()">👥 Teams</button>
|
||||
<button class="tab" data-view="logs" onclick="showView('logs'); loadLogs()">📜 Logs</button>
|
||||
<button class="tab" data-view="creds" onclick="showView('creds'); loadCreds()">🔑 Creds</button>
|
||||
</div>
|
||||
|
||||
<div class="footer-note">Receiver: https://gemastik.imrnes.team · Auto-refresh tiap 15 detik · Flags dirotate lewat panel ini</div>
|
||||
<!-- Challenges view -->
|
||||
<div class="view active" id="view-challs">
|
||||
<div class="grid" id="grid"></div>
|
||||
</div>
|
||||
|
||||
<!-- Topology view -->
|
||||
<div class="view" id="view-topo">
|
||||
<div class="topo-wrap"><svg id="topoSvg" class="topo-svg" height="420"></svg></div>
|
||||
</div>
|
||||
|
||||
<!-- Teams view -->
|
||||
<div class="view" id="view-teams">
|
||||
<div class="card" style="margin-bottom:16px">
|
||||
<div class="team-head">
|
||||
<div><b style="color:#5ad1ff">Jumlah Team</b> <span style="font-size:11px;color:#718096">(auto-create node per team)</span></div>
|
||||
<div style="display:flex;gap:8px;align-items:center">
|
||||
<input type="number" id="teamCount" min="0" max="50" value="0" style="width:90px">
|
||||
<button class="primary" onclick="setTeams()">Set & Buat</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top:10px;display:flex;gap:8px;flex-wrap:wrap">
|
||||
<button class="primary" onclick="startAllTeams()">▶ Start CTF (semua)</button>
|
||||
<button class="danger" onclick="stopAllTeams()">⏹ Stop CTF (semua)</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid" id="teamsGrid"></div>
|
||||
<div class="card" style="margin-top:16px">
|
||||
<div class="team-head">
|
||||
<div><b style="color:#5ad1ff">🏆 Leaderboard</b> <span style="font-size:11px;color:#718096">solve flag per team (dari halaman submit publik)</span></div>
|
||||
<a href="/submit" target="_blank" class="primary" style="text-decoration:none;padding:8px 14px;border-radius:8px">🚩 Halaman Submit Team →</a>
|
||||
</div>
|
||||
<table style="width:100%;border-collapse:collapse;margin-top:10px;font-size:13px">
|
||||
<thead><tr style="color:#718096;text-align:left"><th style="padding:6px">#</th><th>Tim</th><th>Solved</th><th>Challenges</th></tr></thead>
|
||||
<tbody id="lbBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Logs view -->
|
||||
<div class="view" id="view-logs">
|
||||
<div class="card" style="margin-bottom:16px">
|
||||
<div class="team-head">
|
||||
<div><b style="color:#5ad1ff">Log Server</b> <span style="font-size:11px;color:#718096">docker logs per team/service</span></div>
|
||||
<div style="display:flex;gap:8px;align-items:center">
|
||||
<select id="logTeam" style="width:130px"></select>
|
||||
<select id="logService" style="width:150px">
|
||||
<option value="">semua service</option>
|
||||
<option>blogpost</option><option>carbeat</option><option>cdn</option>
|
||||
<option>phew</option><option>sheesh</option><option>warmup</option>
|
||||
</select>
|
||||
<button class="primary" onclick="loadLogs()">Muat</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="history-box" id="logsBox" style="max-height:70vh">Pilih team & service…</div>
|
||||
</div>
|
||||
|
||||
<!-- Creds view -->
|
||||
<div class="view" id="view-creds">
|
||||
<div class="grid" id="credsGrid"></div>
|
||||
</div>
|
||||
|
||||
<div class="footer-note">Receiver: https://gemastik.imrnes.team · Panel: https://panel.gemastik.imrnes.team · Auto-refresh tiap 15 detik</div>
|
||||
|
||||
<!-- modal flag -->
|
||||
<div class="modal-back" id="modalFlag">
|
||||
@@ -122,13 +205,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- modal history -->
|
||||
<div class="modal-back" id="modalHist">
|
||||
<div class="modal" style="width:560px">
|
||||
<h2>Command History (preexec)</h2>
|
||||
<div class="history-box" id="mhBody">loading...</div>
|
||||
<!-- modal team creds -->
|
||||
<div class="modal-back" id="modalTeamCred">
|
||||
<div class="modal" style="width:520px">
|
||||
<h2 id="mtcTitle">Kredensial Team</h2>
|
||||
<div id="mtcBody" style="font-size:12px;color:#dbe6f4;line-height:1.9"></div>
|
||||
<div class="btn-row" style="justify-content:flex-end;margin-top:14px">
|
||||
<button onclick="closeModal('modalHist')">Tutup</button>
|
||||
<button onclick="closeModal('modalTeamCred')">Tutup</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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() {
|
||||
<button onclick="viewCred('${esc(ch.name)}')">SSH</button>
|
||||
</div>
|
||||
</div>`;
|
||||
// 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 = `<b>ctfuser</b> / <b>${esc(c.password)}</b>`;
|
||||
@@ -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 =
|
||||
`<div>User: <b>ctfuser</b></div>
|
||||
<div>Pass: <b>${esc(c.password)}</b></div>
|
||||
@@ -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 += `<line x1="${a.x}" y1="${a.y}" x2="${b.x}" y2="${b.y}" stroke="#2a4a6f" stroke-width="1.5" stroke-dasharray="4 3"/>`;
|
||||
if (e.label) {
|
||||
const mx = (a.x + b.x) / 2, my = (a.y + b.y) / 2 - 6;
|
||||
html += `<text x="${mx}" y="${my}" fill="#5a7a9f" font-size="9" text-anchor="middle">${e.label}</text>`;
|
||||
}
|
||||
});
|
||||
|
||||
// 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 += `<g>
|
||||
<circle cx="${p.x}" cy="${p.y}" r="${r}" fill="${fill}" stroke="${stroke}" stroke-width="1.5"/>
|
||||
<circle cx="${p.x}" cy="${p.y}" r="${r-4}" fill="none" stroke="${stroke}" stroke-opacity="0.3"/>
|
||||
<text x="${p.x}" y="${p.y - (sub ? 0 : 4)}" fill="${color}" font-size="${n.type==='team'?12:10}" font-weight="bold" text-anchor="middle">${esc(n.label)}</text>
|
||||
${sub ? `<text x="${p.x}" y="${p.y + 12}" fill="#5a7a9f" font-size="9" text-anchor="middle">${sub}</text>` : ''}
|
||||
${status ? `<circle cx="${p.x + r - 8}" cy="${p.y - r + 8}" r="5" fill="#34d399"/>` : ''}
|
||||
</g>`;
|
||||
});
|
||||
svg.innerHTML = `<defs><marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="5" markerHeight="5" orient="auto"><path d="M0,0 L10,5 L0,10 z" fill="#2a4a6f"/></marker></defs>` + 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 = '<div class="card"><span style="color:#718096">Belum ada team. Set jumlah team untuk auto-create.</span></div>'; return; }
|
||||
let html = '';
|
||||
for (const t of d.teams) {
|
||||
const alive = t.status === 'running';
|
||||
html += `<div class="card ${alive ? '' : 'dead'}">
|
||||
<div class="team-head">
|
||||
<span class="ch-name">${esc(t.label || ('Team ' + t.index))}</span>
|
||||
<span class="status ${alive ? 'up' : 'down'}">${alive ? '● RUNNING' : '● STOPPED'}</span>
|
||||
</div>
|
||||
<div class="kv">
|
||||
<b>Receiver</b><span>${t.ports.receiver}</span>
|
||||
<b>Admin</b><span>${esc(t.admin_user)}</span>
|
||||
<b>SSH user</b><span>ctfuser</span>
|
||||
<b>Status</b><span>${esc(t.status)}</span>
|
||||
</div>
|
||||
<div class="btn-row">
|
||||
<button class="primary" onclick="startTeam(${t.index})">▶ Start</button>
|
||||
<button class="danger" onclick="stopTeam(${t.index})">⏹ Stop</button>
|
||||
<button onclick="viewTeamCred(${t.index})">🔑 SSH & Pass</button>
|
||||
<button onclick="openTeamLogs(${t.index})">📜 Logs</button>
|
||||
<button onclick="randomizeTeam(${t.index})">🎲 Randomize Flag</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
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 = '<tr><td colspan="4" style="color:#718096">Belum ada solve.</td></tr>'; return; }
|
||||
body.innerHTML = d.teams.map((t,i) => `<tr>
|
||||
<td>${i+1}</td><td>${esc(t.name)}</td><td>${t.solves}</td>
|
||||
<td>${t.challs.map(c=>`<span class="chip">${esc(c)}</span>`).join('')}</td>
|
||||
</tr>`).join('');
|
||||
} catch (e) {}
|
||||
}
|
||||
setInterval(loadLeaderboard, 15000);
|
||||
|
||||
async function viewTeamCred(idx) {
|
||||
try {
|
||||
const t = await api(`/api/teams/${idx}/creds`);
|
||||
let html = `<div class="kv">
|
||||
<b>Label</b><span>${esc(t.team.label || ('Team ' + idx))}</span>
|
||||
<b>Admin receiver</b><span>${esc(t.team.admin_user)} / ${esc(t.team.admin_pass)}</span>
|
||||
<b>Receiver port</b><span>${esc(t.team.ports.receiver)}</span>
|
||||
<b>SSH user</b><span>ctfuser</span>
|
||||
<b>SSH pass</b><span>${esc(t.team.ssh_pass)}</span>
|
||||
</div><div style="margin-top:10px"><b style="color:#5ad1ff">Per-challenge SSH:</b></div>`;
|
||||
for (const [name, p] of Object.entries(t.team.ports)) {
|
||||
if (name === 'receiver' || name === 'panel') continue;
|
||||
html += `<div class="kv" style="margin-top:6px">
|
||||
<b>${name}</b><span>ssh ctfuser@${name}.gemastik.imrnes.team -p ${p.ssh} · pass: ${esc(t.team.chall_passwords[name])}</span>
|
||||
</div>`;
|
||||
}
|
||||
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 => `<option value="${t.index}">Team ${t.index}</option>`).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 = '<div class="card"><span style="color:#718096">Belum ada team.</span></div>'; return; }
|
||||
let html = '';
|
||||
for (const t of d.teams) {
|
||||
html += `<div class="card">
|
||||
<div class="team-head">
|
||||
<span class="ch-name">${esc(t.label || ('Team ' + t.index))}</span>
|
||||
<button style="font-size:11px" onclick="viewTeamCred(${t.index})">Lihat Semua</button>
|
||||
</div>
|
||||
<div class="kv">
|
||||
<b>Admin</b><span>${esc(t.admin_user)} / ${esc(t.admin_pass)}</span>
|
||||
<b>SSH</b><span>ctfuser / ${esc(t.ssh_pass)}</span>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
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');
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Gemastik A/D — Submit Flag</title>
|
||||
<style>
|
||||
:root { --bg:#0b0e14; --panel:#151a23; --panel2:#1d2430; --line:#2a3444; --txt:#e6edf3; --dim:#8b98a9; --accent:#38bdf8; --green:#34d399; --red:#f87171; }
|
||||
* { margin:0; padding:0; box-sizing:border-box; }
|
||||
body { background:var(--bg); color:var(--txt); font-family:'Segoe UI',system-ui,sans-serif; min-height:100vh; }
|
||||
.wrap { max-width:860px; margin:0 auto; padding:24px 16px 60px; }
|
||||
header { display:flex; align-items:center; gap:12px; margin-bottom:24px; }
|
||||
.logo { width:42px; height:42px; border-radius:10px; background:linear-gradient(135deg,#38bdf8,#6366f1); display:flex; align-items:center; justify-content:center; font-weight:800; font-size:20px; color:#fff; }
|
||||
h1 { font-size:22px; }
|
||||
.sub { color:var(--dim); font-size:13px; }
|
||||
.card { background:var(--panel); border:1px solid var(--line); border-radius:14px; padding:20px; margin-bottom:16px; }
|
||||
.card h2 { font-size:15px; margin-bottom:12px; color:var(--accent); }
|
||||
label { display:block; font-size:12px; color:var(--dim); margin:12px 0 5px; }
|
||||
select, input { width:100%; padding:10px 12px; border-radius:8px; border:1px solid var(--line); background:var(--panel2); color:var(--txt); font-size:14px; }
|
||||
button { margin-top:16px; width:100%; padding:12px; border:none; border-radius:8px; background:linear-gradient(135deg,#38bdf8,#6366f1); color:#fff; font-weight:600; font-size:15px; cursor:pointer; }
|
||||
button:hover { filter:brightness(1.1); }
|
||||
#result { margin-top:14px; padding:12px; border-radius:8px; display:none; font-size:14px; }
|
||||
#result.ok { display:block; background:#06352a; border:1px solid var(--green); color:var(--green); }
|
||||
#result.err { display:block; background:#3a1414; border:1px solid var(--red); color:var(--red); }
|
||||
.lb-card { background:var(--panel); border:1px solid var(--line); border-radius:14px; padding:20px; }
|
||||
.lb-row { display:flex; justify-content:space-between; align-items:center; padding:10px 4px; border-bottom:1px solid var(--line); }
|
||||
.lb-row:last-child { border-bottom:none; }
|
||||
.lb-rank { font-weight:700; color:var(--accent); width:36px; }
|
||||
.lb-name { flex:1; }
|
||||
.lb-count { font-weight:600; }
|
||||
table { width:100%; border-collapse:collapse; margin-top:8px; font-size:13px; }
|
||||
th, td { text-align:left; padding:8px 6px; border-bottom:1px solid var(--line); }
|
||||
th { color:var(--dim); font-weight:500; }
|
||||
.chips { display:flex; flex-wrap:wrap; gap:6px; margin-top:6px; }
|
||||
.chip { background:var(--panel2); border:1px solid var(--line); border-radius:20px; padding:3px 10px; font-size:11px; color:var(--dim); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<header>
|
||||
<div class="logo">G</div>
|
||||
<div>
|
||||
<h1>Gemastik XVIII — Attack & Defense</h1>
|
||||
<div class="sub">Submit flag untuk tim kamu. Server: gemastik.imrnes.team</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="card">
|
||||
<h2>🚩 Submit Flag</h2>
|
||||
<label>Tim kamu</label>
|
||||
<select id="team"><option value="">— pilih tim —</option></select>
|
||||
<label>Challenge</label>
|
||||
<select id="challenge">
|
||||
<option value="blogpost">blogpost (web)</option>
|
||||
<option value="carbeat">carbeat (pwn)</option>
|
||||
<option value="cdn">cdn (web)</option>
|
||||
<option value="phew">phew (crypto)</option>
|
||||
<option value="sheesh">sheesh (crypto)</option>
|
||||
<option value="warmup">warmup</option>
|
||||
</select>
|
||||
<label>Flag</label>
|
||||
<input id="flag" placeholder="GEMASTIK18{...}" autocomplete="off">
|
||||
<button onclick="submitFlag()">Submit Flag</button>
|
||||
<div id="result"></div>
|
||||
</div>
|
||||
|
||||
<div class="lb-card">
|
||||
<h2>🏆 Leaderboard</h2>
|
||||
<table>
|
||||
<thead><tr><th>#</th><th>Tim</th><th>Solved</th><th>Challenges</th></tr></thead>
|
||||
<tbody id="lb-body"><tr><td colspan="4" style="color:var(--dim)">Belum ada solve.</td></tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function api(url, opts) {
|
||||
const r = await fetch(url, opts);
|
||||
return r.json();
|
||||
}
|
||||
async function loadTeams() {
|
||||
const d = await api('/api/public/teams');
|
||||
const sel = document.getElementById('team');
|
||||
d.teams.forEach(t => {
|
||||
const o = document.createElement('option');
|
||||
o.value = t.index; o.textContent = t.label;
|
||||
sel.appendChild(o);
|
||||
});
|
||||
}
|
||||
async function loadLeaderboard() {
|
||||
const d = await api('/api/leaderboard');
|
||||
const body = document.getElementById('lb-body');
|
||||
const rows = d.teams.map((t, i) => `
|
||||
<tr>
|
||||
<td>${i + 1}</td>
|
||||
<td>${esc(t.name)}</td>
|
||||
<td>${t.solves}</td>
|
||||
<td><div class="chips">${t.challs.map(c => `<span class="chip">${esc(c)}</span>`).join('')}</div></td>
|
||||
</tr>`).join('');
|
||||
body.innerHTML = rows || '<tr><td colspan="4" style="color:var(--dim)">Belum ada solve.</td></tr>';
|
||||
}
|
||||
function esc(s) {
|
||||
return String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||||
}
|
||||
async function submitFlag() {
|
||||
const res = document.getElementById('result');
|
||||
res.className = '';
|
||||
const body = {
|
||||
team: parseInt(document.getElementById('team').value || '0', 10),
|
||||
challenge: document.getElementById('challenge').value,
|
||||
flag: document.getElementById('flag').value.trim(),
|
||||
};
|
||||
const d = await api('/api/flag/submit', {
|
||||
method: 'POST', headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (d.success) {
|
||||
res.className = 'ok';
|
||||
res.innerHTML = '✅ Flag <b>benar</b>! Solve tercatat untuk tim kamu.';
|
||||
loadLeaderboard();
|
||||
} else {
|
||||
res.className = 'err';
|
||||
res.innerHTML = '❌ ' + esc(d.error || 'Gagal submit');
|
||||
}
|
||||
}
|
||||
loadTeams();
|
||||
loadLeaderboard();
|
||||
setInterval(loadLeaderboard, 15000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+324
@@ -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<N>/
|
||||
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" -> "<team_chall>: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))
|
||||
@@ -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