fix(portal): per-challenge SSH user in web terminal + credential API
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.
This commit is contained in:
1 parent
0a56906b18
commit
50cb782ded
15 files changed
+820
-28
No files matched your search
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env python3
|
||||
"""End-to-end: does the credential the panel SHOWS actually log in over SSH?
|
||||
|
||||
The bug being regression-tested: the panel/terminal reported `ctfuser` for all
|
||||
16 challenges, but 10 of them (the imported XVI/XVII images) only provision
|
||||
`root`, so every participant login was refused. A correct-looking JSON payload
|
||||
proves nothing -- this opens a real paramiko session per challenge with exactly
|
||||
the username/password/port the UI hands out.
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
import paramiko
|
||||
from pathlib import Path
|
||||
|
||||
BASE = Path("/opt/gemastik18-final")
|
||||
sys.path.insert(0, str(BASE / "panel"))
|
||||
import teams # noqa: E402
|
||||
|
||||
TEAM = int(sys.argv[1]) if len(sys.argv) > 1 else 1
|
||||
st = teams.team_state(TEAM)
|
||||
if not st:
|
||||
print(f"team{TEAM} has no state.json")
|
||||
sys.exit(1)
|
||||
|
||||
users = teams.challenge_ssh_users()
|
||||
ok = bad = 0
|
||||
failures = []
|
||||
|
||||
for name, _coff, _soff in teams.CHALLENGES:
|
||||
p = st.get("ports", {}).get(name)
|
||||
if not p:
|
||||
continue
|
||||
user = users.get(name, "ctfuser")
|
||||
pw = st.get("chall_passwords", {}).get(name) or ""
|
||||
port = p["ssh"]
|
||||
cli = paramiko.SSHClient()
|
||||
cli.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
try:
|
||||
cli.connect("127.0.0.1", port=port, username=user, password=pw,
|
||||
timeout=12, allow_agent=False, look_for_keys=False)
|
||||
_, out, _ = cli.exec_command("whoami; hostname", timeout=12)
|
||||
got = out.read().decode().strip().replace("\n", " | ")
|
||||
# The container must actually be the account we claim it is.
|
||||
actual = got.split(" | ")[0].strip() if got else "?"
|
||||
if actual == user:
|
||||
print(f" {name:<15} {user:<8} :{port} OK -> {got}")
|
||||
ok += 1
|
||||
else:
|
||||
print(f" {name:<15} {user:<8} :{port} WHOAMI MISMATCH -> {got}")
|
||||
failures.append((name, user, actual))
|
||||
bad += 1
|
||||
except Exception as e:
|
||||
msg = type(e).__name__
|
||||
print(f" {name:<15} {user:<8} :{port} LOGIN FAILED ({msg})")
|
||||
failures.append((name, user, msg))
|
||||
bad += 1
|
||||
finally:
|
||||
try:
|
||||
cli.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
print(f"\nteam{TEAM}: {ok} logins OK, {bad} failed")
|
||||
if failures:
|
||||
print("failures:")
|
||||
for f in failures:
|
||||
print(" ", f)
|
||||
sys.exit(1 if bad else 0)
|
||||
Reference in new issue
Block a user