#!/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", "