The web SSH terminal and the credential API reported `ctfuser` for all 16
challenges, but only the 6 native GEMASTIK XVIII images provision ctfuser.
Every imported XVI/XVII image does `RUN echo root:${PASSWORD} | chpasswd`,
so 10 of 16 participant logins were refused with "Permission denied".
Root causes (all the same class of bug - login hardcoded in the wrong layer):
- main.py websocket ssh handler read st["ssh_user"], a single team-wide value
defaulting to ctfuser, instead of the per-challenge registry field
- /api/credential proxied the global receiver on :18080, which only knows the
6 native challenges, so the other 10 returned "Invalid challenge"
- team.html hardcoded the challenge picker to those same 6 challenges, making
the other 10 unreachable from the terminal entirely
- index.html rendered `<b>ctfuser</b>` and a stale hardcoded SSH port table
Fixes:
- orch.challenge_credential()/all_teams() read the TEAM's state.json, which
holds the same per-challenge password the panel chpasswds
- gen_receiver_services.py injects SSH_USER_<port> from the registry so the
receiver's /credential endpoint agrees with the panel
- receiver Challenge.credentials() honours SSH_USER_<port> (ctfuser fallback)
- new /api/team/{idx}/own-challenges feeds the picker; targets now carry
challenge + ssh_user
- UI takes user and port from the server instead of hardcoding them
Verified: 32/32 credential payloads correct across teams 1-2, and 32/32 real
paramiko SSH logins succeed with whoami confirming the expected account.
Also adds bulk team delete: POST /api/teams/bulk-delete runs one background
thread and is polled via GET /api/teams/bulk-delete/{job_id}, plus per-team
checkboxes with select-all/clear in the UI. Deletion must stay sequential
because delete_team() regenerates shared artifacts at the end.
26 lines
1014 B
JavaScript
26 lines
1014 B
JavaScript
// Syntax-check every inline <script> block across all panel static pages.
|
|
const fs = require('fs');
|
|
const { execFileSync } = require('child_process');
|
|
const files = ['static/index.html', 'static/team.html', 'static/guide.html'];
|
|
const base = '/opt/gemastik18-final/panel/';
|
|
let bad = 0, total = 0;
|
|
for (const f of files) {
|
|
const html = fs.readFileSync(base + f, 'utf8');
|
|
const re = /<script(?![^>]*\bsrc=)[^>]*>([\s\S]*?)<\/script>/gi;
|
|
let m, i = 0;
|
|
while ((m = re.exec(html)) !== null) {
|
|
i++; total++;
|
|
const p = `/tmp/chk_${f.replace(/\W/g, '_')}_${i}.js`;
|
|
fs.writeFileSync(p, m[1]);
|
|
try {
|
|
execFileSync(process.execPath, ['--check', p], { stdio: 'pipe' });
|
|
console.log(` OK ${f} block#${i}`);
|
|
} catch (e) {
|
|
bad++;
|
|
console.log(` FAIL ${f} block#${i}\n${e.stderr.toString().split('\n').slice(0, 5).join('\n')}`);
|
|
}
|
|
}
|
|
}
|
|
console.log(bad === 0 ? `\nAll ${total} script block(s) parse cleanly.` : `\n${bad}/${total} FAILED.`);
|
|
process.exit(bad ? 1 : 0);
|