Three independent root causes, all found by measuring instead of assuming:
1. SSH failed on 10/16 challenges while state.json looked perfect.
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 nobody
used -> 'Permission denied' on every team. Registry gains a per-challenge
'ssh_user'; chpasswd targets the real login and reports failures loudly.
2. phew SLA timed out on a healthy service, four bugs stacked:
- chall.py block-buffers stdout through the exec pipe (PYTHONUNBUFFERED now
set) and does a fresh Pailier keygen (~12 s) before printing its menu;
- _read_until read a TEXT pipe, so read(1) pulled 8 KB into Python's
TextIOWrapper buffer and select() then blocked on data already in memory;
- its buffer was per-call, so the read satisfying 'pt (hex)' also swallowed
the '> ' the next call waited for -> a race that failed intermittently;
- reaping killed chall.py it did not own: a blanket pkill -f, a
snapshot-diff (concurrent sessions diff against the same pre-spawn set),
and a class-level _children shared across uvicorn's thread pool. The child
now prints its own pid so exactly one session is reaped.
Also: ONE interactive session per check instead of five spawns (Paillier is
randomized per ciphertext, not per process) - 5 keygens were the CPU load
that starved the checks. And the 6 orphan single-node containers from the
original deploy were removed; one held 58 leaked chall.py and drove load
average 76 on 2 CPUs.
3. missing_sidecars() matched compose-generated names (teamN-<svc>-1) while
every service sets an explicit container_name, so it reported all 16 running
challenges as missing and hid the one real gap (anti-alchemy-db, which has
no container_name). Now reads container_name when present and falls back to
the compose default otherwise.
Verified: 64/64 SLA across 4 teams; 64/64 real SSH logins succeed with
correct <chall>_teamN hostnames; phew 3/3 sequential with no process leak.
Adds panel/verify_ssh_creds.py, audit_ssh_users.sh, reset_runtime.sh,
sla_sweep.sh, fix_sidecars.sh, phew_concurrency_test.sh, exec_probe_i.py.
69 lines
1.7 KiB
Python
Executable File
69 lines
1.7 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
import os
|
|
from Pailier import *
|
|
from Crypto.Util.number import *
|
|
|
|
with open("/flag.txt", "rb") as f:
|
|
flag_bytes = f.read()
|
|
|
|
key = os.urandom(66)
|
|
key_int = bytes_to_long(key)
|
|
|
|
cipher = pailier()
|
|
|
|
while True:
|
|
print("1. encrypt")
|
|
print("2. bingo")
|
|
print("3. decrypt")
|
|
print("4. key?")
|
|
try:
|
|
inp = int(input("> "))
|
|
except (ValueError, EOFError):
|
|
print("Invalid input")
|
|
continue
|
|
|
|
if inp == 1:
|
|
print("pt (hex)")
|
|
try:
|
|
inp = input("> ")
|
|
ct = cipher.encrypt(int(inp, 16))
|
|
print('ct : ', '{0:x}'.format(ct))
|
|
except (ValueError, EOFError):
|
|
print("Invalid hex input")
|
|
|
|
elif inp == 2:
|
|
print("key (hex)")
|
|
try:
|
|
user_hex = input("> ").strip()
|
|
user_key = bytes.fromhex(user_hex)
|
|
|
|
if len(user_key) == 66 and user_key == key:
|
|
try:
|
|
print(flag_bytes.decode())
|
|
except Exception:
|
|
print(flag_bytes.hex())
|
|
else:
|
|
print("nope")
|
|
except (ValueError, EOFError):
|
|
print("nope")
|
|
|
|
elif inp == 3:
|
|
print("ct (hex)")
|
|
try:
|
|
inp = input("> ")
|
|
pt = cipher.decrypt(int(inp, 16))
|
|
print('pt : ', '{0:x}'.format(pt))
|
|
except (ValueError, EOFError):
|
|
print("Invalid hex input")
|
|
|
|
elif inp == 4:
|
|
try:
|
|
ct = cipher.encrypt(key_int)
|
|
print('ct : ', '{0:x}'.format(ct))
|
|
except Exception:
|
|
print("Encryption error")
|
|
|
|
else:
|
|
exit()
|