fix(ssh): per-challenge SSH login + phew buffering/leak/timeout
Passwords failed on 10/16 challenges while state.json looked correct:
- only the 6 native GEMASTIK XVIII images provision 'ctfuser'; every imported
XVI/XVII image does 'echo root:${PASSWORD} | chpasswd' and logs in as root.
set_ssh_passwords() hardcoded ctfuser, so chpasswd set a password on an
account nobody uses -> 'Permission denied' everywhere.
Registry gains a per-challenge 'ssh_user'; chpasswd now targets the real
login (and ctfuser/ctf when present) and reports failures loudly.
- phew checker: chall.py block-buffers stdout through the docker exec pipe
(PYTHONUNBUFFERED now set) and leaks chall.py inside the container on
timeout (26 orphans, container saturated) -> reaps the whole exec process
group. Startup does a fresh Pailier keygen (~12 s) so crypto reads need
_CRYPTO_TIMEOUT, not the 5 s prompt default.
Adds panel/verify_ssh_creds.py (proves the state->container binding from
inside via a real login), audit_ssh_users.sh, reset_runtime.sh.
This commit is contained in:
@@ -318,6 +318,12 @@ function esc(s) {
|
||||
return String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||||
}
|
||||
|
||||
// Alias used by the Challenge Manager render. Kept as a separate name so
|
||||
// existing esc() call sites stay untouched, but it MUST exist: a missing
|
||||
// helper throws ReferenceError mid-render and the tab hangs on "Memuat…"
|
||||
// forever with no visible error.
|
||||
function escapeHtml(s) { return esc(s); }
|
||||
|
||||
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));
|
||||
@@ -714,11 +720,15 @@ function topoZoom(factor) {
|
||||
function topoReset() { topoScale = 1; topoPanX = 0; topoPanY = 0; applyTopoView(); }
|
||||
|
||||
// ---------- Teams ----------
|
||||
let TEAM_LABELS = {}; // idx -> label, filled by loadTeams (for confirm dialogs)
|
||||
|
||||
async function loadTeams() {
|
||||
try {
|
||||
const d = await api('/api/teams');
|
||||
document.getElementById('teamCount').value = d.teams.length;
|
||||
loadLeaderboard();
|
||||
TEAM_LABELS = {};
|
||||
for (const t of d.teams) TEAM_LABELS[t.index] = t.label || ('Team ' + t.index);
|
||||
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 = '';
|
||||
@@ -743,6 +753,7 @@ async function loadTeams() {
|
||||
<button onclick="editTeam(${t.index})">✏️ Edit Nama/Domain</button>
|
||||
<button onclick="openTeamLogs(${t.index})">📜 Logs</button>
|
||||
<button onclick="randomizeTeam(${t.index})">🎲 Randomize Flag</button>
|
||||
<button class="danger" onclick="deleteTeam(${t.index})">🗑️ Hapus Team</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
@@ -859,6 +870,38 @@ async function randomizeTeam(idx) {
|
||||
} catch (e) { toast(e.message, true); }
|
||||
}
|
||||
|
||||
async function deleteTeam(idx) {
|
||||
// Per-team delete. Deliberately NOT the same as resetEnv: this removes ONE
|
||||
// team (containers, network, receiver unit, ports, dir, domain) and leaves
|
||||
// the other teams untouched.
|
||||
const label = TEAM_LABELS[idx] || ('Team ' + idx);
|
||||
if (!confirm(
|
||||
`🗑️ HAPUS PERMANEN Team ${idx} (${label})?\n\n` +
|
||||
`Yang akan dihapus:\n` +
|
||||
`• Semua container challenge team ini\n` +
|
||||
`• Receiver team + systemd unit\n` +
|
||||
`• Folder team (port, flag, kredensial)\n` +
|
||||
`• Port firewall UFW team ini\n` +
|
||||
`• Domain ${label}.attackdefense.imrnes.team\n` +
|
||||
`• Skor & riwayat di leaderboard\n\n` +
|
||||
`Tindakan ini TIDAK BISA dibatalkan.\n` +
|
||||
`Team lain tidak terpengaruh.`)) return;
|
||||
// second gate: type the team number to confirm
|
||||
if (prompt(`Ketik angka ${idx} untuk konfirmasi hapus:`)?.trim() !== String(idx)) {
|
||||
toast('Dibatalkan', false);
|
||||
return;
|
||||
}
|
||||
showLoading(`Menghapus Team ${idx} (${label})…<br>Stop container + receiver, hapus port & domain`);
|
||||
try {
|
||||
const d = await api(`/api/teams/${idx}`, {method:'DELETE', headers:{'Content-Type':'application/json'}, body: JSON.stringify({purge_scores: true})});
|
||||
const n = (d.purged ? Object.values(d.purged).reduce((a, b) => a + b, 0) : 0);
|
||||
toast(`Team ${idx} (${label}) dihapus: ${(d.steps || []).join(' · ')}${n ? ` · ${n} skor dibersihkan` : ''}`, false);
|
||||
loadTeams();
|
||||
if (document.getElementById('view-topo').classList.contains('active')) loadTopo();
|
||||
} catch (e) { toast('Hapus team gagal: ' + e.message, true); }
|
||||
finally { hideLoading(); }
|
||||
}
|
||||
|
||||
async function loadLeaderboard() {
|
||||
try {
|
||||
const d = await api('/api/leaderboard');
|
||||
|
||||
Reference in New Issue
Block a user