"""Shared building blocks for the CVE-2026-57827 toolchain. Everything here is deliberately dependency-light and reusable: the HTTP plumbing, the per-scan config, the result record, and a few small URL/id helpers. The detection module (rsfiles_detect.py) and the main exploit (cve_2026_57827.py) both import from this module so there is a single source of truth for network and config behaviour. """ from __future__ import annotations import hashlib import os import random import re import sys import warnings from dataclasses import dataclass from typing import Optional from urllib.parse import urlsplit try: import urllib3 urllib3.disable_warnings() except Exception: # pragma: no cover pass warnings.filterwarnings("ignore") import requests # --------------------------------------------------------------------------- # # Version parsing (optional `packaging`, with a portable fallback) # --------------------------------------------------------------------------- # try: from packaging.version import parse as parse_version except Exception: # pragma: no cover def parse_version(v): return tuple(int(x) for x in re.findall(r"\d+", str(v))) TIMEOUT = 10 MAX_THREADS = 30 SPIN = ("⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏") # --------------------------------------------------------------------------- # # ANSI / colour helpers (auto-disable on non-tty terminals) # --------------------------------------------------------------------------- # def _use_colour() -> bool: if os.environ.get("NO_COLOR"): return False if os.environ.get("FORCE_COLOR"): return True return hasattr(sys.stdout, "isatty") and sys.stdout.isatty() COLOR = _use_colour() def c(s: str, code: str) -> str: return f"\033[{code}m{s}\033[0m" if COLOR else s GREEN, RED, YELLOW, DIM = "32", "31", "33", "90" # --------------------------------------------------------------------------- # # Configuration & result records # --------------------------------------------------------------------------- # @dataclass class Config: """Tunables for one scan. Explicit > hidden globals.""" timeout: int = TIMEOUT threads: int = MAX_THREADS debug: bool = False cleanup: bool = True proxy: Optional[str] = None output: Optional[str] = None # plain text list of RCE URLs csv_output: Optional[str] = None # structured CSV report @dataclass class Result: host: str status: str = "pending" detected: bool = False version: Optional[str] = None upload_ok: bool = False shell_url: Optional[str] = None token: Optional[str] = None rce: bool = False output: Optional[str] = None error: Optional[str] = None elapsed: float = 0.0 protocol: Optional[str] = None def rand_id(n: int = 8) -> str: """Cryptographically random hex id of length n.""" return hashlib.sha256(os.urandom(16)).hexdigest()[:n] # --------------------------------------------------------------------------- # # HTTP client # --------------------------------------------------------------------------- # UA_POOL = [ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/131.0.0.0 Safari/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_7) AppleWebKit/605.1.15 Safari/605.1.15", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/131.0.0.0 Safari/537.36", ] class HttpClient: """Thin wrapper over requests.Session: per-base sessions, proxy, random UA.""" def __init__(self, cfg: Config): self.cfg = cfg self._sessions: dict[str, requests.Session] = {} def _session_for(self, base: str) -> requests.Session: if base not in self._sessions: s = requests.Session() s.verify = False s.headers.update({"User-Agent": random.choice(UA_POOL)}) if self.cfg.proxy: s.proxies = {"http": self.cfg.proxy, "https": self.cfg.proxy} self._sessions[base] = s return self._sessions[base] def request(self, base: str, method: str, url: str, **kw) -> requests.Response: s = self._session_for(base) kw.setdefault("timeout", self.cfg.timeout) return s.request(method, url, **kw) def close_all(self) -> None: for s in self._sessions.values(): try: s.close() except Exception: pass # --------------------------------------------------------------------------- # # URL / host helpers # --------------------------------------------------------------------------- # def normalize_host(raw: str) -> str: """Strip scheme, path, trailing slash and (optionally) default port.""" raw = raw.strip().rstrip("/") raw = re.sub(r"^https?://", "", raw) raw = raw.split("/", 1)[0] raw = re.sub(r":(80|443)$", "", raw) # drop default ports return raw def base_of(url: str) -> str: """Extract the scheme://host[:port] prefix from a full URL. Single source of truth for URL parsing, so callers never repeat the fragile regex + split dance that crashed the original code on odd input. """ p = urlsplit(url) return f"{p.scheme}://{p.netloc}" def base_candidates(host: str) -> list[str]: """Return base URLs to try, https first, always falling back to http. When the host carries an explicit non-standard port, plain HTTP is the more common serving scheme for a raw Joomla box, so it is tried first. """ m = re.search(r":(\d{2,5})$", host) if m and int(m.group(1)) not in (80, 443): return [f"http://{host}", f"https://{host}"] return [f"https://{host}", f"http://{host}"]