mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-25 16:19:19 +02:00
470 lines
19 KiB
Python
470 lines
19 KiB
Python
#!/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 = "<script>alert(1)</script>"
|
|
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", "<title>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()
|