b63654a299
gates / gates (push) Successful in 26s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
90 lines
4.0 KiB
Python
90 lines
4.0 KiB
Python
#!/usr/bin/env python3
|
|
"""Decision 61 (R-752) on 9202, DRILL catalog: calibre-web with a generated admin LOGIN NAME.
|
|
1 the box's template copy carries ADMIN_USER; 2 a stranger polls the public default login from the deploy press until
|
|
the install hold opens (R-741's window); 3 the household's name is read the way the app page reads it (the Settings
|
|
page's reveal, POST /stacks/<n>/auto-field/reveal); 4 positive controls (name + password: the login form and OPDS),
|
|
`admin` + the right password refused; 5 a stranger's 40 wrong tries on `admin` at the 3/minute pace (~14 min); 6 the
|
|
household logs in AT ONCE with its own name (form and OPDS); a wrong password on the real name still refused. Removed."""
|
|
import json, subprocess, threading, time
|
|
import walk as w
|
|
import lk
|
|
|
|
APP, SUB = "calibre-web", "books"
|
|
|
|
|
|
def reveal(env_var):
|
|
sess = open(f"{w.SC}/sess{__import__('os').getpid()}.txt").read().strip()
|
|
csrf = open(f"{w.SC}/csrf{__import__('os').getpid()}.txt").read().strip()
|
|
r = subprocess.run(["curl", "-sk", "-H", w.HOSTHDR, "-H", f"Cookie: {sess}", "-H", f"X-CSRF-Token: {csrf}",
|
|
"-H", "Content-Type: application/x-www-form-urlencoded", "--data", f"env_var={env_var}",
|
|
f"{w.BASE}/stacks/{APP}/auto-field/reveal"], capture_output=True, text=True)
|
|
try:
|
|
return json.loads(r.stdout)["data"]["value"]
|
|
except Exception:
|
|
return ""
|
|
|
|
|
|
w.login()
|
|
for _ in range(12):
|
|
w.sync_rescan()
|
|
cp = w.guest(f"grep -c ADMIN_USER /opt/docker/stacks/{APP}/.felhom.yml").strip()
|
|
if cp not in ("", "0"):
|
|
break
|
|
time.sleep(15)
|
|
lk.p("the box's template copy names ADMIN_USER:", cp, "times")
|
|
|
|
res, stop = [], threading.Event()
|
|
|
|
|
|
def poll():
|
|
while not stop.is_set():
|
|
res.append((lk.now(), lk.calibre("admin", "admin123")))
|
|
time.sleep(2)
|
|
|
|
|
|
t = threading.Thread(target=poll, daemon=True)
|
|
t.start()
|
|
lk.p("deploy:", w.deploy(APP, SUB))
|
|
for _ in range(120):
|
|
time.sleep(5)
|
|
if "hold OPENED" in w.guest(f"docker logs --since 20m felhom-controller 2>&1 | grep '{APP}: install hold OPENED'"):
|
|
break
|
|
time.sleep(5)
|
|
stop.set()
|
|
t.join()
|
|
prev = None
|
|
for ts, c in res:
|
|
if c != prev:
|
|
lk.p(" stranger, default login admin/admin123 ->", c)
|
|
prev = c
|
|
lk.p("stranger got in with the default login:", sum(1 for _, c in res if c == "ok"), "of", len(res))
|
|
lk.p(w.guest("docker logs --since 20m felhom-controller 2>&1 | grep -E 'calibre-web.*(install HOLD|hold OPENED|after_install)' | cut -c1-200").strip())
|
|
lk.p("app.yaml after_install record:", w.guest(f"grep -A3 '^after_install:' /opt/docker/stacks/{APP}/app.yaml").strip().replace("\n", " "))
|
|
|
|
name = reveal("ADMIN_USER")
|
|
pw = (w.GENERATED.get(APP) or {}).get("ADMIN_PASSWORD", "")
|
|
lk.p(f"the household's name (read through the page's reveal): {len(name)} characters, lowercase hex: {name.isalnum() and name == name.lower()}")
|
|
lk.p("users in app.db (total, named admin):", w.guest("""cat > /tmp/cwu.py <<'PY'
|
|
import sqlite3
|
|
c = sqlite3.connect('/config/app.db')
|
|
print(c.execute("SELECT count(*), sum(lower(name) = 'admin') FROM user").fetchone())
|
|
PY
|
|
docker cp /tmp/cwu.py calibre-web:/tmp/cwu.py && docker exec calibre-web python3 /tmp/cwu.py""").strip())
|
|
lk.p("positive — form, name + password:", lk.calibre(name, pw), "| OPDS:", lk.calibre_opds(name, pw))
|
|
lk.p("admin + the RIGHT password (the old name is gone):", lk.calibre("admin", pw))
|
|
lk.p("## a stranger: 40 wrong tries on `admin`, 3 a minute")
|
|
n = 0
|
|
while n < 40:
|
|
for _ in range(3):
|
|
if n < 40:
|
|
n += 1
|
|
r = lk.calibre("admin", f"daily-{n}")
|
|
if n % 10 == 0 or r not in ("wrong",):
|
|
lk.p(f" wrong try {n}: {r}")
|
|
time.sleep(61)
|
|
lk.p("one more stranger try on admin:", lk.calibre("admin", "one-more"))
|
|
lk.p("HOUSEHOLD at once — form, own name + password:", lk.calibre(name, pw), "| OPDS:", lk.calibre_opds(name, pw))
|
|
lk.p("a wrong password on the real name:", lk.calibre(name, "wrong-real"), "| right again:", lk.calibre(name, pw))
|
|
w.remove(APP)
|
|
lk.p("removed; deployed =", w.stack(APP).get("deployed"))
|