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:
1 parent
ef385c3397
commit
ae50acfe40
33 files changed
+1398
-289
No files matched your search
@@ -28,7 +28,7 @@ class Art(Challenge):
|
||||
def check(self):
|
||||
try:
|
||||
word = self.random_string(8)
|
||||
url = f'http://localhost:{self.port}/art/{word}'
|
||||
url = self.url(f'/art/{word}')
|
||||
r = requests.get(url, timeout=5)
|
||||
assert r.text == f'<iframe height="100%" width="100%" frameborder="0" src=https://asciified.thelicato.io/api/v2/ascii?text={word}></iframe>', 'Unexpected response'
|
||||
self.logger.info('Check passed for art')
|
||||
|
||||
@@ -33,7 +33,7 @@ class Burvesigner(Challenge):
|
||||
|
||||
def check(self):
|
||||
try:
|
||||
url = f'http://localhost:{self.port}'
|
||||
url = self.url()
|
||||
flag = open(self.flag_location).read()
|
||||
|
||||
# C1: login guest success
|
||||
|
||||
@@ -10,11 +10,39 @@ class Challenge(object):
|
||||
name = __name__
|
||||
settings = get_settings()
|
||||
port = 0
|
||||
# Host the checker connects to. The team receiver runs on the same machine
|
||||
# as the published team ports, so 127.0.0.1 is correct; override with
|
||||
# RECEIVER_HOST if a receiver ever runs off-host.
|
||||
host = os.environ.get("RECEIVER_HOST", "127.0.0.1")
|
||||
# URL scheme for this challenge. TLS services (gleam-drive/bandit) serve
|
||||
# HTTPS only, so a plain http:// request fails against a healthy service.
|
||||
# Read from CHALLENGE_SCHEME_<NAME> by the concrete checker via _scheme();
|
||||
# this default is overridden per class where needed.
|
||||
scheme = os.environ.get("RECEIVER_SCHEME", "http")
|
||||
|
||||
def __init__(self, port):
|
||||
def __init__(self, port, host=None):
|
||||
self.port = port
|
||||
if host:
|
||||
self.host = host
|
||||
self.add_logger()
|
||||
|
||||
def url(self, path=""):
|
||||
"""Absolute URL for the challenge service.
|
||||
|
||||
Every XVI checker (Art, XL, S3, ...) calls self.url(...) — without this
|
||||
helper they all fail with "'<Class>' object has no attribute 'url'" and
|
||||
the receiver reports the service DOWN while it is actually healthy.
|
||||
|
||||
The scheme is per-challenge: a TLS service (CHALLENGE_SCHEME_<NAME>=https)
|
||||
must be reached over https or requests raises an SSLError. The env key is
|
||||
derived from the class module name, which the generator renders as the
|
||||
challenge name, with hyphens normalized to underscores.
|
||||
"""
|
||||
p = "" if path.startswith("/") else "/"
|
||||
key = self.name.upper().replace("-", "_")
|
||||
scheme = os.environ.get(f"CHALLENGE_SCHEME_{key}") or self.scheme
|
||||
return f"{scheme}://{self.host}:{self.port}{p}{path}"
|
||||
|
||||
def add_logger(self):
|
||||
self.logger = logging.getLogger()
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ class Crawlback(Challenge):
|
||||
|
||||
def check(self):
|
||||
try:
|
||||
r = requests.post(f"http://localhost:{self.port}/crawlback.php", data={'url': MOCK_URL})
|
||||
r = requests.post(self.url(f'/crawlback.php'), data={'url': MOCK_URL})
|
||||
|
||||
assert r.text.split('\n').pop(0) == MOCK_DATA
|
||||
|
||||
|
||||
@@ -35,29 +35,29 @@ class GemasFetcher(Challenge):
|
||||
## register
|
||||
username = self.random_string(5)
|
||||
password = self.random_string(5)
|
||||
r = sess.post(f"http://localhost:{self.port}/auth/register", data={"username":username,"password": password}, allow_redirects=False)
|
||||
r = sess.post(self.url(f'/auth/register'), data={"username":username,"password": password}, allow_redirects=False)
|
||||
assert r.headers.get("location") == "/auth/login", "Register Failed"
|
||||
|
||||
## login
|
||||
r = sess.post(f"http://localhost:{self.port}/auth/login", data={"username":username,"password": password}, allow_redirects=False)
|
||||
r = sess.post(self.url(f'/auth/login'), data={"username":username,"password": password}, allow_redirects=False)
|
||||
assert r.headers.get("location") == "/dashboard", "Login Failed"
|
||||
|
||||
## wget
|
||||
content = {"provider": "wget","url":MOCK_URL}
|
||||
files = {"file": ("visit", b"\x00\x00"+gzip.compress(zlib.compress(json.dumps(content).encode())))}
|
||||
r = sess.post(f"http://localhost:{self.port}/dashboard/fetch_by_file", files=files)
|
||||
r = sess.post(self.url(f'/dashboard/fetch_by_file'), files=files)
|
||||
assert MOCK_DATA_WGET in r.text, "wget Failed"
|
||||
|
||||
## curl
|
||||
content = {"provider": "curl","url":MOCK_URL}
|
||||
files = {"file": ("visit", b"\x00\x01"+gzip.compress(zlib.compress(json.dumps(content).encode())))}
|
||||
r = sess.post(f"http://localhost:{self.port}/dashboard/fetch_by_file", files=files)
|
||||
r = sess.post(self.url(f'/dashboard/fetch_by_file'), files=files)
|
||||
assert r.text.split('\n').pop(0) == MOCK_DATA_CURL, "curl Failed"
|
||||
|
||||
## python
|
||||
content = {"provider": "python","url":MOCK_URL}
|
||||
files = {"file": ("visit", b"\x00\x02"+gzip.compress(zlib.compress(json.dumps(content).encode())))}
|
||||
r = sess.post(f"http://localhost:{self.port}/dashboard/fetch_by_file", files=files)
|
||||
r = sess.post(self.url(f'/dashboard/fetch_by_file'), files=files)
|
||||
assert r.text.startswith('"PCFkb2N0eXBlIGh0bWw'), "python Failed"
|
||||
|
||||
return True
|
||||
|
||||
@@ -4,13 +4,11 @@ import requests
|
||||
|
||||
class GemasNotes(Challenge):
|
||||
history_location = 'history/gemas-notes.txt'
|
||||
host = "http://localhost:12000"
|
||||
|
||||
def distribute(self, flag):
|
||||
try:
|
||||
username = "gemasflagreceiver"
|
||||
password = "AuTeEbn%.Q5$pC_ge6"
|
||||
result = requests.post(f"{self.host}/flag_receiver", json={"flag": flag}, auth=(username,password)).json()
|
||||
result = requests.post(self.url("flag_receiver"), json={"flag": flag}, auth=(username,password)).json()
|
||||
if not result.get("success"):
|
||||
return False
|
||||
|
||||
@@ -26,7 +24,7 @@ class GemasNotes(Challenge):
|
||||
|
||||
def check(self):
|
||||
try:
|
||||
url = f'http://localhost:{self.port}'
|
||||
url = self.url()
|
||||
|
||||
# login
|
||||
token = requests.post(f"{url}/api/login",json={"email":"checker@gemasnotes.id", "password":"uRIqCvJ<IGb;VDT14"}).json()["token"]
|
||||
|
||||
@@ -27,7 +27,7 @@ class Hirnfick(Challenge):
|
||||
def check(self):
|
||||
try:
|
||||
res = requests.post(
|
||||
f"http://localhost:{self.port}/api/run",
|
||||
self.url(f'/api/run'),
|
||||
timeout=5,
|
||||
json={
|
||||
"code":
|
||||
|
||||
@@ -6,8 +6,6 @@ import requests
|
||||
class Pasta(Challenge):
|
||||
flag_location = 'flags/pasta.txt'
|
||||
history_location = 'history/pasta.txt'
|
||||
host = "http://localhost:13000"
|
||||
|
||||
def distribute(self, flag):
|
||||
try:
|
||||
with open(self.flag_location, 'w') as f:
|
||||
@@ -25,7 +23,7 @@ class Pasta(Challenge):
|
||||
|
||||
def check(self):
|
||||
try:
|
||||
url = f'http://localhost:{self.port}'
|
||||
url = self.url()
|
||||
username = f"checker-{self.random_string(8)}"
|
||||
pwd = self.random_string(12)
|
||||
flag = open(self.flag_location).read()
|
||||
|
||||
@@ -7,8 +7,6 @@ import os
|
||||
class S3(Challenge):
|
||||
flag_location = 'flags/s3.txt'
|
||||
history_location = 'history/s3.txt'
|
||||
host = 'http://localhost:20000'
|
||||
|
||||
def distribute(self, flag):
|
||||
try:
|
||||
with open(self.flag_location, 'w') as f:
|
||||
@@ -29,11 +27,11 @@ class S3(Challenge):
|
||||
filename = self.random_string(8) + ".txt"
|
||||
content = self.random_string(64)
|
||||
|
||||
r = requests.post(f"http://localhost:{self.port}/upload", files={'file': (filename, content)})
|
||||
r = requests.post(self.url(f'/upload'), files={'file': (filename, content)})
|
||||
assert r.status_code == 200
|
||||
assert r.text == f'Download <a href="/download?filename={filename}">here</a>'
|
||||
|
||||
r = requests.get(f"http://localhost:{self.port}/download?filename={filename}")
|
||||
r = requests.get(self.url(f'/download?filename={filename}'))
|
||||
assert r.status_code == 200
|
||||
assert r.text == content
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ class XL(Challenge):
|
||||
|
||||
def check(self):
|
||||
try:
|
||||
url = f'http://localhost:{self.port}'
|
||||
url = self.url()
|
||||
files = {'file': self.generate_mock_file()}
|
||||
r = requests.post(url, files=files, timeout=5)
|
||||
assert r.json() == MOCK_RESULT, 'Unexpected response'
|
||||
|
||||
Reference in new issue
Block a user