- Rename repo/domain: attack-defense-platform / attackdefense.imrnes.team (all refs replaced)
- challenge_registry.json: single source of truth (28 challs across gemastik18/xvi/xvii)
- teams.py: registry-driven CHALLENGES, set_challenge_enabled, sync_challenge_runtime
(apply enable/disable to live teams: build/up or stop/remove + receiver restart)
- compose_gen.py: render per-team compose from canonical per-challenge templates
(image reuse, per-team ports 30xxx, flag mounts, passwords)
- gen_canonical_composes.py: canonical docker-compose.yml for all services
- import_new_challenges.py: import XVI/XVII services + EOL base image fixes
(debian:buster→bookworm, node:14→20, python:3.7-slim→3.11)
- receiver: xvi package (10 checkers) + xvii package (12 generic checkers),
Challenge base reads PASSWORD_<team_port> from env; gen_receiver_main.py
generates per-team main.py from registry
- main.py: /api/challenges returns full registry; PATCH /api/challenges/<name>
toggles enabled + applies to live teams
- index.html: 🏗️ Challenge Manager tab (toggle per challenge, grouped by set)
- SLA bonus now dynamic (all enabled challenges, not hardcoded 6)
131 lines
5.7 KiB
HTML
131 lines
5.7 KiB
HTML
<!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: attackdefense.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> |