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:
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Verify each team's SSH passwords actually work in the live containers.
|
||||
|
||||
state.json can look perfect while the container holds a different password —
|
||||
set_ssh_passwords() races container boot and its failures are easy to miss.
|
||||
This proves the binding from the INSIDE (per the skill rule: never trust
|
||||
config, prove it with a real login).
|
||||
|
||||
python3 panel/verify_ssh_creds.py [teamIdx ...]
|
||||
"""
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
import teams as orch
|
||||
|
||||
def probe(user, pw, port, host="127.0.0.1"):
|
||||
r = subprocess.run(
|
||||
["sshpass", "-p", pw, "ssh",
|
||||
"-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null",
|
||||
"-o", "ConnectTimeout=8", "-o", "LogLevel=ERROR",
|
||||
"-p", str(port), f"{user}@{host}", "whoami; hostname"],
|
||||
capture_output=True, text=True, timeout=30)
|
||||
out = (r.stdout or "").strip().splitlines()
|
||||
return (r.returncode == 0 and len(out) >= 2, out, (r.stderr or "").strip()[:80])
|
||||
|
||||
def main():
|
||||
want = [int(a) for a in sys.argv[1:]]
|
||||
teams = [t for t in orch.list_teams() if not want or t["index"] in want]
|
||||
for t in teams:
|
||||
idx = t["index"]
|
||||
names = [c["name"] for c in orch.enabled_challenges()]
|
||||
jobs = []
|
||||
for n in names:
|
||||
if n not in (t.get("ports") or {}):
|
||||
continue
|
||||
jobs.append((n, t["ports"][n]["ssh"], t.get("chall_passwords", {}).get(n)))
|
||||
ok = bad = 0
|
||||
details = []
|
||||
with ThreadPoolExecutor(max_workers=6) as ex:
|
||||
futs = {ex.submit(probe, t.get("ssh_user", "ctfuser"), pw, port): n
|
||||
for n, port, pw in jobs if pw}
|
||||
for fut, n in futs.items():
|
||||
good, out, err = fut.result()
|
||||
if good:
|
||||
ok += 1
|
||||
# hostname must be <challenge>_teamN — proves the binding
|
||||
details.append((n, out[1] if len(out) > 1 else "?"))
|
||||
else:
|
||||
bad += 1
|
||||
details.append((n, f"FAIL {err}"))
|
||||
print(f"team{idx} ({t.get('label')}): ssh {ok} ok / {bad} fail (user={t.get('ssh_user')})")
|
||||
for n, info in details:
|
||||
if info == "FAIL" or info.startswith("FAIL"):
|
||||
print(f" {n}: {info}")
|
||||
hosts = [i for n, i in details if not i.startswith("FAIL")]
|
||||
mism = [(n, i) for n, i in details
|
||||
if not i.startswith("FAIL") and not i.endswith(f"_team{idx}")]
|
||||
if mism:
|
||||
print(f" !! hostname mismatch (not _team{idx}): {mism}")
|
||||
else:
|
||||
print(f" all hostnames correct (e.g. {hosts[0] if hosts else '-'})")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user