Files
felhom.eu/documentation/audits/login-gate-2026-09-29/B/browser.py
T
2026-09-29 08:20:42 +02:00

75 lines
3.1 KiB
Python

# SPIKE evidence tool (2026-09-29): a minimal "browser" — per-host cookie jar (host-only cookies, as a browser keeps
# them), follows redirects across hosts, every host resolved to guest 9202's traefik. Never prints a password or a cookie.
import http.client, json, re, ssl, urllib.parse
IP = "192.168.0.114"
CTX = ssl._create_unverified_context() # scratch guest 9202 serves traefik's self-signed default cert on the LAN
class Browser:
def __init__(self, name):
self.name = name
self.jar = {} # host -> {cookie: value}
def req(self, url, method="GET", body=None, headers=None, follow=True, accept="text/html"):
hops = []
for _ in range(12):
u = urllib.parse.urlsplit(url)
host = u.hostname
path = u.path + ("?" + u.query if u.query else "")
h = {"Host": host, "Accept": accept, "User-Agent": "felhom-spike-browser"}
ck = self.jar.get(host, {})
if ck:
h["Cookie"] = "; ".join(f"{k}={v}" for k, v in ck.items())
h.update(headers or {})
data = body
if isinstance(body, dict):
data = json.dumps(body).encode()
h["Content-Type"] = "application/json"
elif isinstance(body, str):
data = body.encode()
conn = http.client.HTTPSConnection(IP, 443, context=CTX, timeout=30)
conn.connect = _sni_connect(conn, host)
conn.request(method, path, body=data, headers=h)
r = conn.getresponse()
rb = r.read()
for k, v in r.getheaders():
if k.lower() == "set-cookie":
nv = v.split(";", 1)[0]
n, _, val = nv.partition("=")
if "max-age=-1" in v.lower() or val == "":
self.jar.setdefault(host, {}).pop(n, None)
else:
self.jar.setdefault(host, {})[n] = val
hops.append((r.status, host, u.path))
loc = r.getheader("Location")
if follow and r.status in (301, 302, 303, 307, 308) and loc:
url = urllib.parse.urljoin(url, loc)
if r.status in (301, 302, 303):
method, body = "GET", None
continue
return r.status, rb.decode("utf-8", "replace"), hops
return 0, "", hops
def login_dashboard(self, domain, password):
# The dashboard's own login form (POST /login), exactly as walk.login() does it.
return self.req(f"https://felhom.{domain}/login", "POST",
body="password=" + urllib.parse.quote(password),
headers={"Content-Type": "application/x-www-form-urlencoded"})
def hosts_with_cookies(self):
return {h: sorted(v) for h, v in self.jar.items()}
def _sni_connect(conn, host):
import socket
def connect():
sock = socket.create_connection((IP, 443), timeout=30)
conn.sock = CTX.wrap_socket(sock, server_hostname=host)
return connect
def hopstr(hops):
return " -> ".join(f"{s} {h}{p}" for s, h, p in hops)