draggable topology + attack visualizer + tools in containers
- topology nodes draggable (pointer events, SVG transform), layout hint shown - attack visualizer: /api/attacks logs attacker->target events; red pulsing dashed arcs on recent attacks (60s hot), ⚔ counts ok/fail - submit_flag now takes attacker_idx vs target_idx (A/D semantics); UI has target dropdown (enemy teams), leaderboard records target - containers get vim+curl+wget+netcat+git+pip3 (Dockerfiles blogpost/cdn/ phew/sheesh/warmup); warmup base ubuntu:20.04 EOL -> 24.04 - team portal: target dropdown refreshed after login (was empty pre-auth)
This commit is contained in:
+18
-3
@@ -223,6 +223,8 @@ async def api_team_targets(idx: int, req: Request):
|
|||||||
if not p:
|
if not p:
|
||||||
continue
|
continue
|
||||||
out.append({
|
out.append({
|
||||||
|
"team_idx": st.get("index"),
|
||||||
|
"team_label": st.get("label", f"Team {st.get('index')}"),
|
||||||
"domain": st.get("domain") or (st.get("slug", f"team{st.get('index')}") + ".gemastik.imrnes.team"),
|
"domain": st.get("domain") or (st.get("slug", f"team{st.get('index')}") + ".gemastik.imrnes.team"),
|
||||||
"port": p["chall"],
|
"port": p["chall"],
|
||||||
})
|
})
|
||||||
@@ -521,9 +523,11 @@ async def api_reset_environment(req: Request):
|
|||||||
|
|
||||||
@app.post("/api/flag/submit")
|
@app.post("/api/flag/submit")
|
||||||
async def api_flag_submit(req: Request):
|
async def api_flag_submit(req: Request):
|
||||||
"""Public endpoint: teams submit flags. No login needed."""
|
"""Public endpoint: teams submit flags. No login needed.
|
||||||
|
body: {team: attacker, target?: victim, challenge, flag}"""
|
||||||
data = await req.json()
|
data = await req.json()
|
||||||
team = int(data.get("team", 0))
|
team = int(data.get("team", 0)) # attacker (tim yang submit)
|
||||||
|
target = int(data.get("target") or 0) or team # victim team (flag dicuri dari sini)
|
||||||
chall = data.get("challenge", "")
|
chall = data.get("challenge", "")
|
||||||
flag = data.get("flag", "").strip()
|
flag = data.get("flag", "").strip()
|
||||||
team_name = data.get("team_name", "").strip()[:64] or f"Team {team}"
|
team_name = data.get("team_name", "").strip()[:64] or f"Team {team}"
|
||||||
@@ -533,7 +537,18 @@ async def api_flag_submit(req: Request):
|
|||||||
return {"success": False, "error": "Challenge not found"}
|
return {"success": False, "error": "Challenge not found"}
|
||||||
if not flag:
|
if not flag:
|
||||||
return {"success": False, "error": "Flag is required"}
|
return {"success": False, "error": "Flag is required"}
|
||||||
return orch.submit_flag(team, chall, flag, team_name)
|
return orch.submit_flag(target, chall, flag, attacker_idx=team, attacker_name=team_name)
|
||||||
|
|
||||||
|
@app.get("/api/attacks")
|
||||||
|
async def api_attacks(req: Request):
|
||||||
|
"""Admin: recent attack events (attacker -> target) for the topology visualizer."""
|
||||||
|
require_login(req)
|
||||||
|
ap = orch.TEAMS_DIR / "attacks.json"
|
||||||
|
if ap.exists():
|
||||||
|
log = json.loads(ap.read_text())
|
||||||
|
else:
|
||||||
|
log = {"events": []}
|
||||||
|
return {"events": log.get("events", [])}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/leaderboard")
|
@app.get("/api/leaderboard")
|
||||||
|
|||||||
+85
-8
@@ -101,6 +101,12 @@
|
|||||||
@keyframes sp { to { transform:rotate(360deg); } }
|
@keyframes sp { to { transform:rotate(360deg); } }
|
||||||
.topo-wrap { background:#0a0f1c; border:1px solid #1e3a5f; border-radius:12px; padding:20px; overflow-x:auto; }
|
.topo-wrap { background:#0a0f1c; border:1px solid #1e3a5f; border-radius:12px; padding:20px; overflow-x:auto; }
|
||||||
.topo-svg { width:100%; min-width:800px; }
|
.topo-svg { width:100%; min-width:800px; }
|
||||||
|
/* attack visualizer: recent attacks pulse */
|
||||||
|
@keyframes attackPulse { 0%,100% { stroke-opacity:0.4; } 50% { stroke-opacity:1; } }
|
||||||
|
@keyframes attackBlink { 0%,100% { opacity:1; } 50% { opacity:0.35; } }
|
||||||
|
.attack-line { animation: attackPulse 1.2s ease-in-out infinite; }
|
||||||
|
.attack-icon { animation: attackBlink 1s ease-in-out infinite; }
|
||||||
|
.topo-hint { font-size:12px; color:var(--dim); margin-top:8px; }
|
||||||
.team-head { display:flex; justify-content:space-between; align-items:center; gap:10px; flex-wrap:wrap; }
|
.team-head { display:flex; justify-content:space-between; align-items:center; gap:10px; flex-wrap:wrap; }
|
||||||
.team-status { font-size:11px; }
|
.team-status { font-size:11px; }
|
||||||
.kv { display:grid; grid-template-columns:120px 1fr; gap:6px 12px; font-size:12px; }
|
.kv { display:grid; grid-template-columns:120px 1fr; gap:6px 12px; font-size:12px; }
|
||||||
@@ -137,7 +143,8 @@
|
|||||||
|
|
||||||
<!-- Topology view -->
|
<!-- Topology view -->
|
||||||
<div class="view" id="view-topo">
|
<div class="view" id="view-topo">
|
||||||
<div class="topo-wrap"><svg id="topoSvg" class="topo-svg" height="420"></svg></div>
|
<div class="topo-wrap"><svg id="topoSvg" class="topo-svg" height="520"></svg></div>
|
||||||
|
<div class="topo-hint">💡 Geser node untuk mengatur layout • ⚔️ garis merah = serangan (panah attacker → target) • garis <b>putus-putus</b> = serangan baru (60 detik terakhir)</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Teams view -->
|
<!-- Teams view -->
|
||||||
@@ -283,7 +290,15 @@ function esc(s) {
|
|||||||
function showView(v) {
|
function showView(v) {
|
||||||
document.querySelectorAll('.tab').forEach(b => b.classList.toggle('active', b.dataset.view === 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));
|
document.querySelectorAll('.view').forEach(x => x.classList.toggle('active', x.id === 'view-' + v));
|
||||||
|
if (v === 'topo') loadTopo(); // refresh attacks immediately on tab switch + every 10s
|
||||||
}
|
}
|
||||||
|
let topoTimer = null;
|
||||||
|
function startTopoTimer() {
|
||||||
|
if (!topoTimer) topoTimer = setInterval(() => {
|
||||||
|
if (document.getElementById('view-topo').classList.contains('active')) loadTopo();
|
||||||
|
}, 10000);
|
||||||
|
}
|
||||||
|
startTopoTimer();
|
||||||
|
|
||||||
// ---------- Challenges ----------
|
// ---------- Challenges ----------
|
||||||
async function refresh() {
|
async function refresh() {
|
||||||
@@ -371,9 +386,11 @@ async function viewCred(ch) {
|
|||||||
|
|
||||||
// ---------- Topology ----------
|
// ---------- Topology ----------
|
||||||
let topoLoaded = false;
|
let topoLoaded = false;
|
||||||
|
let topoAttacks = [];
|
||||||
async function loadTopo() {
|
async function loadTopo() {
|
||||||
try {
|
try {
|
||||||
const d = await api('/api/topology');
|
const [d, a] = await Promise.all([api('/api/topology'), api('/api/attacks').catch(() => ({events: []}))]);
|
||||||
|
topoAttacks = a.events || [];
|
||||||
renderTopo(d.nodes, d.edges);
|
renderTopo(d.nodes, d.edges);
|
||||||
} catch (e) { toast('Topologi gagal: ' + e.message, true); }
|
} catch (e) { toast('Topologi gagal: ' + e.message, true); }
|
||||||
}
|
}
|
||||||
@@ -381,7 +398,7 @@ async function loadTopo() {
|
|||||||
function renderTopo(nodes, edges) {
|
function renderTopo(nodes, edges) {
|
||||||
const svg = document.getElementById('topoSvg');
|
const svg = document.getElementById('topoSvg');
|
||||||
const W = Math.max(900, nodes.length * 130);
|
const W = Math.max(900, nodes.length * 130);
|
||||||
const H = 400;
|
const H = 500;
|
||||||
svg.setAttribute('width', W); svg.setAttribute('height', H);
|
svg.setAttribute('width', W); svg.setAttribute('height', H);
|
||||||
const cx = W / 2;
|
const cx = W / 2;
|
||||||
const ns = {};
|
const ns = {};
|
||||||
@@ -398,15 +415,26 @@ function renderTopo(nodes, edges) {
|
|||||||
const ang = (i / Math.max(1, teams.length)) * Math.PI * 2 - Math.PI / 2;
|
const ang = (i / Math.max(1, teams.length)) * Math.PI * 2 - Math.PI / 2;
|
||||||
const rx = W * 0.36, ry = 100;
|
const rx = W * 0.36, ry = 100;
|
||||||
const tx = cx + rx * Math.cos(ang);
|
const tx = cx + rx * Math.cos(ang);
|
||||||
const ty = 210 + ry * Math.sin(ang) * 0.6;
|
const ty = 250 + ry * Math.sin(ang) * 0.6;
|
||||||
teamPos[t.index] = {x: tx, y: ty};
|
teamPos[t.index] = {x: tx, y: ty};
|
||||||
pos[t.id] = {x: tx, y: ty};
|
pos[t.id] = {x: tx, y: ty};
|
||||||
});
|
});
|
||||||
nodes.filter(n => n.type === 'challenge').forEach(n => {
|
nodes.filter(n => n.type === 'challenge').forEach(n => {
|
||||||
const ti = n.id.split('-')[0].replace('team','');
|
const ti = n.id.split('-')[0].replace('team','');
|
||||||
const base = teamPos[ti] || {x: cx, y: 250};
|
const base = teamPos[ti] || {x: cx, y: 300};
|
||||||
const chIdx = ['blogpost','carbeat','cdn','phew','sheesh','warmup'].indexOf(n.label.split('-').pop() || n.label);
|
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};
|
pos[n.id] = {x: base.x + (chIdx - 2.5) * 70, y: base.y + 130};
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---- attack arcs (attacker team -> target team), recent only (last 10 min) ----
|
||||||
|
const now = Date.now() / 1000;
|
||||||
|
const recentAttacks = topoAttacks.filter(e => now - (e.ts || 0) < 600);
|
||||||
|
const attackCounts = {}; // "attacker:target" -> {ok, fail, latest}
|
||||||
|
recentAttacks.forEach(e => {
|
||||||
|
const k = `${e.attacker}:${e.target}`;
|
||||||
|
attackCounts[k] = attackCounts[k] || {ok: 0, fail: 0, latest: e.ts};
|
||||||
|
attackCounts[k][e.success ? 'ok' : 'fail']++;
|
||||||
|
attackCounts[k].latest = Math.max(attackCounts[k].latest, e.ts || 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
// edges
|
// edges
|
||||||
@@ -417,10 +445,24 @@ function renderTopo(nodes, edges) {
|
|||||||
html += `<line x1="${a.x}" y1="${a.y}" x2="${b.x}" y2="${b.y}" stroke="#2a4a6f" stroke-width="1.5" stroke-dasharray="4 3"/>`;
|
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) {
|
if (e.label) {
|
||||||
const mx = (a.x + b.x) / 2, my = (a.y + b.y) / 2 - 6;
|
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>`;
|
html += `<text x="${mx}" y="${my}" fill="#5a7a9f" font-size="9" text-anchor="middle">${esc(e.label)}</text>`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---- attack visualizer ----
|
||||||
|
for (const [k, c] of Object.entries(attackCounts)) {
|
||||||
|
const [atk, tgt] = k.split(':');
|
||||||
|
const a = pos['team' + atk], b = pos['team' + tgt];
|
||||||
|
if (!a || !b || atk === tgt) continue;
|
||||||
|
const isHot = now - c.latest < 60;
|
||||||
|
const color = c.ok > 0 ? '#f87171' : '#fb923c';
|
||||||
|
const dash = isHot ? '6 3' : '4 4';
|
||||||
|
html += `<line x1="${a.x}" y1="${a.y}" x2="${b.x}" y2="${b.y}" stroke="${color}" stroke-width="${isHot ? 3 : 2}" stroke-dasharray="${dash}" opacity="0.9" class="attack-line"/>`;
|
||||||
|
const mx = (a.x + b.x) / 2 + 8, my = (a.y + b.y) / 2 - 10;
|
||||||
|
const icon = isHot ? '⚔️' : '⚔';
|
||||||
|
html += `<text x="${mx}" y="${my}" fill="${color}" font-size="12" text-anchor="middle" class="attack-icon">${icon} ${c.ok}✓ ${c.fail}✗</text>`;
|
||||||
|
}
|
||||||
|
|
||||||
// nodes
|
// nodes
|
||||||
nodes.forEach(n => {
|
nodes.forEach(n => {
|
||||||
const p = pos[n.id];
|
const p = pos[n.id];
|
||||||
@@ -432,7 +474,7 @@ function renderTopo(nodes, edges) {
|
|||||||
if (n.type === 'challenge') { r = 26; color = '#c9d4e3'; }
|
if (n.type === 'challenge') { r = 26; color = '#c9d4e3'; }
|
||||||
const status = n.status === 'running' ? ' fill="#34d399"' : '';
|
const status = n.status === 'running' ? ' fill="#34d399"' : '';
|
||||||
const sub = n.type === 'team' ? `:${n.receiver_port}` : (n.port ? `:${n.port}` : '');
|
const sub = n.type === 'team' ? `:${n.receiver_port}` : (n.port ? `:${n.port}` : '');
|
||||||
html += `<g>
|
html += `<g data-node="${esc(n.id)}" style="cursor:grab">
|
||||||
<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}" 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"/>
|
<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>
|
<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>
|
||||||
@@ -441,6 +483,41 @@ function renderTopo(nodes, edges) {
|
|||||||
</g>`;
|
</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;
|
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;
|
||||||
|
enableTopoDrag(svg, pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- draggable topology ----
|
||||||
|
function enableTopoDrag(svg, pos) {
|
||||||
|
let dragEl = null, dx = 0, dy = 0;
|
||||||
|
const onMove = (ev) => {
|
||||||
|
if (!dragEl) return;
|
||||||
|
const ctm = svg.getScreenCTM();
|
||||||
|
if (!ctm) return;
|
||||||
|
const pt = svg.createSVGPoint();
|
||||||
|
pt.x = ev.clientX; pt.y = ev.clientY;
|
||||||
|
const p = pt.matrixTransform(ctm.inverse());
|
||||||
|
dragEl.setAttribute('transform', `translate(${p.x - dx}, ${p.y - dy})`);
|
||||||
|
};
|
||||||
|
const onUp = () => { dragEl = null; svg.style.cursor = ''; };
|
||||||
|
svg.addEventListener('mousedown', (ev) => {
|
||||||
|
const g = ev.target.closest('g[data-node]');
|
||||||
|
if (!g) return;
|
||||||
|
ev.preventDefault();
|
||||||
|
const ctm = svg.getScreenCTM();
|
||||||
|
const pt = svg.createSVGPoint();
|
||||||
|
pt.x = ev.clientX; pt.y = ev.clientY;
|
||||||
|
const p = pt.matrixTransform(ctm.inverse());
|
||||||
|
const c = g.querySelector('circle');
|
||||||
|
const cx = parseFloat(c.getAttribute('cx'));
|
||||||
|
const cy = parseFloat(c.getAttribute('cy'));
|
||||||
|
dx = p.x - cx; dy = p.y - cy;
|
||||||
|
dragEl = g;
|
||||||
|
svg.style.cursor = 'grabbing';
|
||||||
|
g.setPointerCapture && g.setPointerCapture(ev.pointerId);
|
||||||
|
});
|
||||||
|
svg.addEventListener('pointermove', onMove);
|
||||||
|
svg.addEventListener('pointerup', onUp);
|
||||||
|
svg.addEventListener('pointercancel', onUp);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- Teams ----------
|
// ---------- Teams ----------
|
||||||
|
|||||||
+30
-1
@@ -138,7 +138,11 @@
|
|||||||
<option value="sheesh">sheesh (crypto)</option>
|
<option value="sheesh">sheesh (crypto)</option>
|
||||||
<option value="warmup">warmup</option>
|
<option value="warmup">warmup</option>
|
||||||
</select>
|
</select>
|
||||||
<label>Flag</label>
|
<label>Target (tim musuh yang diserang)</label>
|
||||||
|
<select id="target">
|
||||||
|
<option value="">Memuat target…</option>
|
||||||
|
</select>
|
||||||
|
<label>Flag (yang kamu curi dari target)</label>
|
||||||
<input type="text" id="flag" placeholder="GEMASTIK18{...}" autocomplete="off">
|
<input type="text" id="flag" placeholder="GEMASTIK18{...}" autocomplete="off">
|
||||||
<button onclick="submitFlag()" style="width:100%;margin-top:16px">Submit Flag</button>
|
<button onclick="submitFlag()" style="width:100%;margin-top:16px">Submit Flag</button>
|
||||||
<div id="result"></div>
|
<div id="result"></div>
|
||||||
@@ -229,6 +233,8 @@ async function doLogin() {
|
|||||||
document.getElementById('portalScreen').style.display = 'block';
|
document.getElementById('portalScreen').style.display = 'block';
|
||||||
document.getElementById('logoutBtn').style.display = '';
|
document.getElementById('logoutBtn').style.display = '';
|
||||||
loadInfo();
|
loadInfo();
|
||||||
|
loadTargetDropdown(); // target dropdown needs auth — refresh after login
|
||||||
|
loadTargets();
|
||||||
} else {
|
} else {
|
||||||
err.textContent = 'Password salah. Coba lagi.';
|
err.textContent = 'Password salah. Coba lagi.';
|
||||||
err.style.display = 'block';
|
err.style.display = 'block';
|
||||||
@@ -305,6 +311,7 @@ async function submitFlag() {
|
|||||||
method: 'POST', headers: {'Content-Type': 'application/json'},
|
method: 'POST', headers: {'Content-Type': 'application/json'},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
team: TEAM_ID,
|
team: TEAM_ID,
|
||||||
|
target: parseInt(document.getElementById('target').value || '0', 10) || TEAM_ID,
|
||||||
challenge: document.getElementById('challenge').value,
|
challenge: document.getElementById('challenge').value,
|
||||||
flag: document.getElementById('flag').value.trim(),
|
flag: document.getElementById('flag').value.trim(),
|
||||||
}),
|
}),
|
||||||
@@ -313,6 +320,28 @@ async function submitFlag() {
|
|||||||
else { res.className = 'err'; res.textContent = '❌ ' + (d.error || 'Gagal'); }
|
else { res.className = 'err'; res.textContent = '❌ ' + (d.error || 'Gagal'); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// target dropdown: all enemy teams (exclude own)
|
||||||
|
async function loadTargetDropdown() {
|
||||||
|
try {
|
||||||
|
const d = await api(`/api/team/${TEAM_ID}/targets`);
|
||||||
|
const sel = document.getElementById('target');
|
||||||
|
const targets = (d.targets || []).filter(t => t.team_idx !== TEAM_ID);
|
||||||
|
const seen = new Map();
|
||||||
|
for (const t of targets) seen.set(t.team_idx, t.team_label || ('Team ' + t.team_idx));
|
||||||
|
sel.innerHTML = '';
|
||||||
|
if (!seen.size) {
|
||||||
|
sel.innerHTML = '<option value="">Tidak ada tim musuh</option>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const [idx, label] of seen) {
|
||||||
|
const o = document.createElement('option');
|
||||||
|
o.value = idx; o.textContent = `${label} (tim ${idx})`;
|
||||||
|
sel.appendChild(o);
|
||||||
|
}
|
||||||
|
} catch (e) { /* non-fatal */ }
|
||||||
|
}
|
||||||
|
loadTargetDropdown();
|
||||||
|
|
||||||
checkSession();
|
checkSession();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
+39
-9
@@ -374,9 +374,15 @@ def randomize_flags(idx: int) -> dict:
|
|||||||
return mapping
|
return mapping
|
||||||
|
|
||||||
|
|
||||||
def submit_flag(team_idx: int, chall: str, flag: str, team_name: str = "") -> dict:
|
def submit_flag(target_idx: int, chall: str, flag: str,
|
||||||
"""Validate a submitted flag against the owning team's challenge flag."""
|
attacker_idx: int = None, attacker_name: str = "") -> dict:
|
||||||
team_dir = TEAMS_DIR / f"team{team_idx}"
|
"""Validate a submitted flag against the TARGET team's challenge flag.
|
||||||
|
|
||||||
|
A/D semantics: attacker_idx scores by stealing target_idx's flag.
|
||||||
|
Back-compat: attacker_idx defaults to target_idx (self-submit).
|
||||||
|
"""
|
||||||
|
attacker_idx = attacker_idx if attacker_idx else target_idx
|
||||||
|
team_dir = TEAMS_DIR / f"team{target_idx}"
|
||||||
if not (team_dir / "state.json").exists():
|
if not (team_dir / "state.json").exists():
|
||||||
return {"success": False, "error": "unknown team"}
|
return {"success": False, "error": "unknown team"}
|
||||||
flags_dir = team_dir / "receiver" / "flags"
|
flags_dir = team_dir / "receiver" / "flags"
|
||||||
@@ -384,23 +390,47 @@ def submit_flag(team_idx: int, chall: str, flag: str, team_name: str = "") -> di
|
|||||||
if not expected:
|
if not expected:
|
||||||
return {"success": False, "error": "challenge not found"}
|
return {"success": False, "error": "challenge not found"}
|
||||||
if flag.strip() != expected:
|
if flag.strip() != expected:
|
||||||
|
_log_attack(attacker_idx, target_idx, chall, flag[:24], success=False)
|
||||||
return {"success": False, "error": "wrong flag"}
|
return {"success": False, "error": "wrong flag"}
|
||||||
# record leaderboard entry
|
_log_attack(attacker_idx, target_idx, chall, flag, success=True)
|
||||||
|
# record leaderboard entry — score goes to the ATTACKER
|
||||||
lb_path = TEAMS_DIR / "leaderboard.json"
|
lb_path = TEAMS_DIR / "leaderboard.json"
|
||||||
lb = json.loads(lb_path.read_text()) if lb_path.exists() else {"solves": []}
|
lb = json.loads(lb_path.read_text()) if lb_path.exists() else {"solves": []}
|
||||||
entry = {
|
entry = {
|
||||||
"team": team_idx,
|
"team": attacker_idx,
|
||||||
"team_name": team_name or f"Team {team_idx}",
|
"team_name": attacker_name or f"Team {attacker_idx}",
|
||||||
"challenge": chall,
|
"challenge": chall,
|
||||||
|
"target": target_idx,
|
||||||
"flag": flag,
|
"flag": flag,
|
||||||
"ts": time.time(),
|
"ts": time.time(),
|
||||||
"ts_human": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
"ts_human": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
}
|
}
|
||||||
# avoid double-solve duplicates (same flag + same team)
|
# avoid double-solve duplicates (same flag + same attacker team)
|
||||||
if not any(e["team"] == team_idx and e["challenge"] == chall for e in lb["solves"]):
|
if not any(e["team"] == attacker_idx and e["challenge"] == chall
|
||||||
|
and e.get("target") == target_idx for e in lb["solves"]):
|
||||||
lb["solves"].append(entry)
|
lb["solves"].append(entry)
|
||||||
lb_path.write_text(json.dumps(lb, indent=2))
|
lb_path.write_text(json.dumps(lb, indent=2))
|
||||||
return {"success": True, "team": team_idx, "challenge": chall}
|
return {"success": True, "team": attacker_idx, "target": target_idx, "challenge": chall}
|
||||||
|
|
||||||
|
def _log_attack(attacker_team: int, target_team: int, target_chall: str, flag_hint: str, success: bool):
|
||||||
|
"""Append a row to the attack log (used by the topology attack visualizer)."""
|
||||||
|
try:
|
||||||
|
ap = TEAMS_DIR / "attacks.json"
|
||||||
|
log = json.loads(ap.read_text()) if ap.exists() else {"events": []}
|
||||||
|
log["events"].append({
|
||||||
|
"attacker": attacker_team,
|
||||||
|
"target": target_team,
|
||||||
|
"challenge": target_chall,
|
||||||
|
"flag_hint": flag_hint,
|
||||||
|
"success": success,
|
||||||
|
"ts": time.time(),
|
||||||
|
"ts_human": datetime.now().strftime("%H:%M:%S"),
|
||||||
|
})
|
||||||
|
# keep last 200
|
||||||
|
log["events"] = log["events"][-200:]
|
||||||
|
ap.write_text(json.dumps(log, indent=2))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def reset_scores() -> dict:
|
def reset_scores() -> dict:
|
||||||
"""Wipe the leaderboard (all solves removed)."""
|
"""Wipe the leaderboard (all solves removed)."""
|
||||||
|
|||||||
@@ -21,6 +21,12 @@ RUN apt-get update && \
|
|||||||
build-essential \
|
build-essential \
|
||||||
bash \
|
bash \
|
||||||
nano \
|
nano \
|
||||||
|
vim \
|
||||||
|
curl \
|
||||||
|
wget \
|
||||||
|
netcat-openbsd \
|
||||||
|
git \
|
||||||
|
python3-pip \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Create an unprivileged user for running the app / SSH access
|
# Create an unprivileged user for running the app / SSH access
|
||||||
|
|||||||
@@ -22,6 +22,12 @@ RUN apt-get update && \
|
|||||||
build-essential \
|
build-essential \
|
||||||
bash \
|
bash \
|
||||||
nano \
|
nano \
|
||||||
|
vim \
|
||||||
|
curl \
|
||||||
|
wget \
|
||||||
|
netcat-openbsd \
|
||||||
|
git \
|
||||||
|
python3-pip \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Create an unprivileged user for running the app / SSH access
|
# Create an unprivileged user for running the app / SSH access
|
||||||
|
|||||||
@@ -12,6 +12,11 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
libssl-dev \
|
libssl-dev \
|
||||||
python3-dev \
|
python3-dev \
|
||||||
socat \
|
socat \
|
||||||
|
vim \
|
||||||
|
curl \
|
||||||
|
wget \
|
||||||
|
netcat-openbsd \
|
||||||
|
git \
|
||||||
bash \
|
bash \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,11 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
libssl-dev \
|
libssl-dev \
|
||||||
python3-dev \
|
python3-dev \
|
||||||
socat \
|
socat \
|
||||||
|
vim \
|
||||||
|
curl \
|
||||||
|
wget \
|
||||||
|
netcat-openbsd \
|
||||||
|
git \
|
||||||
bash \
|
bash \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
|||||||
@@ -6,14 +6,14 @@ COPY src/main.go .
|
|||||||
RUN go build -o challenge main.go
|
RUN go build -o challenge main.go
|
||||||
|
|
||||||
# Final stage
|
# Final stage
|
||||||
FROM public.ecr.aws/docker/library/ubuntu:20.04
|
FROM public.ecr.aws/docker/library/ubuntu:24.04
|
||||||
|
|
||||||
ARG PASSWORD
|
ARG PASSWORD
|
||||||
|
|
||||||
ENV DEBIAN_FRONTEND=noninteractive
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
# Install necessary packages
|
# Install necessary packages
|
||||||
RUN apt-get update && apt-get install -y nano openssh-server python3 curl netcat-traditional wget sudo nginx golang-go && rm -rf /var/lib/apt/lists/*
|
RUN apt-get update && apt-get install -y nano vim git openssh-server python3 curl netcat-traditional wget sudo nginx golang-go && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Create ctfuser and set password
|
# Create ctfuser and set password
|
||||||
RUN useradd -m -d /home/ctfuser ctfuser && echo ctfuser:${PASSWORD} | chpasswd
|
RUN useradd -m -d /home/ctfuser ctfuser && echo ctfuser:${PASSWORD} | chpasswd
|
||||||
|
|||||||
Reference in New Issue
Block a user