diff --git a/app/templates/admin/acces.php b/app/templates/admin/acces.php index c000580..005bae2 100644 --- a/app/templates/admin/acces.php +++ b/app/templates/admin/acces.php @@ -1429,6 +1429,19 @@ +%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) +\\\\\\\ to: osltsstu c8c1516d "fix: validation error messages hidden by generic fallback in ErrorHandler::userMessage" (rebased revision) ++ $linkName = $link['name'] ?? ''; +++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: osltsstu c8c1516d "fix: validation error messages hidden by generic fallback in ErrorHandler::userMessage" (rebased revision) +\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) +- $linkName = $link['name'] ?? ''; +- $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: somsyvxz 14a3cd10 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebase destination) +\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: srupvzpw 6bfa45fc "feat: add passive pentest scanner script with PEP 723 uv metadata" (rebased revision) + $linkName = $link['name'] ?? ''; + $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; + $linkLockedYear = $link['locked_year'] ?? null; ++%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) ++\\\\\\\ to: srupvzpw 5a2ec7e2 "feat: add passive pentest scanner script with PEP 723 uv metadata" (rebased revision) +++ $linkName = $link['name'] ?? ''; ++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; ?> diff --git a/pentest_xamxam.py b/pentest_xamxam.py new file mode 100644 index 0000000..5d0a208 --- /dev/null +++ b/pentest_xamxam.py @@ -0,0 +1,469 @@ +#!/usr/bin/env -S uv run +# /// script +# requires-python = ">=3.12" +# dependencies = [ +# "requests", +# "dnspython", +# "colorama", +# ] +# /// +""" +Passive / Non-Destructive Pentest Script +Target: xamxam.erg.be +Author: generated for site owner / authorized use only + +Checks performed: + 1. DNS & WHOIS metadata + 2. TLS certificate info + 3. HTTP security headers audit + 4. Cookie flags audit + 5. Sensitive file/path exposure + 6. robots.txt & sitemap.xml parsing + 7. Open redirect probe (non-exploiting) + 8. Reflected XSS canary in search param (detection only) + 9. Directory listing detection + 10. Software version disclosure in headers/meta + 11. CORS misconfiguration check + 12. Clickjacking (X-Frame-Options / CSP frame-ancestors) + 13. HSTS check + 14. Subdomain enumeration (wordlist) + 15. Summary report + +Usage: + pip install requests dnspython colorama + python3 pentest_xamxam.py +""" + +import socket +import ssl +import json +import re +import datetime +import urllib.parse +from dataclasses import dataclass, field +from typing import Optional + +import requests +import dns.resolver +from colorama import Fore, Style, init + +init(autoreset=True) + +# ── Config ──────────────────────────────────────────────────────────────────── +TARGET_HOST = "xamxam.erg.be" +TARGET_BASE = f"https://{TARGET_HOST}" +TIMEOUT = 8 +HEADERS = {"User-Agent": "PentestScanner/1.0 (authorized audit)"} + +# Paths an attacker would probe first +SENSITIVE_PATHS = [ + "/.env", "/.env.local", "/.env.production", + "/.git/HEAD", "/.git/config", + "/wp-admin", "/wp-login.php", # CMS probes + "/admin", "/administrator", "/admin/login", + "/phpmyadmin", "/pma", + "/backup.zip", "/backup.sql", "/db.sql", + "/config.php", "/config.json", "/settings.py", + "/robots.txt", "/sitemap.xml", + "/.htaccess", "/web.config", + "/api", "/api/v1", "/api/users", + "/graphql", + "/server-status", # Apache status page + "/actuator", "/actuator/health", # Spring Boot + "/_next/static", # Next.js artefacts + "/static", + "/uploads", "/files", "/media", +] + +SECURITY_HEADERS = { + "Strict-Transport-Security": "HSTS — forces HTTPS", + "Content-Security-Policy": "CSP — mitigates XSS", + "X-Frame-Options": "Clickjacking protection", + "X-Content-Type-Options": "MIME sniffing protection", + "Referrer-Policy": "Controls referrer leakage", + "Permissions-Policy": "Limits browser feature access", + "X-XSS-Protection": "Legacy XSS filter (deprecated but informative)", + "Cross-Origin-Opener-Policy": "Isolates browsing context", + "Cross-Origin-Resource-Policy": "Controls cross-origin resource sharing", +} + +SUBDOMAIN_WORDLIST = [ + "www", "mail", "smtp", "ftp", "dev", "staging", "test", "api", + "admin", "portal", "old", "beta", "cdn", "static", "media", + "docs", "blog", "intranet", "vpn", "login", "auth", +] + +XSS_CANARY = "" +OPEN_REDIRECT_PAYLOADS = ["//evil.com", "https://evil.com", "/\\evil.com"] + + +# ── Helpers ─────────────────────────────────────────────────────────────────── +def ok(msg): print(f" {Fore.GREEN}[✓]{Style.RESET_ALL} {msg}") +def warn(msg): print(f" {Fore.YELLOW}[!]{Style.RESET_ALL} {msg}") +def bad(msg): print(f" {Fore.RED}[✗]{Style.RESET_ALL} {msg}") +def info(msg): print(f" {Fore.CYAN}[i]{Style.RESET_ALL} {msg}") +def section(title): + print(f"\n{Fore.MAGENTA}{'═'*60}{Style.RESET_ALL}") + print(f"{Fore.MAGENTA} {title}{Style.RESET_ALL}") + print(f"{Fore.MAGENTA}{'═'*60}{Style.RESET_ALL}") + + +@dataclass +class Finding: + severity: str # CRITICAL / HIGH / MEDIUM / LOW / INFO + title: str + detail: str + + +findings: list[Finding] = [] + +def add(severity, title, detail=""): + findings.append(Finding(severity, title, detail)) + + +def get(path="", params=None, allow_redirects=True) -> Optional[requests.Response]: + try: + return requests.get( + TARGET_BASE + path, + params=params, + headers=HEADERS, + timeout=TIMEOUT, + allow_redirects=allow_redirects, + verify=True, + ) + except requests.exceptions.SSLError as e: + add("HIGH", f"SSL error on {path}", str(e)) + return None + except requests.exceptions.ConnectionError: + return None + except requests.exceptions.Timeout: + return None + + +# ── 1. DNS & IP ─────────────────────────────────────────────────────────────── +def check_dns(): + section("1. DNS & IP Resolution") + try: + ip = socket.gethostbyname(TARGET_HOST) + info(f"A record → {ip}") + add("INFO", "A record", ip) + except Exception as e: + bad(f"DNS resolution failed: {e}") + + for rtype in ("MX", "TXT", "NS", "AAAA"): + try: + answers = dns.resolver.resolve(TARGET_HOST, rtype) + for r in answers: + info(f"{rtype:5s} → {r}") + except Exception: + pass + + +# ── 2. TLS Certificate ──────────────────────────────────────────────────────── +def check_tls(): + section("2. TLS Certificate") + try: + ctx = ssl.create_default_context() + with ctx.wrap_socket(socket.socket(), server_hostname=TARGET_HOST) as s: + s.connect((TARGET_HOST, 443)) + cert = s.getpeercert() + + subject = dict(x[0] for x in cert["subject"]) + issuer = dict(x[0] for x in cert["issuer"]) + not_after = datetime.datetime.strptime(cert["notAfter"], "%b %d %H:%M:%S %Y %Z") + days_left = (not_after.replace(tzinfo=datetime.timezone.utc) - datetime.datetime.now(datetime.UTC)).days + + info(f"Subject : {subject.get('commonName')}") + info(f"Issuer : {issuer.get('organizationName')}") + info(f"Expires : {not_after.date()} ({days_left} days)") + + sans = [v for t, v in cert.get("subjectAltName", []) if t == "DNS"] + info(f"SANs : {', '.join(sans)}") + + if days_left < 30: + bad(f"Certificate expires in {days_left} days!") + add("HIGH", "Certificate near expiry", f"{days_left} days remaining") + else: + ok("Certificate valid and not near expiry") + + except Exception as e: + bad(f"TLS check failed: {e}") + add("HIGH", "TLS check failed", str(e)) + + +# ── 3. Security Headers ─────────────────────────────────────────────────────── +def check_headers(): + section("3. HTTP Security Headers") + r = get("/") + if not r: + bad("Could not reach target"); return + + # Show server / technology leakage + for leak_header in ("Server", "X-Powered-By", "X-AspNet-Version", "X-Generator"): + val = r.headers.get(leak_header) + if val: + warn(f"Version disclosure — {leak_header}: {val}") + add("LOW", f"Version disclosure: {leak_header}", val) + + for header, description in SECURITY_HEADERS.items(): + val = r.headers.get(header) + if val: + ok(f"{header}: {val}") + else: + bad(f"Missing: {header} ({description})") + severity = "HIGH" if header in ("Strict-Transport-Security", "Content-Security-Policy", "X-Frame-Options") else "MEDIUM" + add(severity, f"Missing header: {header}", description) + + +# ── 4. Cookie Flags ─────────────────────────────────────────────────────────── +def check_cookies(): + section("4. Cookie Security Flags") + r = get("/") + if not r: return + + raw_cookies = r.headers.get("Set-Cookie", "") + if not raw_cookies: + info("No Set-Cookie header on homepage (may appear after login)") + return + + for cookie_str in raw_cookies.split(","): + name = cookie_str.split("=")[0].strip() + flags = cookie_str.lower() + issues = [] + if "httponly" not in flags: + issues.append("missing HttpOnly") + if "secure" not in flags: + issues.append("missing Secure") + if "samesite" not in flags: + issues.append("missing SameSite") + if issues: + warn(f"Cookie '{name}': {', '.join(issues)}") + add("MEDIUM", f"Cookie flags: {name}", "; ".join(issues)) + else: + ok(f"Cookie '{name}' has all recommended flags") + + +# ── 5. Sensitive File Exposure ──────────────────────────────────────────────── +def check_sensitive_paths(): + section("5. Sensitive Path Exposure") + for path in SENSITIVE_PATHS: + r = get(path, allow_redirects=False) + if r is None: + continue + if r.status_code == 200: + size = len(r.content) + if path in ("/robots.txt", "/sitemap.xml"): + info(f"{r.status_code} {path} ({size}B)") + add("INFO", f"Found: {path}", r.text[:300]) + else: + bad(f"{r.status_code} {path} ({size}B) ← EXPOSED") + add("HIGH", f"Sensitive path exposed: {path}", f"HTTP 200, {size} bytes") + elif r.status_code in (301, 302): + loc = r.headers.get("Location", "") + info(f"{r.status_code} {path} → {loc}") + elif r.status_code == 403: + warn(f"403 {path} (exists but forbidden — still interesting)") + add("LOW", f"Forbidden path (exists): {path}") + + +# ── 6. robots.txt & sitemap ─────────────────────────────────────────────────── +def check_robots(): + section("6. robots.txt Analysis") + r = get("/robots.txt") + if r and r.status_code == 200: + info("robots.txt found:") + disallowed = [l for l in r.text.splitlines() if "Disallow" in l] + for line in disallowed: + warn(f" {line.strip()} ← attacker will check these first") + path = line.split(":", 1)[-1].strip() + add("INFO", "robots.txt Disallow path", path) + else: + info("No robots.txt found") + + +# ── 7. Open Redirect ───────────────────────────────────────────────────────── +def check_open_redirect(): + section("7. Open Redirect Probe") + redirect_params = ["redirect", "url", "next", "return", "returnUrl", "goto", "dest", "target"] + for param in redirect_params: + for payload in OPEN_REDIRECT_PAYLOADS[:1]: # Just one payload for non-destructive check + r = get("/", params={param: payload}, allow_redirects=False) + if r and r.status_code in (301, 302, 303, 307, 308): + loc = r.headers.get("Location", "") + if "evil.com" in loc: + bad(f"Open redirect via ?{param}= → {loc}") + add("HIGH", "Open Redirect", f"?{param}={payload} → {loc}") + else: + info(f"Redirect on ?{param}= but to: {loc} (not vulnerable)") + # No redirect = not vulnerable to this param + + +# ── 8. XSS Canary in Search ─────────────────────────────────────────────────── +def check_xss(): + section("8. Reflected XSS Canary (search param)") + search_paths = ["/", "/repertoire", "/search"] + search_params = ["q", "s", "query", "search", "keyword", "term"] + + for path in search_paths: + for param in search_params: + r = get(path, params={param: XSS_CANARY}) + if r and XSS_CANARY in r.text: + bad(f"XSS canary reflected unencoded at {path}?{param}=") + add("CRITICAL", "Reflected XSS", f"{path}?{param}={XSS_CANARY}") + elif r and urllib.parse.quote(XSS_CANARY) in r.text: + ok(f"{path}?{param}= — canary is URL-encoded (likely safe)") + elif r and "<script>" in r.text.lower(): + ok(f"{path}?{param}= — canary is HTML-encoded (safe)") + + +# ── 9. Directory Listing ────────────────────────────────────────────────────── +def check_dir_listing(): + section("9. Directory Listing") + dirs = ["/uploads/", "/files/", "/media/", "/static/", "/assets/"] + for path in dirs: + r = get(path) + if r and r.status_code == 200: + markers = ["index of", "parent directory", "directory"] + if any(m in r.text.lower() for m in markers): + bad(f"Directory listing ENABLED at {path}") + add("HIGH", "Directory listing enabled", path) + else: + info(f"{path} → 200 but no listing detected") + + +# ── 10. CORS Misconfiguration ───────────────────────────────────────────────── +def check_cors(): + section("10. CORS Misconfiguration") + try: + r = requests.get( + TARGET_BASE + "/api", + headers={**HEADERS, "Origin": "https://evil.com"}, + timeout=TIMEOUT, + verify=True, + ) + acao = r.headers.get("Access-Control-Allow-Origin", "") + acac = r.headers.get("Access-Control-Allow-Credentials", "") + if acao == "*": + warn("CORS: Access-Control-Allow-Origin: * (wildcard — no credentials risk)") + add("LOW", "CORS wildcard origin", "ACAO: *") + elif acao == "https://evil.com": + bad("CORS reflects arbitrary Origin!") + add("HIGH", "CORS reflects arbitrary Origin", f"ACAO: {acao}, ACAC: {acac}") + if acac.lower() == "true": + add("CRITICAL", "CORS reflects Origin + credentials allowed", "Full CORS bypass") + else: + ok(f"CORS origin: '{acao}' (not reflected)") + except Exception as e: + info(f"CORS check skipped: {e}") + + +# ── 11. Subdomain Enumeration ───────────────────────────────────────────────── +def check_subdomains(): + section("11. Subdomain Enumeration (passive wordlist)") + parent = ".".join(TARGET_HOST.split(".")[-2:]) # erg.be + found = [] + for sub in SUBDOMAIN_WORDLIST: + fqdn = f"{sub}.{parent}" + try: + ip = socket.gethostbyname(fqdn) + info(f"Found: {fqdn} → {ip}") + found.append(fqdn) + add("INFO", "Subdomain found", f"{fqdn} → {ip}") + except socket.gaierror: + pass + if not found: + ok("No additional subdomains discovered from wordlist") + + +# ── 12. HSTS Preload ────────────────────────────────────────────────────────── +def check_hsts(): + section("12. HSTS & HTTPS Redirect") + # Check HTTP → HTTPS redirect + try: + r = requests.get(f"http://{TARGET_HOST}/", headers=HEADERS, timeout=TIMEOUT, + allow_redirects=False, verify=False) + if r.status_code in (301, 302): + loc = r.headers.get("Location", "") + if loc.startswith("https://"): + ok(f"HTTP redirects to HTTPS ({r.status_code})") + else: + bad(f"HTTP redirects to non-HTTPS: {loc}") + add("HIGH", "HTTP does not redirect to HTTPS", loc) + else: + bad(f"HTTP returns {r.status_code} (no HTTPS redirect)") + add("HIGH", "No HTTP→HTTPS redirect", f"Status: {r.status_code}") + except Exception: + pass + + r = get("/") + if r: + hsts = r.headers.get("Strict-Transport-Security", "") + if "max-age" in hsts: + age = re.search(r"max-age=(\d+)", hsts) + age_val = int(age.group(1)) if age else 0 + if age_val < 31536000: + warn(f"HSTS max-age too short: {age_val}s (recommend ≥31536000)") + add("LOW", "HSTS max-age too short", f"{age_val}s") + else: + ok(f"HSTS max-age OK: {age_val}s") + if "includeSubDomains" not in hsts: + warn("HSTS missing includeSubDomains") + if "preload" not in hsts: + warn("HSTS missing preload directive") + else: + bad("HSTS header missing entirely") + add("HIGH", "HSTS not set") + + +# ── Summary ─────────────────────────────────────────────────────────────────── +def print_summary(): + section("SUMMARY REPORT") + order = {"CRITICAL": 0, "HIGH": 1, "MEDIUM": 2, "LOW": 3, "INFO": 4} + color_map = { + "CRITICAL": Fore.RED, + "HIGH": Fore.YELLOW, + "MEDIUM": Fore.CYAN, + "LOW": Fore.WHITE, + "INFO": Fore.BLUE, + } + sorted_findings = sorted(findings, key=lambda f: order.get(f.severity, 9)) + counts = {k: 0 for k in order} + for f in sorted_findings: + counts[f.severity] = counts.get(f.severity, 0) + 1 + c = color_map.get(f.severity, "") + print(f" {c}[{f.severity:8s}]{Style.RESET_ALL} {f.title}") + if f.detail: + detail_preview = f.detail[:120].replace("\n", " ") + print(f" └─ {detail_preview}") + + print() + for sev, cnt in counts.items(): + if cnt: + c = color_map[sev] + print(f" {c}{sev}: {cnt}{Style.RESET_ALL}") + + print(f"\n Scan completed: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + print(f" Target: {TARGET_BASE}") + + +# ── Main ────────────────────────────────────────────────────────────────────── +if __name__ == "__main__": + print(f""" +{Fore.CYAN}╔══════════════════════════════════════════════════════════╗ +║ Passive Pentest Scanner — authorized use only ║ +║ Target : {TARGET_HOST:<44} ║ +╚══════════════════════════════════════════════════════════╝{Style.RESET_ALL} +""") + check_dns() + check_tls() + check_headers() + check_cookies() + check_sensitive_paths() + check_robots() + check_open_redirect() + check_xss() + check_dir_listing() + check_cors() + check_subdomains() + check_hsts() + print_summary()